İçeriğe geç
academia.sh

Ders 03 / 11

Çöp Toplayıcılar

Erişilebilirliğini yitiren bir nesnenin temizliğini kim üstleniyor: ne çağıran, ne programın oluşturduğu bir iş parçacığı — kayıtta hiç olmayan bir taraf. Güçlü referans dururken temizleme koşmaz, kaldırılınca koşar; zayıf ve hayalet referans birkaç istekte kuyruğa düşer, yumuşak referans bellek baskısı yokken direnir. `System.gc()` bir istektir, buyruk değil.

İçindekiler

Önceki derste öbekte duran değerlerin hâlâ erişilebilir olduğu varsayıldı. Ama bir nesneye giden son referans de kaybolabilir; program bir nesneyi bırakır, ona artık hiçbir çerçeveden hiçbir statik alandan ulaşılamaz, ve öbekte kimsenin okuyamadığı bir alan kalır. Bu ders o alanın temizliğini kimin üstlendiğini sorar.

İzlemeli çöp toplamanın kendisi — kök kümesi, işaretle ve süpür, nesil temelli toplama — İşletim Sistemi Kavramları kursunda kavram olarak kuruldu ve burada tekrarlanmaz. Bu ders kuramı değil, sonucu ölçer: erişilebilirliğini yitiren bir nesnenin temizliğini hangi iş parçacığı koşturuyor, ve bu iş bir referansın gücüne göre nasıl değişiyor?

Sorunun kendisi programın koduna bakılarak yanıtlanamaz. Bir nesneyi kim ürettiğimiz, alanlarına ne yazdığımız, referansını nereye taşıdığımız — hepsi kaynakta görünür. Ama o nesne artık erişilemez hâle geldiğinde ne olacağı, hangi iş parçacığının onunla ilgileneceği kaynakta hiç yazılı değildir; program yalnız bir öneride bulunabilir, kararı ne verdiğini ne de ne zaman verdiğini görebilir.

Erişilebilirliğini Yitiren Nesnenin Temizliğini Kim Üstleniyor

“Erişilebilirlik” önceki dersin kurduğu dört yerle doğrudan bağlantılıdır. Bir nesne, ona giden bir yol varsa erişilebilirdir: bu yol bir çağrı çerçevesindeki bir yerel değişkenden, bir sınıfın statik alanından, ya da erişilebilir başka bir nesnenin alanından başlayabilir. Yol bu üç başlangıç noktasının hiçbirinden kurulamıyorsa nesne erişilemez sayılır — programın kendisi artık ona ulaşamaz, ama nesne öbekte durmaya devam eder, ta ki biri onu temizleyene kadar. Bir nesneye kayıtlı bir temizleme eylemi bağlanabilir: nesne toplanmaya uygun hâle gelince bu eylem çalışır. Eylemi çalıştıran iş parçacığı ise ne çağıran iş parçacığıdır, ne de programın kendi oluşturduğu bir iş parçacığı — kayıtta hiç olmayan bir taraftır. Bu, önceki derste sınıf başlatması için kurulan iki taraflı ayrımın (çağıran / oluşturulan) yetmediği üçüncü bir durumdur.

Toplanmaya uygunluğun kendisi de tek bir kurala bağlıdır: bir nesneye giden güçlü referans durduğu sürece nesne toplanamaz — bu, kararı JVM’in verdiği bir sezgi değil, dilin kendisinin verdiği bir garantidir. Güçlü referans kaldırıldığında nesne toplanmaya uygun hâle gelir, ama toplanacağı an garanti edilmez; program yalnız bir istekte bulunabilir. Bu ikili yapı — erişilebilirlik derleme zamanında yazılmış bir gerçek değil, çalışma zamanında sürekli değişen bir durumdur, ve o durumun sonucuna göre verilecek karar da programın dışındadır — ölçümün neden bir mandal ve bir döngüye ihtiyaç duyduğunu açıklar: “temizlendi mi” sorusunun yanıtı tek bir okumada değil, tekrar eden bir yoklamada bulunur.

  • JV12 — Bir nesnenin toplanıp toplanmadığı kendi kurduğumuz temizleme eylemiyle izlenir; eylem çalıştığında bir mandalı düşürür, çalışmadığı sürece mandal düşmez.
  • JV13System.gc() bir istektir, buyruk değil; ölçüm bu isteği kısa aralıklarla yineler ve isteğin karşılık bulup bulmadığını mandal üzerinden okur — hiçbir süre ya da duraklama ölçülmez.
  • JV14 — Temizleme eylemini koşturan tarafın kimliği yalnız kendi kaydımızla okunur; kayıtta olmayan her iş parçacığı “bilinmeyen” sayılır.
// Kim.java — bir eylemi hangi is parcacigi kosturuyor: kendi kaydimizla okunur
import java.util.*;

class Kim {
    static final Map<Thread, String> etiketler = Collections.synchronizedMap(new IdentityHashMap<>());
    static void tanit(String etiket) { etiketler.put(Thread.currentThread(), etiket); }
    static String oku() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }
}
// Temizleme.java — guclu referans dururken temizleme kosmaz; kaldirilinca kosar ve kosturan taraf bilinmeyendir
import java.lang.ref.Cleaner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Temizleme {
    public static void main(String[] args) throws InterruptedException {
        Kim.tanit("cagiran");

        Cleaner temizleyici = Cleaner.create();
        CountDownLatch tamam = new CountDownLatch(1);
        String[] kimKosturdu = { null };

        Object nesne = new Object();
        temizleyici.register(nesne, () -> { kimKosturdu[0] = Kim.oku(); tamam.countDown(); });

        for (int i = 0; i < 3; i++) {
            System.gc();
            boolean kosti = tamam.await(50, TimeUnit.MILLISECONDS);
            System.out.println("guclu referans dururken, istek " + (i + 1) + ": " + (kosti ? "kosti" : "kosmadi"));
        }

        nesne = null;
        while (!tamam.await(50, TimeUnit.MILLISECONDS)) System.gc();
        System.out.println("guclu referans kaldirilinca             : kosti");
        System.out.println("temizleme eylemini kosturan taraf        : " + kimKosturdu[0]);
        System.out.println("kosturan taraf cagiran mi                : " + "cagiran".equals(kimKosturdu[0]));
    }
}
guclu referans dururken, istek 1: kosmadi
guclu referans dururken, istek 2: kosmadi
guclu referans dururken, istek 3: kosmadi
guclu referans kaldirilinca             : kosti
temizleme eylemini kosturan taraf        : bilinmeyen
kosturan taraf cagiran mi                : false

Nesneye giden nesne değişkeni elde durduğu sürece üç ayrı istek de karşılıksız kaldı — bu beklenmedik bir gecikme değil, dilin garantisidir: güçlü referans duruyorsa toplama olamaz, kaç kez istenirse istensin. nesne = null satırından sonra döngü yeniden istekte bulundu ve bu kez karşılık geldi. Temizleme eylemini koşturan tarafın kaydı Kim.oku() ile okundu ve “bilinmeyen” çıktı — eylem ne maini koşturan cagiran iş parçacığında çalıştı, ne de programın hiç oluşturmadığı bir taraf olmasının dışında bir açıklaması var. Program bu iş parçacığını hiç yaratmadı, adlandırmadı, kaydetmedi; buna karşılık kendi temizleme kodunu o koşturdu.

Önceki iki derste kayıtlı iki taraf yeterliydi: bir sınıfın başlatmasını ya çağıran ya da programın kendi oluşturduğu bir iş parçacığı koşturuyordu, ikisinin dışına hiç çıkılmadı. Bu ders üçüncü bir tarafı zorunlu kılıyor, çünkü temizleme eylemini koşturan iş parçacığı bu programın hiçbir satırında new Thread(...) ile üretilmedi. “Bilinmeyen” etiketi bir sahiplik iddiası değildir — iş parçacığının kendisi JVM’in içinde gerçekten var ve gerçekten çalışıyor, yalnızca bizim kaydımızda bir karşılığı yok. Bir programın çalışma zamanı yüzeyi, programcının kendi yazdığı iş parçacıklarından ibaret değildir.

Dört Referans Gücü ve Referans Kuyruğu

Bir nesneye erişimin kendisi de tek bir güçte değildir. Güçlü referans yukarıda görüldüğü gibi toplamayı tamamen engeller. Yumuşak referans nesneyi tutar, ama JVM’e bellek baskısı altında bırakabileceğini söyler. Zayıf referans nesneyi hiç tutmaz; başka güçlü bir referans kalmadığı an toplanmaya açıktır. Hayalet referans ise en zayıfıdır: get() çağrısı toplanmadan önce bile, her zaman null döner — hayalet referans bir nesneyi okumak için değil, toplandığı anı bir kuyruktan öğrenmek için vardır.

Bir referans bir ReferenceQueue’ya bağlanabilir; nesne toplanmaya uygun hâle geldiğinde referansın kendisi bu kuyruğa düşer — kuyruk, “hangi referans artık boşaldı” sorusunun yanıtını okumanın tek yoludur, çünkü referansı elinde tutan kod nesneyi kaybettiği anı kendiliğinden öğrenemez. Dört gücün sırası rastgele değildir: her biri bir öncekinden daha az direnç gösterir. Güçlü referans hiçbir koşulda bırakılmaz; yumuşak referans yalnız JVM gerçekten sıkışırsa bırakılır; zayıf referans ilk fırsatta bırakılır; hayalet referans ise zaten hiçbir zaman tutmaz, yalnız “toplandı” haberini taşır. Aşağıdaki ölçüm üç hedef nesneye üç ayrı güçte referans kurar ve hepsinin son güçlü bağını koparır.

  • JV15 — Dört referans nesnesi de aynı koşumda, aynı üç hedefe kurulur; hangi referansın kuyruğa düştüğü nesne kimliğiyle (==) ayrılır, sırayla değil.
// ReferansDeney.java — dort referans gucu: zayif ve hayalet kuyruga duser, yumusak direnir
import java.lang.ref.*;

public class ReferansDeney {
    public static void main(String[] args) throws Exception {
        ReferenceQueue<Object> kuyruk = new ReferenceQueue<>();

        Object hedefZayif = new Object();
        Object hedefYumusak = new Object();
        Object hedefHayalet = new Object();

        WeakReference<Object> zayif = new WeakReference<>(hedefZayif, kuyruk);
        SoftReference<Object> yumusak = new SoftReference<>(hedefYumusak, kuyruk);
        PhantomReference<Object> hayalet = new PhantomReference<>(hedefHayalet, kuyruk);

        System.out.println("hayalet.get() toplanmadan once bile: " + hayalet.get());

        hedefZayif = null;
        hedefYumusak = null;
        hedefHayalet = null;

        boolean zayifDustu = false, hayaletDustu = false;
        for (int i = 0; i < 50 && !(zayifDustu && hayaletDustu); i++) {
            System.gc();
            Reference<?> r;
            while ((r = kuyruk.poll()) != null) {
                if (r == zayif) zayifDustu = true;
                if (r == hayalet) hayaletDustu = true;
            }
            if (!(zayifDustu && hayaletDustu)) Thread.sleep(20);
        }

        System.out.println("zayif referans kuyruga dustu mu   : " + zayifDustu);
        System.out.println("hayalet referans kuyruga dustu mu : " + hayaletDustu);
        System.out.println("zayif.get() (toplandiktan sonra)  : " + zayif.get());
        System.out.println("yumusak kuyruga hic dustu mu      : " + (kuyruk.poll() != null));
        System.out.println("yumusak.get() (bellek baskisi yok): " + (yumusak.get() != null ? "hala erisilebilir" : "null"));
    }
}
hayalet.get() toplanmadan once bile: null
zayif referans kuyruga dustu mu   : true
hayalet referans kuyruga dustu mu : true
zayif.get() (toplandiktan sonra)  : null
yumusak kuyruga hic dustu mu      : false
yumusak.get() (bellek baskisi yok): hala erisilebilir

hayalet.get()in ilk satırda bile null vermesi bir toplama sonucu değil, sözleşmenin kendisidir: hayalet referans hiçbir zaman nesneyi geri vermez, yalnız kuyruğa düşerek “toplandı” der. İstek döngüsü birkaç kez tekrarlandıktan sonra hem zayıf hem hayalet referans kuyruğa düştü; zayıf referansın get()i de o noktadan sonra null’a döndü — nesneyi tutmadığı için, güçlü bağ koptuğu an toplanmaya açık hâle geldi. Yumuşak referans ise aynı istek turlarında kuyruğa hiç düşmedi ve get()i hâlâ nesneyi verdi: JVM, bellek sıkışmadığı sürece yumuşak referansı bozmayı tercih etmedi. Üç referansın da hedefine giden son güçlü bağ aynı anda koptu; farkı yaratan tek şey referansın gücüydü.

Bu üç sonuç, üç referans türünün neden farklı işler için kullanıldığını da açıklıyor. Bir nesneyi başka hiçbir yerde tutmayan, yalnız “hâlâ var mı” diye izleyen bir kayıt zayıf referans ister — kayıt kendisi nesnenin ömrünü uzatmamalıdır. Yeniden hesaplanabilecek ama hesaplaması pahalı bir sonucu saklamak isteyen bir kayıt yumuşak referans ister — bellek boşken sonucu tutsun, sıkışınca bıraksın. “Nesne artık kesin gitti” haberini beklemek isteyen bir kod ise hayalet referans ister — nesneyi hiç geri istemez, yalnız kuyruktan haberi alır. Üçü de aynı ReferenceQueue düzeneğini kullanır; farkı yaratan yalnız referansın kendi gücüdür.

Bu düzenek, bir önceki bölümde ölçülen Cleaner ile de aynı temelden gelir: standart kitaplığın kendi belgesi Cleaner’ın hayalet erişilebilirlik üzerine kurulduğunu söyler — her kayıtlı temizleme eylemi, arka planda tam olarak yukarıdaki gibi bir hayalet referans ve bir kuyruk çiftiyle izlenir. İki ölçüm de aynı soruyu iki ayrı düzeyden sorar: Temizleme hazır bir API üzerinden “kim koşturuyor” sorusunu, ReferansDeney ise onun temelindeki düzeneği elle kurarak “ne zaman ve hangi güçte koşuyor” sorusunu ölçer.

Sınırlayıcı Ölçüm: Bir İstek Ne Zaman Koştuğunu Söylemez

Her iki ölçüm de aynı örüntüyü paylaşıyor: System.gc() bir kez çağrılmadı, bir döngü içinde yinelendi. Bunun nedeni gecikme değil, çağrının doğasıdır — System.gc() bir istektir, bir buyruk değil; JVM bu isteği yok sayabilir, erteleyebilir ya da farklı bir biçimde karşılayabilir. Tek bir çağrının karşılıksız kalması “toplama olmadı” anlamına gelmez, yalnızca “bu istekte olmadı” anlamına gelir. Bu, birinci dersteki kör noktayla aynı biçimdedir: orada boş bir satır “tetiklenmedi” demekti, burada karşılıksız bir istek “reddedildi” değil “henüz karşılık bulmadı” demektir. Tek fark, oradaki sınıf başlatması istendiğinde kesin olarak olan bir şeydi (koşulu doğruysa mutlaka çalışırdı); buradaki toplama isteği ise koşulu doğru olsa bile (nesne gerçekten toplanmaya uygun olsa bile) JVM’in kendi takdirine bağlıdır.

Ders bu yüzden isteği bir mandal düşene kadar yineler ve ne zaman koştuğunu hiç ölçmez — yalnız koştuğunda kimin koşturduğunu ölçer. Bu ayrım rastgele değildir: “ne zaman” sorusunun yanıtı çalıştığı makinenin o anki bellek durumuna, başka programların yükünü ne kadar paylaştığına bağlıdır ve koşumdan koşuma değişir — ölçülüp tabloya yazılacak belirlenimci bir sayı vermez. “Kim” sorusunun yanıtı ise değişmez: hangi makinede, hangi yükte koşarsa koşsun, temizleme eylemini koşturan taraf hep kayıtta olmayan bir taraftır, çünkü bu JVM’in kendi tasarımının bir sonucudur, o anki yük durumunun değil. Bu ikisinin ayrımı bu kursun genel kuralının bir örneğidir: koşuma bağlı olan (ne zaman) tabloya girmez, koşumdan koşuma sabit kalan (kim, kaç, hangi taraf) girer.

İki ölçüm de bunun ötesine geçmiyor: hangi toplayıcı algoritmasının çalıştığı, bir duraklamanın ne kadar sürdüğü, hangi neslin ne kadar büyüdüğü, ne kadar bellek geri kazanıldığı — bunların hiçbiri bu derste yazılmıyor, çünkü hepsi gerçekleştirime ve çalıştığı makineye bağlıdır. Kursun adında “Başarım” geçmesi bunu değiştirmez: burada sayılan şey yalnız koşturan taraf ve kuyruğa düşen referans sayısıdır.

Özet

  • Erişilebilirliğini yitiren bir nesnenin temizliğini koşturan taraf ne çağıran iş parçacığıdır ne de programın oluşturduğu bir iş parçacığı — kayıtta hiç olmayan, “bilinmeyen” bir taraftır.
  • Güçlü referans durduğu sürece bir nesne toplanamaz; bu bir sezgi değil dilin garantisidir. Güçlü referans kaldırılınca nesne toplanmaya açılır, ama toplanacağı an garanti edilmez.
  • Zayıf ve hayalet referans, güçlü bağ koptuktan sonra birkaç istekte referans kuyruğuna düşer; yumuşak referans bellek baskısı yokken direnir. Hayalet referansın get()i toplanmadan önce bile her zaman null döner.
  • System.gc() bir istektir, buyruk değil; bu yüzden ölçüm isteği bir döngüyle yineler ve yalnız koştuğunda kimin koşturduğunu, ne zaman koştuğunu değil.
  • Nesil temelli toplama, kök kümesi ve işaretle-süpür kuramı bu derste tekrarlanmadı; bu ders yalnız erişilebilirliğin sonucunu — kimin temizlediğini ve hangi referansın ne zaman toplanmaya açıldığını — ölçtü.
  • Toplayıcı gerçekleştirimi, duraklatma süresi, nesil boyutu ve geri kazanılan bellek miktarı bu derste yazılmadı; hepsi ortama bağlıdır. Cleaner de bu ölçümdeki ReferenceQueue düzeneğinin kendisiyle, hayalet erişilebilirlik üzerine kurulur — iki ölçüm aynı sorunun iki ayrı düzeyidir.

Sonraki Adım

Bu derste sınıf dosyasında bulunmayan bir karar görüldü: hangi nesnenin ne zaman toplanacağı, programın kendisinin değil çalışma zamanının verdiği bir karardı, ve bu kararı kimin koşturduğu programın kaydından okunamadı — yalnız kendi kaydımızla ölçülebildi. Sınıf dosyasında bulunmayan başka bir karar daha var — bu kez temizlikle değil, hızla ilgili. Bir yöntemin gövdesi sınıf dosyasında hiç değişmeden dururken, aynı yöntem birinci çağrısında ile bininci çağrısında aynı biçimde koşmayabilir. Sıradaki ders bu farkı ölçer: kararı veren taraf yine programın kendisi değildir ve yine koda bakarak görünmez, ama bu kez topladığı şey çöp değil, çalışırken biriktirdiği bir gözlemdir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat