Overriding Object.finalize () gerçekten kötü mü?


34

Geçersiz kılmaya karşı başlıca iki argüman Object.finalize()şudur:

  1. Ne zaman aranacağına karar veremezsin.

  2. Hiç çağrılmayabilir.

Bunu doğru anlarsam, bunların Object.finalize()bu kadar nefret etmek için yeterince neden olduğunu sanmıyorum .

  1. Bir nesneyi serbest bırakmak için doğru zamanın geliştirici değil olup olmadığını belirlemek VM uygulamasına ve GC'ye bağlıdır. Ne zaman Object.finalize()aranacağına karar vermek neden önemlidir ?

  2. Normalde ve yanlış yaparsam beni düzelt Object.finalize(), GC'nin çalıştırılma şansı olmadan, başvurunun sonlandırıldığı tek zaman aranmaz. Bununla birlikte, başvurunun süreci sonlandırıldığında nesne yine de serbest bırakıldı. Bu yüzden Object.finalize()aranmadı çünkü aranmasına gerek yoktu. Geliştirici neden umursar ki?

Manuel olarak kapatmak zorunda olduğum (dosya tutamaçları ve bağlantılar gibi) kullandığım nesneleri kullandığımda çok sinirli oluyorum. Bir nesnenin bir uygulaması olup olmadığını sürekli kontrol etmem gerekiyor close()ve eminim ki geçmişte bazı noktalarda birkaç çağrıyı kaçırdım. close()Uygulamayı devreye sokarak bu nesneleri elden çıkarmak için VM ve GC'ye bırakmak neden daha kolay ve daha güvenli değil Object.finalize()?


1
Ayrıca not: Java 1.0 dönemindeki birçok API gibi, iş parçacığı anlambilimi de finalize()biraz dağılmış durumda. Bunu uygularsanız, aynı nesnede diğer tüm yöntemlere göre iş parçacığının güvenli olduğundan emin olun.
billc.cn

2
Sonlandırıcıların kötü olduğunu söyleyen insanları duyduğunuzda, programınız varsa, çalışmayı durduramaz; Bütün sonuçlandırma fikrinin oldukça yararsız olduğu anlamına gelir.
user253751

1
Bu soruya +1. Aşağıdaki cevapların çoğu, dosya tanımlayıcıları gibi kaynakların sınırlı olduğunu ve manuel olarak toplanması gerektiğini belirtir. Aynı şey bellek için de geçerliydi, bu nedenle bellek koleksiyonunda biraz gecikme kabul edersek, neden dosya tanımlayıcıları ve / veya diğer kaynaklar için kabul etmiyoruz?
Mbonnin

Son paragrafınıza hitap ederek, dosya tutamaçları ve bağlantılar gibi şeyleri sizin açınızdan çok az çaba sarf ederek başa çıkmak için Java'ya bırakabilirsiniz. Kaynaklarla deneyin bloğunu kullanın - cevaplarda ve yorumlarda zaten birkaç kez bahsedilir, ancak bence buraya koymaya değer. Bunun için Oracle öğreticisi docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Yanıtlar:


45

Tecrübelerime göre, geçersiz kılmanın tek bir nedeni var Object.finalize(), ama bu çok iyi bir sebep :

finalize()Eğer çağırmayı unutursanız, sizi bilgilendiren hata günlüğü kodunu yerleştirmek için close().

Statik analiz sadece önemsiz kullanım senaryolarında ihmalleri yakalayabilir ve başka bir cevapta belirtilen derleyici uyarıları, önemsiz olmayan bir şeyi yapmak için gerçekten devre dışı bırakmanız gereken şeyler hakkında çok basit bir görüşe sahiptir. (Tanıdığım veya duyduğum diğer programlayıcılardan çok daha fazla uyarım var, ancak aptal uyarıları etkinleştirmedim.)

Sonlandırma, kaynakların keşfedilmediğinden emin olmak için iyi bir mekanizma gibi görünebilir, ancak çoğu insan bunu tamamen yanlış bir şekilde görür: alternatif bir geri dönüş mekanizması olarak düşünürler, bunu otomatik olarak kurtaracak bir "ikinci şans" güvencesidir. unuttukları kaynakları elden çıkararak günü. Bu ölü bir yanlış . Herhangi bir şeyi yapmanın tek bir yolu olmalı: ya her zaman her şeyi kapatırsın ya da sonlandırma her zaman her şeyi kapatır. Ancak sonlandırma güvenilir olmadığından, sonlandırma olamaz.

Bu yüzden, Zorunlu İmha dediğim bu program var ve programcının her zaman açıkçaCloseable veya uygulayan her şeyi açıkça kapatmaktan sorumlu olduğunu belirtiyor AutoCloseable. (Kaynaklarla deneyin ifadesi hala kesin bir kapanış olarak sayılır.) Elbette, programcı unutabilir, bu yüzden sonuçlandırma devreye giriyor, ancak sonunda sihirli bir şey yapabilecek sihirli bir peri değil o close()çağrılan değildi, öyle değilonu çağırmaya teşebbüs etmek, kesin olarak (matematiksel kesinlikle), çok tembel veya yapamayacakları çok fazla işi bilerek yapmak için ona güvenecek n00b programcılarının orduları olacak. Bu nedenle, zorunlu elden çıkarma ile, sonlandırma close()çağrılmadığını keşfettiğinde, parlak kırmızı bir hata mesajı kaydeder ve programcıya, büyük harflerini büyük harflerle yazmasını söyler.

Ek bir fayda olarak, söylentiye göre “JVM önemsiz bir finalize () yöntemini görmezden gelecektir (örneğin, Nesne sınıfında tanımlandığı gibi herhangi bir şey yapmadan dönen sadece bir tane)”, böylece zorunlu elden çıkarma ile tüm sonlandırmalardan kaçınabilirsiniz. Sisteminizin genelindeki ek yük ( bu ek yükün ne kadar korkunç olduğu konusunda bilgi için alip'in cevabına bakınız ) finalize()yönteminizi şöyle kodlayın:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

Bunun arkasındaki fikir , derleme zamanında değeri bilinen Global.DEBUGbir static finaldeğişkendir, öyleyse falsederleyici tüm ifdeyim için hiçbir kod çıkarmaz , bu da önemsiz (boş) bir sonlandırıcı olur. Sınıfınızın sonlandırıcı yokmuş gibi muamele göreceği anlamına gelir. (C # 'da bu güzel bir #if DEBUGblokla yapılır , fakat ne yapabiliriz, bu javadır, kodda bariz bir şekilde ek yükü olan beyinde ek basitlik ödüyoruz.)

Zorunlu Elden Çıkarma hakkında daha fazla bilgi, noktaların Net olarak atılmasıyla ilgili ek tartışmalarla birlikte, burada: michael.gr: Zorunlu elden çıkarma, "Bertaraftan çıkarma" ile ilgili suistimal


2
@MikeNakis Unutma, Closeable ikinci kez çağrıldığında hiçbir şey yapmamak olarak tanımlanır: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Derslerim iki kez kapatıldığında bazen bir uyarı kaydettiğimi itiraf ediyorum, ancak teknik olarak bunu yapmanız gerekmiyor. Teknik olarak olsa, .close () işlevini bir defadan fazla kapatılabilir öğesinin kullanılması tamamen geçerlidir.
Patrick M,

1
@ usr, testinize güvenip güvenmeme konusunda ya da testlerinize güvenmeme konusunda tamamen kaynamaktadır. Eğer test güvenmiyorsanız, emin, devam edip etmek sonuçlandırma yükü acı da close() her ihtimale karşı. Eğer testim güvenilir değilse, o zaman sistemi üretime sokmamamın daha iyi olacağına inanıyorum.
Mike Nakis

3
@Mike, if( Global.DEBUG && ...yapının çalışması için bu nedenle JVM finalize()yöntemi önemsiz olarak görmezden gelecek Global.DEBUG, derleme zamanında (enjekte edilen vb. Yerine) ayarlanmalıdır, bu nedenle aşağıdakiler ölü kod olacaktır. super.finalize()İf bloğunun dışına yapılan çağrı Global.DEBUG, JVM'nin, süper sınıfın #finalize()da önemsiz olmasına rağmen , önemsiz olmadığını ( en azından HotSpot 1.8'deki değerinden bağımsız olarak) kabul etmesi için yeterlidir !
alip

1
@Mike, korkarım durum bu. Bağlandığınız makaledeki testin (biraz değiştirilmiş versiyonu) ve ayrıntılı GC çıktısı (şaşırtıcı derecede düşük performansla birlikte) nesnelerin kurtulan / eski nesil alana kopyalandığını ve tam yığın GC'ye ihtiyaç duyulduğunu onayladım kurtulmak.
alip

1
Genellikle göz ardı edilen, sonlandırıcıdaki kaynakları serbest bırakmayı çok tehlikeli bir hareket haline getiren beklenenden daha erken nesne toplama riskidir. Java 9'dan önce, sonlandırıcının hala kullanımdayken kaynağı kapatmayacağından emin olmanın tek yolu, nesne üzerinde sonlandırıcının ve kaynağı kullanan yöntem (ler) in senkronize edilmesidir. Bu yüzden işe yarıyor java.io. Bu tür bir iplik güvenliği istek listesinde değilse, finalize()
Holger

28

Manuel olarak kapatmak zorunda olduğum (dosya tutamaçları ve bağlantılar gibi) kullandığım nesneleri kullandığımda çok sinirli oluyorum. [...] close()Uygulamayı devreye sokarak bu nesneleri elden çıkarmak için VM ve GC'ye bırakmak neden daha kolay ve daha güvenli değil Object.finalize()?

Dosya tanıtıcıları ve bağlantıları (yani, Linux ve POSIX sistemlerindeki dosya tanımlayıcıları ) oldukça kıt bir kaynak olduğundan (bazı sistemlerde bunlardan 256 tanesi, bazılarında ise 16384 sınırlı olabilir; setrlimit (2) ). GC'nin bu tür sınırlı kaynakları tüketmemek için yeterince sık (veya doğru zamanda) çağrılacağının garantisi yoktur. Eğer GC yeterince çağrılmazsa (veya sonlandırma doğru zamanda yapılmazsa) bu (muhtemelen düşük) sınırına ulaşırsınız.

Sonlandırma, JVM'de "en iyi çaba" işidir. Çağrılmamış veya çok geç çağrılmış olabilir ... Özellikle, çok fazla RAM'iniz varsa veya programınız çok fazla nesne tahsis etmiyorsa (veya çoğu yeterince eski hale getirilmeden önce ölürse) bir kopya nesiller GC tarafından üretilir), GC oldukça nadir olarak adlandırılabilir ve sonlandırmalar çok sık çalışmayabilir (ya da hiç çalışmayabilir).

Yani closemümkünse dosya, açıkça açıklayıcılarının. Bunları sızdırmaktan korkuyorsanız, sonlandırmayı ana önlem olarak değil, ekstra bir önlem olarak kullanın.


7
Bir dosyayı veya sokete bir akışı kapatmanın normalde onu temizlediğini eklerdim. Akışları açık bırakmak, güç kesilirse, bağlantı kesildiğinde (bu aynı zamanda ağ üzerinden erişilen dosyalar için de bir risktir) vs. gereksiz yere veri kaybı riskini artırır.

2
Aslında, izin verilen dosya tanıtıcılarının sayısı düşük olsa da, bu gerçekten yapışkan nokta değildir, çünkü kişi bunu en azından GC için bir sinyal olarak kullanabilir. Gerçekten problemli olan a) GC'ye tamamen yönetilmeyen kaynakların ne kadar dayandığı hakkında GC'ye tamamen şeffaf değildir ve b) bu ​​kaynakların çoğu benzersizdir, bu nedenle diğerleri engellenebilir veya reddedilebilir.
Deduplicator

2
Ve eğer bir dosyayı açık bırakırsanız, onu kullanan başka birine müdahale edebilir. (Windows 8 XPS görüntüleyici, sana bakıyorum!)
Loren Pechtel

2
"[Dosya tanımlayıcıları] 'nı sızdırmaktan korkuyorsanız, sonlandırmayı ana önlem olarak değil, ekstra bir önlem olarak kullanın." Bu ifade bana balık gibi geliyor. Kodunuzu iyi tasarlarsanız, temizleme işlemini birden fazla yere yayan fazlalık kodunu gerçekten tanıtmanız gerekir mi?
mucaho

2
@BasileStarynkevitch İdeal bir dünyada, evet, artıklığın kötü olduğu, ancak pratikte tüm ilgili yönleri öngöremediğiniz, üzgün olmaktan daha mı güvenli olacağınız?
mucaho

13

Konuya bu şekilde bakın: yalnızca (a) doğru (aksi halde programınız yanlış) kodunu yazmalısınız (b) gerekli (aksi halde kodunuz çok büyük, bu da daha fazla RAM gerekli, daha fazla döngü harcanmış demektir) işe yaramaz şeyler, onu anlamak için daha fazla çaba, sürdürmek için daha çok zaman harcandı vb.)

Şimdi bir sonlandırıcıda ne yapmak istediğinizi düşünün. Ya gerekli. Bu durumda, bir sonlandırıcıya koyamazsınız, çünkü çağrılıp çağrılmayacağını bilmiyorsunuz. Yeterince iyi değil. Ya da gerekli değil - o zaman ilk başta yazmamalısınız! Her iki durumda da, sonlandırıcının içine koymak yanlış bir seçimdir.

(Not bu dosya akışı, kapanış gibi isim örnekleri göz onlar gerçekten gerekli değildir, ama onlar sanki. Sadece sisteminizde açık dosya tanıtıcısı üst sayıya kadar o, olur değil uyarı olduğunu senin kod yanlıştır. Ama bu sınır işletim sisteminin bir özelliğidir ve gerçekten, gerçekten öyle, bu nedenle daha da öngörülemeyen finalizers için JVM politikasına daha olduğunu dosya kolları boşa harcamayın önemlidir.)


Yalnızca "gerekli" olan bir kod yazmam gerekiyorsa, tüm GUI uygulamalarımdaki stillerin tümünü engellemeli miyim? Elbette hiçbiri gerekli değildir; GUI'ler şekillendirme olmadan gayet iyi çalışacak, sadece korkunç görünecekler. Sonlandırıcı hakkında, bir şey yapmak gerekebilir, ancak sonlandırıcıya koymak için yine de tamamdır, çünkü sonlandırıcı hiç değilse nesne GC sırasında çağrılır. Bir kaynağı, özellikle çöp toplamaya hazır olduğunda kapatmanız gerekirse , bir sonlandırıcı en uygunudur. Program ya kaynağınızı bırakmayı sonlandırır ya da sonlandırıcı aranır.
Kröw

Bir RAM ağır, pahalı, üretilebilir, kapatılabilir bir nesneye sahipsem ve onu zayıf bir şekilde referans vermeye karar verirsem, gerekmediğinde temizlenebilsem, o zaman finalize()bir gc döngüsünün gerçekten serbest kalması gerektiğinde onu kapatmak için kullanabilirim VERİ DEPOSU. Aksi halde, her kullanmam gerektiğinde yeniden oluşturmak ve kapatmak zorunda kalmak yerine nesneyi RAM'de tutardım. Tabii ki, açtığı kaynaklar nesne GCed olana kadar serbest bırakılmayacak, ne zaman olursa olsun, ancak kaynaklarımın belirli bir zamanda serbest bırakıldığını garanti etmem gerekmeyebilir.
Kröw

8

Sonlandırıcılara güvenmemek için en büyük nedenlerden biri sonlandırıcıda temizlik yapmaya özendirilebilecek kaynakların çoğunun çok sınırlı olmasıdır. Çöp toplayıcı yalnızca her sıklıkta çalışır, çünkü bir şeyin serbest bırakılıp bırakılmayacağını belirlemek için çapraz referanslar pahalıdır. Bu, nesnelerinizin gerçekten yok edilmesinden önce 'bir süre' olabileceği anlamına gelir. Örneğin, kısa ömürlü veritabanı bağlantılarını açan birçok nesneniz varsa, örneğin, bu bağlantıların temizlenmesi için sonlandırıcıların bırakılması, çöp toplayıcısının sonunda çalışmasını ve bitmiş bağlantıları serbest bırakmasını beklerken bağlantı havuzunuzu tüketebilir. Ardından, beklemeniz nedeniyle, hızlı bir şekilde bağlantı havuzunu hızla tüketen sıralı isteklerden oluşan büyük bir birikiminiz ortaya çıkıyor. O'

Ek olarak, kaynaklar ile deneyin kullanımı tamamlandığında 'kapatılabilir' nesnelerin kapatılmasını kolaylaştırır. Bu yapıya aşina değilseniz, göz atmanızı öneririm: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html


8

Kaynakların serbest bırakılması için sonlandırıcıya bırakılmasının neden genel olarak kötü bir fikir olduğu, sonlandırılabilir nesnelerin baştan sona getirilmesiyle birlikte gelir.

Gönderen Java teori ve pratik: Çöp toplama ve performans (Brian Goetz), finalizers arkadaşınız değildir :

Sonlandırıcılı nesneler (önemsiz olmayan bir finalize () yöntemine sahip olanlar), sonlandırıcısız nesnelere kıyasla önemli bir ek yüke sahiptir ve az miktarda kullanılmalıdır. Sonlandırılabilir nesneler hem tahsisatın daha yavaş, hem de toplanmanın daha yavaştır. Tahsis sırasında, JVM sonlandırılabilir nesneleri çöp toplayıcıya kaydetmeli ve (en azından HotSpot JVM uygulamasında) sonlandırılabilir nesneler diğer nesnelerin çoğundan daha yavaş bir tahsisat yolu izlemelidir. Benzer şekilde, sonlandırılabilir nesnelerin de toplanması daha yavaştır. Sonlandırılabilir bir nesneyi geri kazanabilmek için en az iki çöp toplama döngüsü (en iyi durumda) gerekir ve çöp toplayıcı sonlandırıcının çağırılması için fazladan çalışma yapmak zorundadır. Sonuç, nesneleri tahsis etmek ve toplamak için harcanan daha fazla zaman ve çöp toplayıcıya daha fazla baskı yapılması, çünkü ulaşılamaz sonlandırılabilir nesneler tarafından kullanılan hafıza daha uzun tutulur. Kesinleştiricilerin tahmin edilebilir herhangi bir zaman diliminde veya hatta hiç çalıştırılmasının garanti edilmemesi gerçeğiyle birleştirin ve sonlandırmanın kullanılması için doğru araç olan nispeten az durum olduğunu görebilirsiniz.


Mükemmel nokta Genel gider performansından kaçınmanın bir yolunu sağlayan cevabımı görün finalize().
Mike Nakis

7

En azından kaçınmamın en sevdiğim nedeni, Object.finalizebekledikten sonra nesnelerin sonuçlanabileceği değil, beklemeden önce sonuçlandırılması olabilir. Sorun, Java'nın artık erişilebilir olmadığına karar vermesi durumunda, hala kapsamda olan bir nesnenin kapsamdan çıkmadan sonlandırılabilmesi değildir.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

Daha fazla ayrıntı için bu soruya bakın. Daha da eğlenceli olanı, bu kararın sadece sıcak nokta optimizasyonunun devreye girmesinden sonra verilebileceği ve bu da hata ayıklamak için acı verici olacağıdır.


1
Mesele şu ki HasFinalize.stream, ayrı ayrı sonlandırılabilir bir nesne olmalı. Yani, HasFinalizesonlandırılmamalı veya temizlemeye çalışılmaması gerekir stream. Ya da gerekirse, streamerişilemez hale getirmelidir .
acelent


4

Bir nesnenin close () uygulamasının olup olmadığını sürekli kontrol etmeliyim ve geçmişte bazı noktalarda birkaç çağrıyı kaçırdığıma eminim.

Eclipse, ben bunu uygular yakın bir şey unutmak çalışanlara uyarı olsun Closeable/ AutoCloseable. Bunun Eclipse olayı olup olmadığından veya resmi derleyicinin bir parçası olup olmadığından emin değilim, ancak size yardımcı olması için benzer statik analiz araçlarını kullanmayı düşünebilirsiniz. Örneğin FindBugs, bir kaynağı kapatmayı unuttuğunuzu kontrol etmenize yardımcı olabilir.


1
Söylemesi iyi fikir AutoCloseable. Kaynakları denemelerle kaynakları yönetmeyi beyin ölümüne kolaylaştırır. Bu, söz konusu argümanların birkaçını geçersiz kılar.

2

İlk sorunuza:

Bir nesneyi serbest bırakmak için doğru zamanın geliştirici değil olup olmadığını belirlemek VM uygulamasına ve GC'ye bağlıdır. Ne zaman Object.finalize()aranacağına karar vermek neden önemlidir ?

Peki, JVM, bir nesneye tahsis edilmiş depolamayı geri almanın iyi bir nokta olduğunu belirleyecektir. Bu mutlaka kaynak temizlenirken, gerçekleştirmek istediğiniz zaman değildir.finalize() . Bu, SO'daki “finalize () ile Java 8'de kolayca erişilebilir nesneye çağrılan” sorusunda gösterilmiştir. Orada, bir close()yöntem bir yöntem tarafından çağrılırken finalize(), aynı nesnenin akıştan okuma girişimi hala beklemededir. Bu yüzden finalize()geç kalmak üzere bilinen iyi bilinen ihtimalin yanı sıra, çok erken çağrılma olasılığı da var.

İkinci sorunuzun öncülü:

Normalde ve yanlış yaparsam beni düzelt Object.finalize(), GC'nin çalıştırılma şansı olmadan, başvurunun sonlandırıldığı tek zaman aranmaz.

sadece yanlış. JVM'nin kesinleşmeyi desteklemesi şartı yoktur. Tamamen yanlış değildir, çünkü başvurunuzun hiç bitmeyeceğini varsayarak, “sonlandırma gerçekleşmeden önce başvuru sonlandırılmıştır” olarak yorumlayabilirsiniz.

Ancak, orijinal ifadenizin “GC” si ile “sonlandırma” terimi arasındaki küçük farklılığa dikkat edin. Çöp toplama işlemi kesinleşmeye açıktır. Bellek yönetimi bir nesneye erişilemez olduğunu tespit ettiğinde, herhangi bir özel özelliği yoksa, alanını geri alabilirfinalize() metoda veya sonlandırma basitçe desteklenmiyorsa, veya sonlandırma için nesneyi zorlayabilir. Bu nedenle, bir çöp toplama döngüsünün tamamlanması, sonlandırıcıların gerçekleştirildiği anlamına gelmez. Bu daha sonra sıra işlendiğinde ya da hiçbir zaman gerçekleşmeyebilir.

Bu nokta ayrıca, sonlandırma desteğine sahip JVM'lerde bile, kaynak temizliği için ona güvenmenin tehlikeli olmasının nedenidir. Çöp toplama, hafıza yönetiminin bir parçasıdır ve bu nedenle hafıza ihtiyaçları tarafından tetiklenir. Çöp toplama işleminin hiçbir zaman gerçekleşmemesi olasıdır, çünkü çalışma süresi boyunca yeterli bellek vardır (yine de, “GC çalışma şansı vermeden önce uygulama sonlandırıldı” açıklamasına uygun). GC'nin çalışması da mümkündür ancak daha sonra geri kazanılmış yeterli bellek vardır, bu nedenle sonlandırıcı sırası işlenmez.

Başka bir deyişle, bu şekilde yönetilen yerel kaynaklar hala bellek yönetimine yabancıdır. Bir OutOfMemoryErroranın ancak hafızayı boşaltmak için yeterli teşebbüsten geçtikten sonra atılması garanti edilse de , bunun yerel kaynaklar ve sonlandırma için geçerli değildir. Bir dosya açmanın, kaynakların yetersiz kalması nedeniyle başarısız olması, sonlandırma kuyruğu ise, bu kaynakların serbest bırakılmasıyla sonuçlandı.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.