JavaProgramlamaKıdemli Java Geliştirici

JDK sınıflarında yerel belleği yöneten explicit kaynak serbestleştirmenin otomatik temizleme ile rekabet ettiği zaman ortaya çıkan senkronizasyon tehlikesi nedir, **Inflater** uygulaması ile örneklendirilmiştir?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

Tarihçe: Java 9 öncesinde, Inflater ve Deflater gibi sınıflarda yerel kaynak yönetimi Object.finalize() yöntemine dayanıyordu. Bu mekanizma, öngörülemezlik, ciddi performans kaybı ve çöp toplamanın gecikmesine neden olabilecek nesne yeniden doğumu riski nedeniyle kullanımdan kaldırıldı. Java 9, temizleme mantığını nesnenin yaşam döngüsünden ayrıştırırken, nesnenin temizleme sırasında erişilemez olmasını sağlayan PhantomReference ve ReferenceQueue kullanan modern bir alternatif olarak Cleaner API'sini tanıttı.

Problem: Inflater uygulamasında, temel yerel z_stream yapısının, yerel bellek sızıntılarını önlemek üzere end() yöntemi aracılığıyla açıkça serbest bırakılması gerekmektedir. Bir uygulama iş parçacığı end() yöntemini açıkça çağırdığında, Cleaner iş parçacığı aynı anda kayıtlı temizleme eylemini yürütmeye çalışıyorsa, bir yarış durumu ortaya çıkar. Uygun bir senkronizasyon olmadan, her iki iş parçacığı da aynı yerel işaretçiyi serbest bırakmaya çalışabilir, bu da çift serbest bırakma hatasına veya bir iş parçacığının diğerinin serbest bıraktığı kaynağa erişmesine (serbest sonrası kullanım) neden olarak JVM çökmesine yol açabilir (SIGSEGV) yerel zlib kütüphanesinde.

Çözüm: Çözüm, yerel temizlemenin hangi iş parçacığı tarafından başlatıldığına bakılmaksızın tam olarak bir kez çalıştırılmasını sağlamak için bir AtomicBoolean durum bayrağı kullanır. Hem açık end() yöntemi hem de Cleaner'ın temizleme eylemi bu bayrağın üzerinde karşılaştırma ve ayarlama (CAS) işlemi gerçekleştirir. Sadece bayrağı false'dan true'ya başarıyla geçiren iş parçacığı, yerel serbest bırakma rutinini çağırmak için ilerler. Bu kilitsiz yaklaşım, sıkıştırma işlemleri için gereken yüksek performansı korurken, iş parçacığı güvenliğini garanti eder.

Hayattan bir durum

Yüksek verimli bir günlük sıkıştırma servisi, tahsisat aşamasını en aza indirmek için havuzlanmış Deflater örneklerini kullanarak günlük girişlerinin milyonlarını günlük işleme alır. Kaynak kullanımını optimize etmek için geliştiriciler, iş parçacıklarının Deflater örneklerini havuza geri bırakmadan önce end() yöntemini açıkça çağırdığı return-to-pool modelini uyguladı, aynı zamanda işleme hattındaki işlenmemiş istisnalardan kaynaklanan sızdırılan örneklerin geri kazanımı için çöp toplama mekanizmasını da kullandılar.

Sistem, zirve yükü altında ara sıra ancak kritik JVM çöküşleri (SIGSEGV) yaşadı ve çekirdek dökümleri yerel zlib kütüphanesinde bellek bozulması gösterdi. Araştırmalar, bir Deflater örneği havuza döndüğünde, uygulama iş parçacığının end()'i çağırdığını, ancak eğer örneğin aynı anda çöp toplama için uygun hale geldiğinde, Cleaner iş parçacığının da aynı yerel z_stream tutamacını temizlemeye çalıştığını ortaya çıkardı. Bu senkronize edilmemiş erişim, işlemin öngörülemez bir şekilde çökmesine yol açtı.

İlk çözüm olarak, her bir Deflater örneğine erişimi senkronize etmek için synchronized bloklar veya yöntemler kullanmayı düşünmüştük. Bu yaklaşım, karşıt durumu etkili bir şekilde önleyecekti ancak, yüksek frekanslı sıkıştırma hattında önemli bir içerik rekabeti ortaya çıkarıyor ve nesne birden fazla iş parçacığı tarafından yanlışlıkla erişildiğinde kilitlenmelere neden olabilirdi; bu da sınıfın iş parçacığı güvenliği sözleşmesini ihlal edebilirdi.

İkinci yaklaşım, temizleme durumunu izlemek için bir AtomicBoolean kullanmayı içeriyordu. Açık end() yöntemi ve Cleaner eylemi, yerel kaynağı etkilemeden önce bu bayrağı atomik olarak kontrol edip ayarlayacaktı. Bu, kilitsiz güvenlik sunarken minimal performans cezası sağladı, ancak yerel tutamağın atomik kontrol sonrasında erişilmediğinden emin olmak için dikkatli bir şekilde uygulanması gerekiyordu.

Üçüncü seçenek, tüm açık end() çağrılarını tamamen kaldırmak ve sadece kaynak yönetimi için Cleaner'a güvenmekti. Bu, karşıt durumu tamamen ortadan kaldırdı, ancak yerel bellek serbest bırakma zamanlamalarında öngörülemezlik getirdi ve geri toplama duraklamaları sırasında ciddi bellek baskısı yaratarak yerel yapıların tahsisat oranının gerisinde kalmasına neden olabiliyordu.

Ekip, kesin durum kontrolü sağladığı için AtomicBoolean yaklaşımını (Çözüm 2) seçti; böylece mümkün olduğunda (açık çağrı) hemen temizleme yapıldı ve Cleaner daha sonra çalıştığında güvenlik sağlanmış oldu. Sarma sınıfını AutoCloseable uygulayacak şekilde değiştirdiler ve atomik durum kontrolünün yerel serbest bırakmayı koruduğundan emin oldular. Bu, çökmeleri tamamen ortadan kaldırırken gereken verimliliği korudular, üretimde yerel bellek ile ilgili çökmeleri ortadan kaldırdılar.

Adayların genellikle gözden kaçırdığı noktalar

Cleaner API, Object.finalize()'deki nesne yeniden doğumunu nasıl önlüyor? Object.finalize()'de, nesne hala finalize() yöntemi çalıştığı zaman erişilebilir durumdadır çünkü this referansı geçerlidir; bu durum nesnenin kendisine statik bir alanda referans saklayarak yeniden doğmasına olanak tanır. Bu yeniden doğum, nesne sürekli olarak yeniden doğma gerçekleştirdiği takdirde çöp toplamanın sonsuz bir şekilde gecikmesine neden olur. Cleaner API bunu PhantomReference kullanarak önler. Cleaner'ın temizleme eylemi çalıştığında, referans (temizlenen nesne) zaten hayalet erişilebilir durumda olduğu için yeniden doğurulamaz; çünkü onunla ilgili herhangi bir güçlü, yumuşak veya zayıf referans yoktur. Temizleme eylemi bir Runnable'dır ve bu da nesnenin kendisi üzerinde bir yöntem değildir; bu durum, nesnenin tüm temizleme süreci boyunca erişilemez durumda kalmasını sağlar.

Thread.interrupt(), JVM kapanışı sırasında bir Cleaner iş parçacığını durdurmak için neden etkisizdir ve bu ne tür sonuçlar doğurur? Cleaner iş parçacığı, hayalet referanslarının kullanılabilir hale gelmelerini beklemek üzere devamlı olarak ReferenceQueue.remove()'ta bloku açık bir iş parçacığıdır. ReferenceQueue.remove() interrupt'lara yanıt vererek InterruptedException fırlatır; ancak Cleaner uygulaması bu istisnayı yakalar ve sonsuz döngüsüne devam eder, etkili bir şekilde interrupt'ları yok sayar. Bu tasarım, kritik kaynak temizlemenin kapanış dizileri sırasında bile tamamlanmasını garanti eder. Ancak, kayıtlı bir temizleme eylemi sonsuz bir döngüde takılı kalırsa (örneğin, bir ağ zaman aşımını beklerken), Cleaner iş parçacığı asla sonlanmaz. Bu, diğer daemon olmayan iş parçacıklarının, temizleyicinin serbest bırakması gereken kaynakları beklemesi durumunda JVM'ın düzgün bir şekilde kapanmasını engelleyebilir.

Cleaner'ın temizleme eylemi, temizlenen nesneye bir güçlü referans yakalarsa hangi felaket bellek sızıntısı meydana gelir? Eğer Cleaner.register()'a geçirilen Runnable, nesneyi (örneğin, this::cleanupMethod veya this'i referans alan bir lambda aracılığıyla) güçlü bir referans ile yakalarsa, bu ölümcül bir referans döngüsü oluşturur. Cleaner içsel olarak Cleanable nesnelerinin bir kümesini tutar; her biri temizleme Runnable'ına bir referans tutar. Eğer bu Runnable, orijinal nesneyi referans alıyorsa, nesne, Cleaner iş parçacığı tarafından güçlü bir şekilde erişilebilir durumda kalır. Sonuç olarak, nesne asla hayalet erişilebilir duruma geçmez, PhantomReference asla kuyruğa girmez ve temizleme eylemi asla çalışmaz. Bu arada, nesne çöp toplanamaz ve bu, her Cleaner'a kaydedilen nesne ile birlikte kritik bir bellek sızıntısına yol açar ve nihayetinde OutOfMemoryError meydana getirir.