---
title: 'Çöp Toplayıcılar'
source: 'https://academia.sh/tr/kurslar/jvm-ve-eszamanlilik/cop-toplayicilar'
course: 'JVM, Eşzamanlılık ve Başarım'
language: tr
updated: '2026-08-23T15:28:42+00:00'
license: 'CC BY-SA 4.0'
---

# Çö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.

Ö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 **istek**te 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.
- **JV13** — `System.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.

```java
// 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;
    }
}
```

```java
// 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 `main`i 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.

```java
// 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.
