Deniz Arıkan
Akbank Teknoloji
Published in
15 min readNov 2, 2023

--

Java’da bellek yönetimi nasıl yapılır? Garbage collector nasıl çalışır? Memory leak nedir?

Modern programlama dillerinde bellek yönetimi, yani bir başka deyişle kullanılmayan nesnelerin silinerek yeni nesnelere yer açılması işlemi, için farklı metotlar kullanılmaktadır. Bazı dillerde bu işlem tamamen geliştiricinin yönetimine bırakılırken bazılarında ise sistem arka planda bu işi otomatik olarak yapar.

Örneğin aşağıdaki kod parçası C dilinde 15 adet int’i içerebilen bir bellek bloğunu rezerve etmek için kullanılır

int *ptr;
ptr = malloc(15 * sizeof(*ptr));

Bu satırlarda kullanılan ve malloc ile bellek ayrılan liste ile yapılan iş tamamlandıktan sonra ilgili nesnenin silinmesi için geliştiricinin bu işlemi yapan bir komutu (free, realloc vb.) programına eklemesi gereklidir.

Bu durum geliştiricinin eline çok kuvvetli bir araç verir ancak aynı zamanda yazılımda hata çıkma ihtimalini arttırır ve geliştirme sürecini uzatabilir. Yeni nesil programlama dillerinin çoğunda ise programcının yazılacak koda odaklanması ve geliştirme sürecinin hızlı olması için bellek yönetimi sistemin bir parçası haline getirilmiştir.

Java programlama dilinde memory yönetimi JVM(Java virtual machine) in bir parçası olan Garbage collector (GC) tarafından otomatik olarak yapılır. Bu yapı arka planda gereken zamanlarda çalışarak kullanılmayan (referansı olmayan) nesneleri siler, bazı durumlarda kullanılan blokları taşır ve yeni nesneleri oluşturulması için belleği temiz tutar. Bu yapı Java dilinin en önemli özelliklerden biridir.

Kullanılan nesneler ve kullanılmayan nesneler

Garbage Collection başlangıç noktalarından referansı olup erişilebilen nesneler kullanılan (canlı — live) nesneler olarak, referansı olmayanlar ise kullanılmayan (ölü — dead) nesneler olarak değerlendirilir. Örneğin aşağıdaki resimde mavi olan nesneler thread’e ait kök seviyeden referans’a sahip oldukları için “canlı”, gri olanlar ise referansları olmadığı için “ölü” olarak değerlendirilirler.

Yukarıdaki grafik tabi ki memory durumunun çok basitleştirilmiş bir halidir. Yoğun yük alan bir uygulama çok kısa bir süre içinde bile aşağıdakinden daha karmaşık bir obje referans durumuna sahip olur.

Garbage Collection (Çöp toplama)

Garbage Collection, otomatik bellek yönetimi mekanizmasıdır ve temel amacı kullanılan objelerin tespit edilmesi ve referans edilmeyenlerin silinmesidir. Kullanılmayan/referans edilmeyen nesneler silinerek kapladıkları alan bellekte boşa çıkarılır, gereken durumlarda kullanılan kısımlar taşınarak birleştirilir ve bellekte yeni değişkenler vb. için kullanıma açık boş yer oluşturulur.

Garbage Collection, işlemi en basit haliyle mark (işaretle), sweep (süpür-sil), compact (birleştir) işlemi yapar. Örneğin aşağıda mavi kısımlar kullanılan, gri kısımlar ise artık kullanılmayan nesneler olsun;

  • Mark adımında kullanılmayan nesneler işaretlenir
  • Sweep adımında işaretlenen nesneler silinir
  • Compact adımında ise bellekte kalan bu parçalar bir araya getirilir.

Java Garbage Collector ile ilgili çok sayıda teknik terim bulunmaktadır ve Java GC işleminin çalışma mantığının net olarak anlaşılmasının yolu bu teknik terimlerin bilinmesinden geçer.

Java Garbage Collector’u bilmek ne işe yarar?

Yazının başında değinildiği gibi Java GC arka planda otomatik olarak çalışır ve programcının GC ile ilgili herhangi bir kodu çalıştırmasına gerek yoktur. Ancak, Garbage collector’un nasıl çalıştığını bilmek daha etkin kodlama yapılmasına ve hataların giderilmesine yardımcı olur.

  • GC Tuning
  • Memory leak durumları
  • Daha efektif kodlama
  • Sistem performansına etkisi

Stack ve Heap

Stack ve Heap bellekte bulunan mantıksal yapılardır ve boyutları Java çalıştırılırken parametreler ile belirlenebilir. Stack’de bir Java programı çalışırken kullanılan metod’lar, bu metodlar içinde kullanılan Java nesnelerine referanslar ve primitive veri tipleri bulunur. Heap’de ise Java metotlarında kullanılan nesnelerin kendileri bulunur. Stack’deki referanslar heap memory’deki gerçek nesneleri gösterir. Java’da Stack ve Heap yapılarını aşağıdaki örnek program ile inceleyelim;

public class PersonPrinter {
public static void main(String[] args) {
int i=1;
Person p = new Person();
Car c = new Car();
new PersonPrinter().printPerson(p);
}
private void printPerson(Person p) {
p.setAge(35);
String str = p.toString();
System.out.println(str);
}
}

Bu programda basitçe bir Person ve bir Car nesneleri oluşturulur, sonrasında oluşturulan person nesnesi bir metot’a parametre olarak geçirilir.

1. İlk aşamada Person ve Car nesnelerinin referansları Stack memory’de, nesnelerin kendileri ise Heap memory’de oluşturulur. Primitive int ise Stack memory’dedir.

2. printPerson metot’u çalıştırıldığında Stack memory’ye eklenir, buraya Person nesnesinin referansı “p” değer olarak (pass by value) geçirilir. “p” referansı Heap memory’de bir önceki adımda oluşturulan Person nesnesini gösterir.

  • Stack memory, heap memory ile kıyaslandığında çok daha küçüktür.
  • Heap memory’ye bütün uygulama erişirken stack memory o anda çalışan thread tarafından kullanılır.
  • Stack memory LIFO (last in first out) yapısındadır ve basittir, heap memory ise Java nesnelerini ve bu nesneler arası referansları tuttuğu için daha karmaşıktır.
  • Daha basit yapısı nedeniyle stack memory çok daha hızlıdır.
  • Stack memory dolduğunda java.lang.StackOverFlowError hatası alınır, heap memory dolduğunda ise java.lang.OutOfMemoryError: Java Heap Space hatası alınır.
  • Çalışan metod bittiğinde stack memory’deki ilgili kısım otomatik olarak temizlenir, heap memory’nin yapısı daha karmaşık olduğu için bu işi Garbage collector yapar.
  • Heap memory’nin Young-Generation, Old-Generation vb. bölümleri vardır.
  • Heap memory boyutunu belirlemek için uygulama açılırken -Xms and -Xmx JVM parametreleri kullanılabilir. Xms açılışta istenen heap boyutunu, Xmx de maksimum heap boyutunu belirler. Stack için ise –Xss kullanılır. Burada <heap boyutu> kısmına istenilen boyut yazılırken [birim] kısmına da gb için g, MB için m, KB için k yazılabilir.
-Xms<heap boyutu>[birim]
-Xmx<heap boyutu>[birim]
-Xss<stack boyutu>[birim]

Örneğin aşağıdaki parametreler ile Stack ve Heap memory boyutları ayarlanabilir.

java -Xss1m
java -Xss1024k
java -Xmx4G –Xms1G
java -Xmx128m -Xms64m
java –Xmx4096k –Xms2048k

Memory leak

Memory leak, bir Java uygulamasının işi bittikten sonra ayırdığı bellek kısımlarını serbest bırakmaması durumudur. Bu kısımlar süre içinde birikerek OOM (OutOfMemory) durumuna neden olabilirler. Class seviyesinde tutulan bir liste ya da map’e sürekli yeni nesne eklenmesi ve temizlenmemesi, kullanılan bir cache’leme mekanizmasında yapılan bir hata nedeniyle sürekli yeni ekleme yapılması ve eskilerinin silinmemesi vb. durumlar memory leak’e neden olabilirler.

Eğer bir Java uygulamasında yukarıda bahsedilen sebeplerden dolayı bir memory leak var ise bu durumun etkilerinin ortaya çıkma zamanı uygulamanın çalışma yoğunluğuna göre değişebilir. Çok istek alan uygulamalarda performansın etkilenmesi dakikalar içinde olabilirken memory leak yapan kodun daha az istek aldığı durumlarda exception’ların oluşması günler sürebilir.

Memory leak belirtileri :

  • Uygulama performansında kötüleşme : Uygulama sürekli Garbage Collection (GC) yaptığı için performansın etkilenmesi
  • OutOfMemory hataları : Belleğin tükenmesi sonucu oluşan exception’lar
  • Rastlantısal uygulama hataları : OutOfMemory hataları dışında belleğin azalması nedeniyle uygulamanın farklı yerlerinde birbirinden bağımsız hatalar oluşması

Memory leak içeren bir Java uygulamasına ait bellek grafiği incelendiğinde yukarı doğru çıkan bir testere deseni görülür. Aşağıdaki grafik memory leak durumunda oluşan bellek kullanımının tipik bir örneğidir.

class seviyesinde static listeler

Örneğin aşağıdaki Java programında class seviyesinde kullanılan bir listeye sürekli yeni Double nesneleri eklenir ve sonunda (döngü çok uzarsa veya aynı metot çok sayıda çağırılırsa) bu listenin aşırı büyümesi OutOfMemory hatasına neden olunur.

public class StaticTest {
public static List<Double> list = new ArrayList<>();
public void populateList() {
for (int i = 0; i < 1000000; i++) {
list.add(Math.random());
}
}
public static void main(String[] args) {
new StaticTest().populateList();
Log.info("Debug Log");
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.test.StaticTest.populateList(StaticTest.java:11)
at com.test.StaticTest.main(StaticTest.java:22)

Bu örnekte class seviyesinde static List kullanılması buraya olan referansın hiçbir zaman kaybolmamasına neden olur, bu sebeple de Java Garbage Collector ilgili List nesnesini hiçbir zaman toplayamaz ve bellek kullanımı artar. Aşağıdaki grafikte mavi kısımda “Used heap”in devam ettiği görülüyor.

Aynı örnekte List değişkeninin önündeki static kaldırılırsa bu değişken ilgili nesneye bağlanır ve bu nesnenin kullanımı bitince de toplanmaya aday hale gelir. Bu kullanımda ilk örnektekinden çok farklı bir bellek kullanım grafiği ortaya çıkar. List nesnesi kullanıldıktan sonra Garbage Collector tarafından toplanıldığı görülür.

equals() ve hashCode()

Class seviyesinde liste veya map kullanımına benzer şekilde equals() ve hashCode() metotlarının yanlış kodlanması veya kodlanmaması da kullanılan belleğin artmasına neden olabilir. Java’da projelerde sıklıkla kullanılan HashSet ve HashMap sınıfları bu 2 metot’u yoğun olarak kullanır. HashSet ve HashMap sınıflarına yapılan yeni eklemelerde equals() ve hashCode() metotlarının düzgün sonuç dönmemesi eklenen kaydın sürekli yeni bir nesne olarak eklenmesine sebep olur.

Örneğin aşağıdaki basit kodda Car nesnesine ait equals() ve hashCode() metotları olmadığı için bütün nesneler map e eklenir. Döngü bittiğinde map’in boyutunun 100 olduğu görülür.

Map<Car, Integer> map = new HashMap<>();
for(int i=0; i<100; i++) {
map.put(new Car("My car"), 1);
}

Eğer Car nesnesine constructor’da verilen String’in kontrol edildiği bir hashCode() metotu (return name.hashCode() gibi) yazılmış olsaydı bütün Car nesneleri aynı yere eklenecek ve map’in boyutunun 1 olduğu görülecekti.

finalize() metotu

finalize() metotlarının kullanımı, potansiyel bellek sızıntısı sorunlarının bir başka kaynağı olabilir. Bir sınıfın finalize() yöntemi kodlandığında, o sınıfın nesneleri anında GC’ye girmez. Bunun yerine GC, daha sonraki bir zamanda gerçekleşecek olan finalize işlemi için bunları sıraya koyar. Ek olarak eğer finalize metotu iyi yazılmadıysa buradaki sırada bekleyen nesneler hiçbir zaman Garbage collector’a yetişemezler ve nihayetinde OutOfMemory (OOM) hatası oluşur. finalize() metotu kodlanmayarak bu durum engellenebilir.

Heap dump

Yoğun performans ile çalışan ortamlarda Java uygulaması OOM (OutOfMemory) hatası aldıktan sonra oluşan heapdump dosyaları incelenerek bellekte biriken nesneler bulunabilir. Örneğin aşağıdaki durumda bellekte HashMap Node’ları 8GB’a yakın yer işgal etmektedir. Sorunun kök seviyesinde ise 108 adet LinkedBlockingQueue bulunmaktadır.

Bu analizler için Eclipse Memory Analyzer (MAT), JProfiler, IBM HeapAnalyzer vb. araçlar kullanılabilir.

Heap memory bölümleri

Heap memory Java nesnelerinin tutulduğu alandır. Aşağıdaki bölümlerden oluşur.

· Young generation (Nursery)
o Eden
o Survivor space
— From
— To

· Old generation (Tenured adı verilen tek bir bölümden oluşur)
o Permgen/Metaspace

Young generation

Heap memory’e ait bu bölüm Java uygulamasında tüm yeni nesnelerin oluşturulduğu ve yaşlanmaya başladığı kısımdır. Yeni oluşan Java objeleri bu bölümün Eden kısmına eklenirler. Uygulama çalışmaya başladıktan bir süre sonra kullanılmayan nesneler burayı doldurur ve toplanmayı bekler. Young Generation dolduğunda ise ‘Minor Garbage Collection’ işlemi çalıştırılarak bu kısım temizlenmeye çalışılır. Belli bir süre sonra ‘surviving’ hayatta kalan nesneler ‘Old Generation’ alanına taşınır.

Young generation içinde aşağıdaki aşağıdaki bölümler bulunur.

· Eden
· Survivor space
o From
o To

Nesneler ilk oluşturulduklarında Eden kısmında bulunurlar. Eden alanı dolduğunda ‘minor garbage collection’ işlemi tetiklenir. Bu işlem sonrasında kalanlar ‘Survivor’ alanına taşınırlar.

Old generation

Heap memory’e ait bu bölümde belirli bir sayıdak GC sonrası kullanımı devam eden nesneler bulunur. Old generation dolduğunda ise ‘Major Garbage Collection’ işlemi çalıştırılır.

Garbage collection işlemi

Uygulama başladıktan sonra yeni nesnelerin oluşturulması, kullanılmayan nesnelerin temizlenmesi, nesnelerin survivor space ve old generation’a kopyalanmaları en basit haliyle aşağıdaki şekilde gerçekleşir. (Shenandoah, ZGC gibi algoritmalar mevcut thread’ler ile birlikte çalıştığı ve belleği çok daha fazla sayıda küçük parçaya böldükleri için bellek yönetiminde farklılaşabilirler)

1. Java uygulaması çalışır ve ihtiyacı olan nesneleri ilk olarak Eden alanında oluşturur. Aşağıda yeşil olarak gösterilen nesneler kullanılan, referansı olan “canlı” nesnelerdir.

2. Bir süre geçtikten ve yeni nesneler oluşturulduktan sonra “Eden” alanı dolar. Bu süre içinde bazı nesnelerin kullanımı devam ederken (yeşil ile gösterilen canlı nesneler) bazılarına ihtiyaç kalmaz (gri ile gösterilen çöp nesneler) ve bu nesneler toplanmaya uygun hale gelirler. Bu durumda bir minör garbage collection çalıştırılır.

3. Minör GC işleminden sonra referansı olmayan çöp nesneler toplanır ve kullanılan canlı nesneler de “Eden” alanından Survivor space altındaki “From” kısmına kopyalanır. Bu işlem sırasında Java canlı nesnelerin kaç GC sonrası “hayatta kaldıklarını” tutar. Örneğin aşağıdaki nesneler 1 kere GC’den sağ çıkmışlardır.

4. Java uygulaması çalışmaya devam eder ve bu sırada yeni objeler Eden alanına eklenir. Hem Eden alanında hem de Survivor alanında bazı nesnelerin kullanımı devam ederken bazıları da kullanılmayan nesne durumuna düşerler. Eden dolduğunda yine bir minör GC işlemi tetiklenir.

5. Bu GC işlemi ile birlikte hem Eden alanı temizlenir hem de bir önceki GC işleminde Survivor space altındaki “From” alanına kopyalananlar kontrol edilerek kullanımı devam edenler “To” alanında taşınırlar, kullanılmayanlar da temizlenir. Bir önceki adımda olduğu gibi Java nesnelerin kaç kere GC sonrası hayatta kaldıklarını tutmaya devam eder.

6. Bu döngü Java uygulamasının yaşamı boyunca devam eder, Eden alanı kontrol edilir, Survivor space’deki nesneler From’da ise To’ya, To’da iseler From’a kontrol edilerek taşınırlar. Nesnelerin belirli bir kere GC sonrası kullanımlarının devam ettiği görülürse bu nesneler Old generation’a taşınırlar. Bu işleme “Promotion” denir.

7. Örneğin aşağıdaki nesneler 8 kere yapılan GC işlemi sonrası hala kullanılır durumda oldukları için “Young generation”dan “Old generation”a taşınmışlardır. Bu sayıya Java bellek durumuna bakarak kendisi karar verir.

Java uygulamalarında yapılan araştırmalarda oluşturulan nesnelerin %90’ının çok kısa süre kullanıldığı, sonrasında da ilk birkaç GC ile bu nesnelerin silindiği görülmüştür. Ayrıca belirli bir sayıda GC ile toplanmayan Java nesnelerinin ise çoğu zaman uygulama çalıştığı sürece kullanılmaya devam ettiği fark edilmiştir. Java runtime, heap’i Young ve Old generation olarak bölerek ve bu şekilde yöneterek Garbage collection işlemini olabildiğince kısa tutmayı amaçlar. Bu sayede, kısa süre kullanılarak referansı kalmayan nesneler Young generation’da kalır/temizlenir, uzun süre kullanılan nesneler de Old generation’a taşınır ve GC işleminin bütün heap’de değil de çoğunlukla belleğin bir kısmında çalışması sağlanmış olur.

Stop-the-world

Java Garbage collector çalışırken mevcut nesne bağlantılarının değişmemesi için ‘Stop-the-World’ durumu oluşturulur, yani garbage collector işini yaparken uygulamadaki tüm diğer threadler durur. Hem minör GC hem de majör GC işlemi ‘Stop-the-World’ durumu oluşturur ancak minör GC sadece Eden ve From/To’da çalıştığı için çok çok hızlı gerçekleşir ve uygulama üzerinde pratikte bir etkisi görülmez. Majör GC ise ‘Old generation’ alanında da çalışır ve buradaki nesneleri temizler. Bu nedenle minör GC ye göre çok daha yavaştır ve pratikte suspension olarak hissedilir. Bu süre boyunca Java uygulaması dışardan gelen isteklere cevap veremez, herhangi bir işlem yapamaz. Bu durum nedeniyle Java’ya G1GC gibi daha kısa süreli suspension yapan (low-latency) garbage collection algoritmaları eklenmiştir.

Örneğin aşağıda Dynatrace aracından alınan ekran görüntüsünde 8 GB kullanılan bellek üzerinde gerçekleştirilen bir majör GC olayı 5.48 sn. sürmüştür.

PermGen

Permanent Generation (Permgen) Heap içinde Java tarafından yönetilen bir bellek alanıdır. Bu özel bellek alanında Java sınıfları içindeki tüm static tanımlamalar ve Java’nın sınıflara ait tuttuğu tanımlayıcı bilgiler bulunur. Permanent Generation bellek bölümü ihtiyaç duyulan verilere hızlı erişim için bir indeks gibi kullanılır. Tıpkı uygulamanın çalıştığı heap bellek alanı gibi, sınırlandırılmış bu alanı temizleyebilmek için de garbage collection çalışması gerekir. Garbage collection’ın çalışması ve bu alanı temizlemeye yetmemesi durumunda ise normal heap alanlarında olduğu gibi “OutOfMemoryError” hataları oluşur.

PermSize ve MaxPermSize JVM parametreleri ile Permgen alanı büyüklüğü ayarlanabilir fakat bu alanın büyüklüğü dinamik olarak çalışmamaktadır bu yüzden sürekli olarak uygulamanın takip edilmesi gerekir. Permgen’deki bu sabit yapı nedeniyle Java 1.8’den itibaren PermGen alanı daha dinamik olan Metaspace ile değiştirilmiştir.

Metaspace

Java 1.8 ile birlikte gelen ve Permgen’in yerini alan yapıdır. Permgen de var olan bellek alanı yönetimi geliştirilerek, belleğe yüklenen static veriler veya metadata’lara otomatik olarak büyüyebilen bir yapı haline getirilmiştir. Metaspace bellek alanı belirlenen sınıra ulaştığında JVM otomatik olarak bu kısımda memory arttırır. Metaspace bellek bölümü ile Java’nın PermGen’in yapısı nedeniyle oluşan kararsız yapısından kurtulması hedeflenmiştir. Yukarıda belirtildiği gibi PermGen/Metaspace alanları dolduğunda da GC işlemi çalıştırılır, MetaSpace yapısı ile burada çalışan Garbage collection da daha verimli hale getirilmiştir.

-XX: MetaspaceSize

Meta veriler için ayrılan başlangıç miktarıdır.

-XX: MaxMetaspaceSize

Meta veriler için ayrılan maksimum miktardır.

-XX: MinMetaspaceFreeRatio

Meta veriler minimum için boş bırakılacak orandır. Örneğin; -XX:MinMetaspaceFreeRatio=20 dendiğinde Metaspace üzerinde GC işleminden sonra en az %20 boş alan bırakılır.

-XX: MaxMetaspaceFreeRatio

Meta veriler maksimum için boş bırakılacak orandır. Yukarıdakine benzer şekilde kullanılır ve en fazla boş bırakılacak alanı belirler.

Java Garbage Collector türleri

Farklı Java sürümleri ile birlikte JVM’e yeni GC türleri eklenmekte ve sürüme ait varsayılan GC algoritması değişmektedir.

Serial Garbage Collector

Parallel Garbage Collector

CMS (Concurrent Mark Sweep) Garbage Collector

G1 Garbage Collector (G1GC)

Z Garbage Collector

Serial Garbage Collector

Çöp toplama işlemini tek bir thread ile yapan en basit GC algoritmasıdır. Çalıştığında “stop-the-world” ile bütün thread’leri durdurur. Kurumsal sunucular gibi multi-threaded ortamlar için uygun değildir, daha çok single-thread çalışan ve basit yapısı olan Java uygulamalarında kullanılabilir.

Uygulamada Serial Garbage Collector’ın kullanılması isteniyorsa aşağıdaki parametre ile aktive edilebilir.

java -XX:+UseSerialGC

Parallel Garbage Collector

Serial Garbage Collector’dan farklı olarak çok sayıda thread ile çöp toplama işlemini yapar ve heap alanını yönetir. Benzer şekilde, çalıştığında bütün thread’leri durdurur. Bu algoritmada “stop-the-world” durumu süresince çok sayıda GC thread’i referansı olmayan nesneleri birlikte topladığı için seri çöp toplamaya göre daha hızlı çalışır ve suspension’ı azaltır.

Uygulamada Parallel Garbage Collector’ın kullanılması isteniyorsa aşağıdaki parametre ile aktive edilebilir.

java -XX:+ UseParallelGC

Paralel Garbage Collector kullanıldığında aşağıdaki JVM parametreleri ile GC davranışı değiştirilebilir

-XX:ParallelGCThreads=<n>

Garbage collector thread sayısını belirler

-XX:MaxGCPauseMillis=<t>

Garbage collection işleminin maksimum durma süresini belirler.
Örneğin; -XX:MaxGCPauseMillis=100 dendiğinde GC işlemlerinin mümkün olan durumlarda en fazla 100 ms durması istenir.

-XX:GCTimeRatio=<n>

GC zamanı ile GC dışında geçen zamanın oranını belirler. Bu parametredeki n sayısı 1/(1+n) formülü ile çalışır.
Örneğin; -XX:GCTimeRatio=19 dendiğinde toplam sürenin %5’i GC için %95’i de işlem için ayırılır. İşlem süresi GC süresinin 19 katı olur.

CMS Garbage Collector

Concurrent Mark Sweep (CMS) yapısı Garbage collection için çok sayıda thread’i mevcut uygulama thread’leri ile birlikte kullanır. Kısa GC sürelerini tercih eden ve CPU kaynağının belirgin bir kısmını Garbage collector ile paylaşmayı kabul edebilen uygulamalar için tasarlanmıştır. CMS GC Java 9 ile birlikte Deprecated duruma getirilmiş, Java 14’ten itibaren de tamamen kaldırılmıştır.

Garbage First Garbage Collector (G1GC)

G1 (Garbage First) Garbage Collector memory alanı geniş ve çok sayıda CPU içeren sistemler için geliştirilmiştir ve JDK7 Update 4 ile birlikte CMS collector’un yerine eklenmiştir. Kaynakları CMS collector’dan daha verimli kullanır. Java 9 ile birlikte varsayılan Garbage collector G1GC haline gelmiştir.

Diğer collector algoritmalarından farklı olarak G1GC heap alanını eşit boyutlu daha küçük parçalara bölerek yönetir. Bu alanlar Eden space, Survivor space veya Old generation olarak kullanılabilir. Bu sayede sadece GC çalıştırılan bellek bölümünü kullanan thread’ler durur ve suspension süreleri kısalır. Çok daha fazla alanı sürekli yönettiği için G1GC’nin CPU maliyeti diğer algoritmalara göre biraz daha fazla olabilir.

Garbage collection işlemi başladığında bu eşit boyutlu küçük memory alanlarında kullanılmayan nesneler işaretlenir ve öncelikle daha az canlı nesnesi (daha çok çöpü) olanlar toplanır. Garbage-First adı bu yapı nedeniyle verilmiştir. G1GC bu parçalı temizleme yapısı sayesinde çok nadiren “stop-the-world” işlemi yapar.

Uygulamada Garbage First Garbage Collector’ın kullanılması isteniyorsa aşağıdaki parametre ile aktive edilebilir.

java -XX:+ UseG1GC

Java 8 ile JVM parametrelerine aynı String’leri tek bir nesne olarak tutması için UseStringDeduplication parametresi eklenmiştir. Bu sayede heap alanı tekrar eden String’lerden temizlenerek daha verimli olarak kullanılır. Bu parametrenin kullanılması GC işleminin performansını olumlu etkileyecektir.

-XX:+UseStringDeduplication

G1GC algoritması GC işleminden etkilenen thread sayısını olabildiğince düşük tutarak “stop-the-world” durumlarını minimuma indirir, dolayısıyla da Java uygulamasının suspension’ları (cevap verememe) azalır. Bu durum CPU maliyetini bir miktar artırabilir.

Aşağıdaki grafiklerde G1GC ile Paralel GC algoritmaları karşılaştırılmıştır. G1GC algoritması Paralel GC’ye göre bir miktar daha fazla CPU kullanmakta ancak suspension süreleri en fazla 220 ms civarında olmaktadır. Paralel GC’de ise bu süre 1.5 saniyenin üzerine çıkabilmektedir.

Z Garbage Collector

İlk olarak Java 11 ile gelen, sonrasında Java 15 ile “üretim ortamına hazır” statüsüne kavuşan Z Garbage Collector (ZGC) ise G1GC gibi heap’i bölümlere ayırarak yönetir. Amacı suspension sürelerinin heap boyutu ile orantılı olarak artmasını engellemektir. Birkaç yüz MB’tan 16TB’a kadar olan heap boyutlarında performanslı bir şekilde çalışmaktadır. ZGC, G1GC’ye benzer mantıkta çalışır ancak heap’i böldüğü parçaların boyutları eşit olmayabilir.

ZGC özünde mevcut Java thread’leri beraber çalışan bir çöp toplama algoritmasıdır, yani çoğu zaman iş yapan Java thread’lerinin GC işlemi sırasında suspension’a uğramasına gerek kalmaz, gerek olan durumlarda da bu süre çok kısa olur.

Uygulamada Z Garbage Collector’ın kullanılması isteniyorsa aşağıdaki parametre ile aktive edilebilir.

java -XX:+ UseZGC

ZGC’nin mevcutta 2 farklı sürümü bulunmaktadır. Bellek yönetiminde Generation kullanmayan ilk sürüm ve Generation kullanan yeni sürüm. Generation yapısı yukarıda da anlatıldığı gibi belleği nesnelerin hayatta kalma sürelerine göre böler ve GC işleminin daha verimli çalışmasını sağlar.

ZGC algoritmasının Generation yapısını içeren yeni sürümünün kullanılması tavsiye edilmektedir. Bu sürüm aşağıdaki parametreler ile aktive edilebilir.

-XX:+UseZGC -XX:+ZGenerational

Shenandoah GC

RedHat tarafından geliştirilmiştir ve Open JDK 11’e dahil edilmiştir. Shenandoah GC standart Oracle Java kütüphanelerinde bulunmamaktadır, bu nedenle Oracle Java kullanılan durumlarda alternatifi olan ZGC tercih edilebilir.

Shenandoah GC, G1GC üzerine yapılan iyileştirmeler ile oluşturulmuştur. Garbage collection için kullanılan mekanizmalar bellek üzerinde G1GC’ye oranla biraz daha büyüktür ama bu GC türünde bekleme süreleri daha da kısaltılmıştır.

ZGC gibi mümkün olduğunda mevcut Java thread’leri ile beraber çalışarak suspension sürelerini minimumda tutar. GC süresi üzerinde çalışılan heap boyutundan bağımsızdır, yani 2 GB’lık heap’te de 200 GB’lık heap’te de aynı performansı gösterir.

Open JDK kullanan bir Java uygulamasında Shenandoah Garbage Collector’ın kullanılması isteniyorsa aşağıdaki parametre ile aktive edilebilir.

java -XX:+ UseShenandoahGC

Referanslar

https://blogs.oracle.com/javamagazine/post/java-garbage-collectors-evolution

https://tugrulbayrak.medium.com/jvm-garbage-collector-nedir-96e76b6f6239

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gc-ergonomics.html

https://www.geeksforgeeks.org/types-of-jvm-garbage-collectors-in-java-with-implementation-details/

https://www.baeldung.com/jvm-garbage-collectors

https://www.baeldung.com/java-memory-leaks

--

--