Java 17 ve Öncesi Garbage Collector (Çöp Toplayıcı)

İçindekiler
- Giriş
- Garbage Collector (GC) Parametreleri
- GC Türleri ve Karşılaştırmaları
- Java 8, Java 11 ve Java 17 GC Performans Kıyaslamaları
- Optimizasyonlar
- Farklı GC Türlerinin Demo Uygulaması
- Demo Uygulama Çıktısı
- Sonuç ve Öneriler
- Kaynakça
1. Giriş
Çöp Toplama (Garbage Collection), program çalışırken kullanılmayan blokları tespit edip serbest bırakarak heap (dinamik bellek) yönetimini kolaylaştıran bir mekanizmadır. Çöp toplama bazı dillerde manuel olarak yönetilirken bazılarında otomatiktir. Örneğin C++’ da manuel olarak yönetilirken, Java’da böyle bir maliyet söz konusu değildir. Bu çalışmada çöp toplama yapısını Java üzerinden ele alacağız.
Java programlarında nesneler oluşturulduğunda, bu nesneler dinamik bellekte saklanır. Zamanla program daha fazla nesne oluşturduğunda, dinamik bellek dolmaya başlar ve bu durum programın yavaşlamasına hatta çökmesine neden olabilir. İşte bu noktada Garbage Collector devreye girerek bu sorunu önlemeye yardımcı olur. Garbage Collector tarafından yapılan işlemleri şöyle sıralayabiliriz:
- Program tarafından kullanılmayan ve erişilemez hale gelen nesneleri otomatik olarak tespit edip dinamik belleği bu nesnelerden temizler.
- Uygulama yığınındaki nesnelerin yaşam döngüsünü yönetir.
- Erişilemeyen nesneleri tanımlarken çalışan threadler (iş parçacıkları) tarafından hala erişilebilir olanları kontrol eder.
- Bir nesne artık referansa sahip olmadığında ve başka bir nesne tarafından kullanılmadığında, "çöp" olarak kabul edilir. Garbage Collector belirli periyotlarla yazımızda değineceğimiz algoritmaları kullanarak temizlik işlemi yapar.
- Dinamik belleğin verimli olarak kullanılabilmesi için çöp toplama işlemin belirli periyotlarla yapılması gerekmektedir. Java’daki Garbage Collector, geliştiricileri dinamik bellek yönetimi karmaşıklığından koruyarak geliştiricilere kod yazmaya odaklanma fırsatı sunar.
Java, Garbage Collector yapısının, uygulamaların farklı ihtiyaçları bazında kişiselleştirilebilmesi için bizlere bazı parametreler sunmuştur. Çalışmamızda ilk olarak bu parametrelerden bahsedelim.
2. Garbage Collector Parametreleri
Adı | Parametre Adı | Varsayılan Değer | Açıklama | |
---|---|---|---|---|
Garbage Collector Türü | -XX:+UseSerialGC -XX:+UseParallelGC -XX:+UseParNewGC veya -XX:+UseConcMarkSweepGC -XX:+UseG1GC | G1 GC (Garbage-First Garbage Collector) | Hangi Garbage Collector türünü kullanmak istediğimizi belirtmek için bu parametre kullanılır. | |
Başlangıç Dinamik Bellek Boyutu | Fiziksel hafızanın 1/64'üdür | Uygulamanın başlangıçta dinamik bellek için ayıracağı hafızanın boyutunu belirtir. | ||
Parallel GC (Paralel Çöp Toplayıcısı) | Maksimum Dinamik Bellek Boyutu | -Xmx | Fiziksel hafızanın 1/64'üdür | Uygulamanın en çok kullanabileceği hafıza boyutunu belirtir. |
Minimum Dinamik Bellek Boyutu | -Xms | - | Uygulamanın en az kullanabileceği hafıza boyutunu belirtir. | |
Maksimum Duraklama Süresi | -XX:MaxGCPauseMillis | - | Bir çöp toplama süresinin istenen en yüksek değerini belirtir | |
Garbage Collector Zaman Oranı | -XX:GCTimeRatio | - | Çöp toplama işleminde geçen zamanın uygulamada geçen zamana oranıdır. Örneğin 19 olarak belirtilirse, 19 zaman biriminin uygulamada 1 zaman biriminin temizlemede geçmesini istiyoruz demektir. | |
Concurrent Mark and Sweep Garbage Collector (Eşzamanlı İşaretle ve Geç Çöp Toplayıcısı) | Uygulamanın Hafıza Hatası Atma Limiti | -XX:-UseGCOverheadLimit | %98 | Uygulamanın ne zaman OutOfMemoryException atacağını belirten bir orandır. Eğer zamanın %X'i Garbage Collector içinde geçiyorsa ve son temizlemede sadece %(100-X)'lik bir temizleme yapılmışsa o zaman OutOfMemoryException atılır. |
Tabloda da karşımıza çıkan “Garbage-Collector Türü” parametresinden anlayacağımız üzere farklı Garbage Collector türleri bulunmaktadır. Bir sonraki başlığımızda bu türleri inceleyelim.
3. GC Türleri ve Karşılaştırmaları
Doğru GC seçimi karmaşıktır ve doğru seçimi yapabilmek için öncelikle hedeflerimizi anlamalıyız. Bu hedefler genellikle verimlilik (throughput), gecikme (latency) ve kaynak kullanımı (footprint) optimizasyonunu içerir. İdeal olarak, en iyi performansı elde etmek için bu faktörleri optimize etmeliyiz. Ancak, her algoritmanın kendi avantajları ve dezavantajları vardır.
Java, farklı uygulama gereksinimlerini karşılamak için birçok GC sunar. Uygulama için doğru Garbage Collector’ü seçmek, GC performansı üzerinde büyük etkisi olan bir karardır. Temel kriterler şunlardır:
- Verimlilik: Toplam zamanın ne kadarının işe yarar uygulama faaliyetine harcandığı ile dinamik bellek tahsisi ve çöp toplama zamanı arasındaki yüzdedir. Örneğin, işlem kapasitesi %95 ise, bu demek oluyor ki uygulama kodu %95 zamanında çalışıyor ve çöp toplama %5 zamanında çalışıyor. Verimliliğe odaklanan bir GC, Java uygulamasının daha yüksek işlem kapasitesine sahip olmasını sağlar.
- Gecikme: Uygulamanın tek bir işleminin ne kadar sürede tamamlandığına dair bir gösterge sağlar. Gecikmeye odaklanan GC algoritması, mümkün olduğunca az bekleme süresi oluşturmaya çalışır. Yani GC algoritması, işlemi mümkün olduğunca hızlı bir şekilde tamamlamaya çalışır.
- Kaynak Kullanımı: Bir GC işleminin sorunsuz çalışabilmesi için gereken dinamik belleği ifade eder.
Bu 3 metrik arasındaki ilişki genellikle her bir köşesinde bir metriğin bulunduğu üçgen içinde grafikleştirilir.

Şekil 1. GC Optimizasyon Metrikleri
Her GC algoritması, sağladıkları metriğe bağlı olarak bu üçgenin bir bölümünü işgal eder.
Farklı gereksinimler göz önüne alındığında yazımızda değineceğimiz Garbage Collector çeşitleri şunlardır:
- Serial GC (Sıralı Garbage Collector)
- Parallel GC (Paralel Garbage Collector)
- G1 GC (Çöp Öncelikli Garbage Collector)
- ZGC (Z Garbage Collector)
Garbage Collector | Odak Noktası | İçeriği |
---|---|---|
Serial GC | İz ve Başlatma Süresi (Footprint and Startup time) | Tek thread destekli stop-the-world (dünyayı durdur) sıkıştırma, eş zamanlı çalışma sırasında canlılık ve jenerasyonel toplama |
Parallel GC | Birim Zamanda Yapılan İş Miktarı | Çoklu thread destekli stop-the-world sıkıştırma ve jenerasyonel toplama |
G1 GC | Dengeli Performans | Çoklu thread destekli stop-the-world sıkıştırma ve jenerasyonel toplama |
ZGC | Gecikme | Her şey uygulama ile eş zamanlı |
Bu Garbage Collector çeşitlerini daha detaylı inceleyelim:
Serial GC
Serial GC, Java’daki en eski ve en basit GC uygulamasıdır. Serial GC, toplama işlemini gerçekleştirirken tüm threadleri dondurduğu ve tek bir thread üzerinde çalıştığı için tek thread’li ortamlarda kullanılır.
Serial GC, düşük duraklama gereksinimlerine sahip olmayan client-side (istemci tarafı) uygulamalar için uygundur. Multi-threaded (çok iş parçacıklı) uygulamalar gibi sunucu ortamlarında kullanmak iyi bir fikir değildir.
Serial GC, belirli donanım ve işletim sistemi yapılandırmalarında varsayılan olarak seçilir. Örneğin RAM limiti 1792 MB’den az olarak ayarlanmışsa veya CPU (işlemci) sayısı 2’den az ise otomatik olarak kullanılır.

Şekil 2. Serial GC
Serial GC’yi etkinleştirmek için -XX:+UseSerialGCVM etiketi kullanılır.
Parallel GC
Paralel GC sıklıkla işlem kapasitesinin gecikme süresinden daha önemli olduğu durumlarda kullanıldığından throughput collector (verim toplayıcısı) olarak bilinir. Uzun duraklamaların kabul edilebilir olduğu durumlarda, örneğin toplu veri işleme, toplu işler vb. gibi durumlarda, paralel collector kullanılabilir. Parallel collector’u Serial GC’den ayıran en önemli farkı Garbage Collector’u hızlandırmak için multi-thread’e (çoklu iş parçacığı) sahip olmasıdır.
Eğer uygulamanın gereksinimi yüksek işlem kapasitesine ulaşmak ise paralel collector uygun olabilir. Fakat uygulamada bir saniye ya da daha uzun duraklama riskini göze almak gerekebilir. Paralel collector, multi-processor (çoklu işlemci) veya multi-threaded makinelerde çalışan orta ve büyük veri kümelerine sahip uygulamalar için kullanılabilir.
Paralel GC’yi etkinleştimek için -XX:+UseParallelGC etiketi kullanılabilir. Bunun yanı sıra, ek derleyici seçenekleri ile parametrelerin birçoğunun konfigure edilmesine olanak tanır:
-
-XX:ParallelGCThreads=n, GC thread sayısını belirtir.
-
-XX:MaxGCPauseMillis=n, maksimum duraklama süresinin hedefini milisaniye cinsinden belirtir. Varsayılan olarak duraklama süresine bir sınırlama getirilmez, ancak bu seçenekle n veya daha az milisaniye süren duraklama süreleri beklenir.
-
-XX:GCTimeRatio=n, uygulamanın işlem kapasitesi hedefini elde etmeye yardımcı olur. Bu seçenek, çöp toplama işlemi için ayrılan süreyi 1/(1+n) oranında belirler. Örneğin, -XX:GCTimeRatio=24, 1/25 hedefini belirler, böylece toplam sürenin %4’ü çöp toplamaya harcanır. Varsayılan değer 99’dur, bu da %1’in çöp toplamaya harcandığı anlamına gelir.
Örneğin, java -XX:+UseParallelGC -XX:ParallelGCThreads=10 -jar yourApp.java komutu ile Paralel GC 10 thread ile etkinleştirilir. Ancak gerçek bir senaryoda thread sayısı işlemci sayısına dayalı olarak hesaplanır. Paralel GC, tarama işlemi için birden fazla thread kullanır, ancak nesneleri kaldırmak için tek bir thread kullanır.

Şekil 3. Parallel GC
G1 GC
G1 GC, büyük dinamik belleğe sahip çok işlemcili makineler için sunucu tipli bir collectordur. G1 GC ile dinamik bellek, birden çok bölgeye ayrılır ve her bölgede bağımsız olarak çöp toplama işlemi gerçekleştirilir. Bu, G1 GC’nin büyük bellekleri daha verimli bir şekilde yöneterek yüksek işlem kapasitesi elde etmesini ve çöp toplama duraklamalarının sıklığını ve süresini azaltmasını sağlar.
G1, belirli donanım ve işletim sistemleri üzerinde varsayılan olarak seçilir ve -XX:+UseG1GC seçeneği aracılığıyla etkinleştirilebilir. G1 GC uygulama ile birlikte bazı maliyetli işlemleri eş zamanlı olarak gerçekleştirdiği için çoğunlukla concurrent collector (eş zamanlı toplayıcı) olarak adlandırılır. G1 GC, aşağıdaki kriterlerden birini veya daha fazlasını karşılayan uygulamalar için yüksek işlem kapasitesi ve düşük gecikme süresi elde edebilir:
G1 GC, uygulamanın dinamik bellek kullanımını yaklaşık %10 azaltabilecek olan “string deduplication (karakter dizesi yinelenmelerinin giderilmesi)” optimizasyonu sunar. -XX:+UseStringDeduplication derleyici seçeneği ile G1 Garbage Collector’e yinelenen dizeleri bulma görevi verilir ve çöp toplama işlemi sırasında bu yinelenen dizeler üzerinde işlem yaparken yalnızca bir dizeye aktif bir referans tutar. Bu derleyici seçeneği dışında aşağıda derleyici seçenekleri şunlardır:
XX:+PrintStringDeduplicationStatistics, XX:+UseStringDeduplication seçeneği ile çalıştırıldığında detaylı bir şekilde yineleme (duplication) istatistiklerini yazdırır.
XX:StringDeduplicationAgeThreshold=n.n adet çöp toplama döngülerine ulaşan dize objelerinin yinelenen olarak kabul edilmesine neden olur. Varsayılan değer 3’tür.
ZGC
ZGC, tıpkı G1 gibi eş zamanlı olarak çalışabilen ve çok büyük belleklerle iyi çalışan düşük gecikme (low latency) sağlayan garbage collectordur. ZGC, uygulama thread’lerinin çalışmasını 10 ms’den fazla durdurmaz. Bu özelliklerinden dolayı, çok büyük miktarda belleğe ihtiyaç duyan ve çok kısa duraklama sürelerine gereksinim duyan uygulamalar için uygundur.
Z Garbage Collector, Java 15’te kullanılabilir özellik olarak tanıtıldığından Java 15’ten düşük bir JDK (Java Development Kit- Java Geliştirme Paketi) versiyonu kullanılması durumunda -XX:+UnlockExperimentalVMOptions -XX:+UseZGC komut satırı seçeneği ile kullanılabilir. Java 15’ten sonra ise deneysel modun eklenmesine ihtiyaç duyulmaz ve -XX:+UseZGC seçeneği ile ZGC etkinleştirilebilir. ZGC kullanırken maksimum dinamik bellek boyutunu ayarlamak oldukça önemlidir çünkü collector’un davranışı, tahsis hızı değişkenliğine ve veri kümesinin ne kadarının canlı olduğuna bağlıdır.
Gördüldüğü üzere farklı çeşitlerde Garbage Collector türleri bulunmaktadır. Peki bu türler arasında ne gibi bir performans farkı var? Gelin bir sonraki başlığımızda bu performans farklarından bahsedelim.
4. Java 8, Java 11 ve Java 17 GC Performans Kıyaslamaları
JDK 17, yeni dil özellikleriyle sınırlı kalmayıp önceki JDK sürümlerine göre önemli bir performans artışı sunmaktadır. Özellikle önceki uzun süreli destek sürümleri olan JDK 8 ve JDK 11 ile karşılaştırıldığında bu artış daha belirgin hale gelmektedir. İyileştirilen performansın büyük bir kısmı JVM (Java Virtual Machine-Java Sanal Makinesi)’deki yeni özellikler ve optimizasyonlardan kaynaklanmaktadır.
JDK 8’den bu yana gözlemlenen ilerlemeye göre, tüm GC’lerin genel olarak geliştiği görülmektedir. Bu gelişmeleri daha iyi görmek amacıyla, GC’ler özel bir testle değerlendirildi. Aynı zamanda bu test, çöp toplama performansının yanı sıra Java platformunun genel gelişimini de göstermektedir. Bu karşılaştırma, verimlilik, gecikme ve kaynak kullanımı metrikleri üzerinden değerlendirildi.
Uygulanan test, tüm GC algoritmalarının sonuçlarına bakılmaksızın aynı seviyede etkilenecek şekilde bir saat boyunca sabit bir yük altında çalıştırıldı.
Java 11 ile tanıtılan ZGC’nin, Java 11 ve Java 17 sürümleri arasında karşılaştırılmasını 3 ana başlıkta aşağıdaki gibi yapabiliriz:
Verimlilik

Şekil 4. GC Algortimalarının Java sürümleri arasındaki verimlilik karşılaştırma grafiği
Verimlilik anlamında tüm GC’lerde yeni bir java versiyonunda sürekli iyileştirme görülürken en büyük iyileştirmenin ZGC’de olduğu direkt göze çarpmaktadır. Bu performansın yalnızca GC iyileştirmesinden ziyade JIT derleyicisi gibi Java platformunun başka bölümlerinin de iyileştirilmesiyle dolaylı olarak GC performansına katkıda bulunduğunu göz önünde bulundurmak lazım.
JDK 8’in kullanıldığı durumlarda genellikle performans sorunları ve gecikmeler yaşanabiliyordu. Ancak JDK 11 ile birlikte, G1 GC ve Paralel GC’lerin iyileştirilmesi, uygulamalardaki performansı önemli ölçüde artırdı. Bu iyileştirmeler, dinamik bellek yönetimi ve çöp toplama süreçlerinde uygulamanın daha verimli çalışmasını sağladı. Asıl dikkat çeken gelişme, JDK 17 ile geldi. Bu sürümde tanıtılan ZGC algoritması, büyük ölçekli dinamik bellek yönetimini büyük ölçüde optimize etti. ZGC, büyük bellek havuzlarına sahip uygulamalarda daha istikrarlı ve yüksek performans sergileyerek JDK 11’e kıyasla belirgin bir gelişme sundu.
Sonuç olarak, JDK 8’den JDK 17’ye geçişlerde sürekli bir performans artışı görülmüş olup bu durum Java uygulamalarının daha hızlı, daha verimli ve daha istikrarlı çalışmasını sağlamaktadır. Bu gelişmeler, Java platformunun sürekli olarak güncellenerek daha rekabetçi ve güçlü hale gelmeye devam ettiğini göstermektedir.
Gecikme

Şekil 5. GC Algortimalarının Java sürümleri arasındaki gecikme karşılaştırma grafiği
Gecikme puanı, uygulamanın genel olarak cevap verme yeteneğini gösterir ve bu puan GC işlemlerinden kaynaklanan duraklamaları içerir. Bekleme zamanı grafiği GC’deki iyileştirmenin etkilerini daha detaylı gösterir. Tüm GC’ler Java versiyonları arttıkça gecikme sonucu açısından daha iyi performans göstermekte olup, ZGC’nin dahil edilmediği ölçümde G1 en iyi performansı vermektedir.

Şekil 6. GC Algortimalarının Java sürümleri arasındaki duraklama zamanı karşılaştırma grafiği
Bekleme zamanı uygulamanın GC işleri için durduğu zamandır. GC bekleme zamanları açısından tüm algoritmalar performansını arttırmakta olup en iyi performansı JDK 17’de 0,1 milisaniye olacak şekilde ZGC vermektedir. JDK 17’de gelen dinamik thread oluşturma özelliği sayesinde yeteri kadar thread’in GC tarafından kullanılması sağlanmış ve uygulama thread’i daha çok CPU zamanından yararlanmıştır. Sonuçta daha yüksek verimlilik ve daha düşük bekleme süresi elde edilmiştir.
Fazla hafızanın gecikme zamanı üzerinde etkisini incelemek için hafıza 128 GB’a çıkarılarak ölçümler yapılmıştır. ZGC bu değişiklikten etkilenmemiş G1 GC ise Paralel GC’den daha iyi tepki vermiştir.
Kaynak Kullanımı

Şekil 7. GC Algortimalarının Java sürümleri arasındaki kaynak kullanımı karşılaştırma grafiği
Şekil 6’daki grafik, üç farklı GC türünün maksimum dinamik bellek kullanımını karşılaştırmaktadır. Hem Paralel GC hem de ZGC, bu bağlamda oldukça istikrarlı bir performans sergilemektedir, bu nedenle burada ham verilere odaklanmak mantıklıdır. Grafikte net bir şekilde görüldüğü üzere, G1 GC, maksimum dinamik bellek kullanımında çok önemli bir gelişme kaydetmiştir. Bu gelişmenin temel nedeni, hatırlanan set yönetimini daha verimli hale getirmek amacıyla yapılan tüm özellikler ve geliştirmelerdir.
Diğer Garbage Collector türlerinin, dinamik bellek kullanımını azaltmasa bile, ek dinamik bellek kullanımına ihtiyaç duymadan performanslarını artırabildiklerini göz ardı etmemek önemlidir. Sonuç olarak grafik, farklı Garbage Collector’lerin maksimum dinamik bellek kullanımını karşılaştırarak performans iyileştirmeleri ve dinamik bellek yönetimi stratejilerinin uygulama performansına nasıl etki edebileceğini göstermektedir.
5. Optimizasyonlar
Garbage Collector alanında yapılan optimizasyonlar Java’nın son sürümlerinde performansın arttırılmasına yönelik önemli adımların atıldığını göstermektedir.
-
G1 GC için yapılan ölçümlere göre Java 17 Java 11’den %8,66 daha hızlı, Java 16’dan %2.41 daha hızlıdır.
-
Paralel GC için yapılan ölçümlere göre Java 17 Java 11’den %6,54 daha hızlı, Java 16’dan %0,37 daha hızlıdır.
-
Paralel GC G1 GC’den %16,39 daha hızlıdır.
Aşağıda Java 8 ile Java 17 arasında, GC algoritmaları üzerinde yapılan optimizasyonlardan bahsedilmektedir.
Parallel GC
Dinamik paralel referans işleme özelliği eklendi. Bu özellik, sistemdeki işlem yüküne göre thread sayısını otomatik olarak ayarlayabilmektedir. Bu sayede, işlem yükü arttığında daha fazla thread kullanarak daha hızlı ve verimli bir dinamik bellek temizleme süreci gerçekleştirebilmektedir. Özellikle, dinamik bellek yönetimi işlemlerinde performansı artırarak, uygulamanın genel performansına olumlu bir katkı sağlar.
G1 GC
Önleyici çöp toplama özelliği sayesinde kısa süreli yaşayan ama hafızayı dolduran objelerin uygulamayı uzun süre duraklatmaları engellenir. Bellek parçalanması azalır ve uygulamanın genel performansı artar.
Var olan MarkSweepDeadRatio parametresi kullanıldı. GC işlemi yapılırken bazı bölgelerdeki referanslar hala aktiftir, bunlardan dönecek hafıza da azdır ve yapılan işleme değmez. Varsayılan değeri 5’tir, yüzde ile ifade edilir. MarkSweepDeadRatio’nun 5 olması, sistemin her canlı nesne için 5 ölü nesnelik yer tutmayı amaçladığı anlamına gelir. Diğer bir ifadeyle, GC işleminden sonra yığının yaklaşık %16,7’si (6’da 1) canlı nesne ve %83,3’ü boş alan olmalıdır. Bu ayar, çok sayıda nesnenin hızla GC tarafından toplanmasını beklediğimiz ve GC’nin çalışma sıklığını en aza indirmek istediğimiz senaryolarda yararlı olabilir.
ZGC
Dinamik thread özelliği eklendi. Java 17 öncesinde CPU’nun %12,5’i ayrılıyordu, bazı testlerde bunun yetmediği görüldü. %25 gibi bir değere çıkarıldığında ise gecikme problemleriyle karşılaşıldı çünkü uygulamanın ihtiyacı olan CPU GC’ye ayrılıyordu. Yeni eklenen dinamik thread özelliği ile varsayılan olarak CPU’nun %25’i GC’ye ayrıldı ve çeşitli metrikler gözetilerek her GC döngüsünde %1-%25 arasında bir değer alması sağlandı.
İşaret yığını hafıza kullanımı azaltıldı. GC dinamik belleği parçalara bölerek yönetir. Her thread bir ya da daha fazla parçadan sorumludur. Bir thread başka bir thread’deki nesneye erişmek isterse bu nesne referansı ortak bir yığına yüklenir. Fakat N:1 ilişkili nesneler yüzünden bu ortak yığın gittikçe fazla yer kaplamaya başlar. Aynı zamanda Java thread’leri de nesneleri işaretlemeden ortak yığına atar. Java 17 ile ise GC threadlerinin nesne referansını işaretlemesi ve ortak yığına yüklemesi, Java threadlerinin ise işaretlenmemiş nesne referanslarını ortak yığına atması ile sorun çözüldü. Bu yaklaşım, GC thread’lerin birbirlerinin nesnesine erişimine yol açsa da dinamik bellek dostu yaklaşımını hissedilir ölçüde olumsuz etkilememiştir.
Aşağıda Java 9’dan başlayarak Java 17’ye kadar GC üzerinde yapılan optimizasyonlar ve yenilikler kronolojik olarak verilmektedir.
Java 9 (Eylül 2017)
- CMS (Concurrent Mark-Sweep) GC algoritmasının gelecekteki sürümlerde kaldırılacağı duyuruldu ve onun yerine G1 GC JVM varsayılanı GC olarak ayarlandı.
Java 10 (Mart 2018)
- G1 GC single-thread (tekli iş parçacığı) yapıdan multi-thread yapıya evrimleşerek paralel işlemeyi desteklemeye başladı. Böylece G1 bekleme (pause) zamanı düşürüldü.
Java 11 LTS (Eylül 2018)
- Ölçeklenebilir ve düşük gecikmeli özelliği ile ön plana çıkan ZGC adında yeni bir GC tanıtıldı.
Java 12 (Mart 2019)
- G1 GC algoritması, zorunlu ve isteğe bağlı olarak işlem yapacak alanlar eklenmesi ile güncellendi. Bu sayede G1 algoritması, zorunlu olarak işaretlediği çöpleri temizlerken isteğe bağlı olarak işaretlenenleri silmeme seçeneği sunar. Bu, doğru zamanda doğru temizlik işlemini yapmak için yapılan bir optimizasyonu temsil eder.
Java 13 (Eylül 2019)
- ZGC, kullanılmayan dinamik belleği serbest bırakarak ve uzun süre kullanılmayan dinamik bellek alanlarını işletim sistemine geri vererek Java uygulamalarının dinamik bellek yönetimini geliştirmek için optimize edildi. Ayrıca, minimum dinamik bellek boyutunu korumayı hedefler. Eğer maksimum dinamik bellek boyutu minimum dinamik bellek boyutuyla aynı ise, dinamik bellek işletim sistemine geri verilmez. Bu sayede ZGC, Java uygulamalarının dinamik bellek kullanımını optimize eder ve daha iyi performans sağlar.
Java 14 (Mart 2020)
- CMS GC algoritması kullanımdan kaldırıldı.
Java 15 (Eylül 2020)
- Java 11 ile beraber tanıtılan ZGC algoritması deneysel (experimental) statüsünden ürün (production) statüsüne çekildi.
Java 16 (Mart 2021)
- Daha önceki Java sürümlerinde, ZGC thread yığını işlemi güvenli noktada gerçekleştiriliyordu, bu da uygulamanın geçici olarak durdurulmasına yol açıyordu. Ancak bu optimizasyon ile bu işlem eş zamanlı bir aşamada gerçekleştirilir hale getirildi, böylece uygulama kesintiye uğramadan ZGC işlemlerini gerçekleştirme yeteneğine sahip oldu. Bu da ZGC’nin daha düşük gecikmeli çalışmasına katkı sağladı.
- ZGC ile ilgili 46 iyileştirme ve 25 bug fix yapıldı.
Java 17 LTS (Eylül 2021)
- Yeni dinamik thread oluşturma yeteneği sayesinde, GC’nin uygun bir şekilde kaynakları yönetmesi sağlandı ve bu da uygulamanın CPU sürelerini daha etkili bir şekilde kullanmasına olanak sağladı. Sonuç olarak, daha yüksek bir işlem verimliliği elde edildi ve bekleme süreleri önemli ölçüde azaldı.
Bu performans farklarını daha iyi anlamak için bir sonraki başlıkta yaptığımız demo uygulamasını inceleyelim.
6. Farklı GC Türlerinin Demo Uygulaması
Garbage Collector türlerini test etmek için aşağıdaki test kodu kullanılabilir. Bu kodu çalıştırırken Java komutuna hangi Garbage Collector ile çalışacağını parametre olarak verebiliriz. Bu parametreler için Garbage Collector Parametreleri bölümü incelenebilir.
Bu program, bir GC’nin nasıl çalıştığını ve nesnelerin dinamik bellekteki durumunu izleme yeteneğini simüle eder. Ancak, gerçek bir GC’nin karmaşıklığı ve detayları bu basit simülasyonu aşar. Bu kodun amacı, temel GC kavramlarını anlamak ve basit bir örnek üzerinden göstermektir.
Kodun Açıklaması:
- NodeObject sınıfı, özel bir nesne tipidir ve programda kullanılan nesneleri temsil eder. Bu nesneler, büyük bir dinamik bellek bloğu içerir ve bu blokların kullanımını ve temizlenmesini izler.
- ListNode sınıfı, bir bağlı liste (Linked List) yapısını temsil eder ve NodeObject nesnelerini ekleme ve kaldırma fonksiyonlarına sahiptir.
- Monitor sınıfı, bir thread olarak çalışır ve belirli aralıklarla sistem dinamik bellek kullanımını, GC tarafından temizlenen nesne sayısını ve diğer ilgili istatistikleri ekrana yazdırır.
- GCTest sınıfı, programın ana sınıfıdır. main fonksiyonunda bir ListNode ve bir Monitor nesnesi oluşturur. Ardından myTest fonksiyonu çağrılarak başlangıç adımları ve test adımları gerçekleştirilir.
- myTest fonksiyonu, önce başlangıç adımları için belirli bir süre bekler ve ardından belirli bir döngü içinde NodeObject nesnelerini bağlı listeye ekler ve kaldırır.
- Monitor thread'i, sürekli olarak sistem dinamik bellek kullanımı ve GC istatistiklerini ekrana yazdırır.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public class GCTest {
static ListNode objList = null;
static int wait = 1; // milliseconds
static int initSteps = 16;
static int testSteps = 1;
static int objSize = 2048; // 2 MB
public static void main(String[] arg) {
objList = new ListNode();
Monitor m = new Monitor();
m.setDaemon(true);
m.start();
myTest();
}
public static void myTest() {
for (int m = 0; m < initSteps; m++) {
sleep(wait);
objList.add(new NodeObject());
}
for (int n = 0; n < 10 * 8 * 8 / testSteps; n++) {
for (int m = 0; m < testSteps; m++) {
sleep(wait);
objList.add(new NodeObject());
}
for (int m = 0; m < testSteps; m++) {
sleep(wait);
objList.removeTail();
// objList.removeHead();
}
}
}
static void sleep(int t) {
try {
Thread.sleep(t);
} catch (InterruptedException e) {
System.out.println("Interreupted...");
}
}
static class NodeObject {
private static long count = 0;
static long cleaned = 0;
private long[] obj;
public NodeObject next = null;
public NodeObject prev = null;
public NodeObject() {
count++;
obj = new long[objSize * 128];
}
protected final void finalize() {
count--;
cleaned++;
}
static long getNodeCount() {
return count;
}
static long getCleanedNodeCount() {
return cleaned;
}
}
static class ListNode {
static long count = 0;
NodeObject head = null;
NodeObject tail = null;
static long getCount() {
return count;
}
void add(NodeObject o) {
// add the new object to the head;
if (head == null) {
head = o;
tail = o;
} else {
o.prev = head;
head.next = o;
head = o;
}
count++;
}
void removeTail() {
if (tail != null) {
if (tail.next == null) {
tail = null;
head = null;
} else {
tail = tail.next;
tail.prev = null;
}
count--;
}
}
}
static class Monitor extends Thread {
public void run() {
Runtime rt = Runtime.getRuntime();
System.out.println(
"Total\tFree\tFree\tTotal\tAct.\tClnd\tDead\tOver");
System.out.println(
"Mem.\tMem.\tPer.\t Obj.\tObj.\tObj.\tObj.\tHead");
while (true) {
long totalMemory = rt.totalMemory() / 1024;
long freeMemory = rt.freeMemory() / 1024;
long memoryUsageRatio = (100 * freeMemory) / totalMemory;
long totalMemoryOfActiveObjects = NodeObject.getNodeCount() * objSize;
long totalMemoryOfCleanedObjects = NodeObject.getCleanedNodeCount() * objSize;
long totalMemoryOfUsedObjects = ListNode.getCount() * objSize;
System.out.println(totalMemory // Total Memory
+ "\t" + freeMemory // Free Memory
+ "\t" + memoryUsageRatio + "%" // Free Memory Ratio
+ "\t" + totalMemoryOfActiveObjects // Total Object Memory
+ "\t" + totalMemoryOfUsedObjects // Active Objects Memory
+ "\t" + totalMemoryOfCleanedObjects // Cleaned Objects Memory
+ "\t" + (totalMemoryOfActiveObjects - totalMemoryOfUsedObjects) // GC'yi bekleyen memory
+ "\t" + (totalMemory - freeMemory - totalMemoryOfActiveObjects) // Sistemin Kullandığı Alan
);
GCTest.sleep(wait);
}
}
}
}
7. Demo Uygulama Çıktısı

Şekil 8. Toplam Hafıza Kullanımı

Şekil 9. Kullanılmayan Hafıza

Şekil 10. Kullanılmayan Hafıza (Yüzdelik)

Şekil 11. Silinmeyi Bekleyen Hafıza

Şekil 12. Temizlenen Alan
8. Sonuç ve Öneriler
Bu yazımızda çöp toplama özelliğini Java ile ele alarak detaylı bir şekilde inceledik. GC algoritmalarından G1 GC, Serial GC, Parallel GC ve ZGC algoritmalarınının ayrıntılarını, kullanım senaryolarını ve çalışma yöntemlerini ele aldık. GC algoritmaları arasında net olarak bir algoritma diğerinden daha iyi veya kötü gibi bir çıkarımda bulunamayız. Bunun yerine performans karşılaştırması için verimlilik, gecikme ve kaynak kullanımı olmak üzere 3 temel parametreyi ele aldık. Bu parametreler doğrultusunda Java 8, Java 11 ve Java 17 versiyonlarında GC algoritmalarının performans kıyaslamasını göreceli olarak değerlendirdik. Ayrıca Java 9’dan başlayarak Java 17’ye kadar GC adına yapılan iyileştirmelerin ve yeniliklerin tarihçesinden bahsettik. Son olarak demo bir kod ile local’de simüle edebileceğiniz bir test kodu sunduk.
Bu yazımızdan çıkaracağımız sonucu GC bakış açısından tek bir cümleyle şöyle özetleyebiliriz. Bir programcı Garbage Collector’a sormuş: “Sonuç olarak ne önerirsin?” Garbage Collector cevaplamış: “Null’ların peşinden git, gerisi kolay!”
9. Kaynakça
- IBM https://www.ibm.com/topics/garbage-collection-java
- Oracle Blogs https://blogs.oracle.com/javamagazine/post/understanding-garbage-collectors
- New Relic https://newrelic.com/blog/best-practices/java-garbage-collection#:~:text=Tenured%3A%20The%20tenured%20space%20is,collector%20checks%20it%20less%20often
- DZone https://dzone.com/articles/understanding-the-java-memory-model-and-the-garbag
- Oracle Blogs https://blogs.oracle.com/javamagazine/post/java-garbage-collectors-evolution
- Oracle https://www.oracle.com/java/technologies/javase/8u-relnotes.html
- Open JDK https://cr.openjdk.org/~mchung/jdk9/unified-api/overview-summary.html
- Oracle https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
- Oracle Docs https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html#GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573
- RedHat https://developers.redhat.com/articles/2021/11/02/how-choose-best-java-garbage-collector#parallel_collector
- Bell https://bell-sw.com/announcements/2022/09/07/garbage-collection-in-java/
- Medium hasithalgamge https://medium.com/@hasithalgamge/seven-types-of-java-garbage-collectors-6297a1418e82
- Bell https://bell-sw.com/announcements/2022/09/07/garbage-collection-in-java/#garbage-collection-best-practices
- RedHat https://developers.redhat.com/articles/2021/11/02/how-choose-best-java-garbage-collector#garbage_first__g1__collector
- Oracle Blogs https://blogs.oracle.com/javamagazine/post/java-garbage-collectors-evolution
- OpenSource https://opensource.com/article/22/7/garbage-collection-java
- Baeldung https://www.baeldung.com/jvm-garbage-collectors
- Github https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html
- Optaplanner https://www.optaplanner.org/blog/2021/09/15/HowMuchFasterIsJava17.html
- Github https://tschatzl.github.io/2021/09/16/jdk17-g1-parallel-gc-changes.html
- OpenJDK https://bugs.openjdk.org/browse/JDK-8260267
- OpenJDK https://bugs.openjdk.org/browse/JDK-8268372
- OpenJDK https://bugs.openjdk.org/browse/JDK-8262068
- Malloc Blog https://malloc.se/blog/zgc-jdk17
- Herongyang http://herongyang.com/JVM/GC-Garbage-Collection-Test-Program.html