JVM Nasıl Çalışır Yazı Serisi – Çöplerin Efendisi

Java programcısının çok sadık bir hizmetçisi var. Her türlü çöplüğü, pisliği arkasından devamlı toplar, hiç sesini çıkarmaz. Çöplerin efendisidir, ama bir o kadar da mütevazidir. Kimseye belli etmeden işini görür. Bu yüzden birçok Java programcısı onun farkında bile değildir. Ama o işini yapmasa Java programcısının hali çok vahim olur, C/C++ ile kod yazan meslektaşlarından bir farkı kalmaz, bilgisayarın hafızası denilen kara delikte kaybolur gider, yazdığı programlar devamlı sallanır.

Garbage Collector’dan bahsediyorum. Çoğu Java programcısının varlığından bile haberdar olmadığı, bazılarının çekindiği, bazılarının ne yaptığını tam olarak anlamdığı, bazılarının ise yaptığı işe hayran olup, ince ince hayranlık duyduğu Garbage Collector… Java’yı Java yapan özelliklerinden bir tanesi bir Garbage Collector’e sahip olmasıdır. Şimdi yakından tanıyalım bu kahramanı.

Java dilinde herhangi bir sınıftan bir nesne oluşturmak için new operatörü, Class.forName().newInstance(), clone() veya readObject() metodu kullanılır. Örneğin new operatörü sınıfın konstrüktörünü kullanarak JVM bünyesinde yeni bir nesne oluşturur. Nesne inşa edildikten sonra Java Heap içinde konuşlandırılır. Nesnelerin hepsi kullanıldıkları sürece heap icinde mutlu, mesut hayatlarını sürdürürler. Ama her canlı gibi onlar da birgün ölürler. Öldüklerinde onları toplayıp, ebediyete intikal ettiren Garbage Collector’dür.

İngilizce’de garbage çöp anlamına gelmektedir. Garbage Collector JVM bünyesinde hem yeni nesnelerin doğmasına yardımcı olan, hem de ölen nesneleri ortadan kaldıran modüldür. new operatörü ile doğan yeni nesneler için Garbage Collector hafıza alanını temin eder. Ölen ve çöp olarak isimlendirilen nesnelerin işgal ettikleri hafıza alanını Garbage Collector boşaltır ve yeni nesnelere tayin eder. Garbage Collector kısaca Java’da otomatik hafıza yönetiminden sorumludur. Onsuz Java’nın C/C++ dilinden bir farkı kalmazdı.

Garbage Collector’ün nasıl çalıştığını görebilmek için aşağıda yer alan videoyu hazırladım. Collector’ü kızdırmak için bolca çöp (garbage) üreten minik bir Java programını şu şekilde oluşturmak yeterli:

package com.kurumsaljava.jvm.garbage;

import java.util.ArrayList;
import java.util.List;

public class Heap {


	public static void main(String[] args) throws Exception {
		List<String> temp = new ArrayList<String>();

		Thread.sleep(10000);
		while (true) {

			temp.add("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
			//System.out.println(temp.size());
			
			if(temp.size() > 3000000) {
				temp = null;
				temp = new ArrayList<String>();
				
			}
		}
	}
}

Heap ismini taşıyan sınıf bünyesinde x’lerden oluşan uzunca bir String nesnesini sonsuz bir döngü içinde temp ismindeki bir listeye yerleştiriyoruz. Liste bünyesinde yer alan String nesne adedi 300.000’e ulaştığında listeye null değerini atayarak siliyor ve yeniden bir liste oluşturuyoruz. Döngü içinde tekrar String nesneleri yeni listeye ekleniyor. Buradaki amacım hafızanın tükenmesi ile java.lang.OutOfMemoryError oluşmasını engellemek ve Garbage Collector’ün değişik heap alanları üzerinde nasıl çalıştığını gösterebilmekti. Java’da herhangi bir nesneye null degeri atandığında Garbage Collector’e “bu nesne ölmüştür, yok edilebilir” mesajı verilmektedir. Eğer bu nesneye herhangi başka bir nesneden referans yoksa, Garbage Collector bu nesneyi öldü kabul edip, temizleyecektir. Şimdi beraber videoyu izleyelim.

Videoda yer alan program için 64 MB büyüklüğünde bir heap alanını -Xms64m -Xmx64m JVM parametreleri kullanarak tahsis ettim. Eğer heap alanı 10 MB olursa OutOfMemoryError hatası oluşmaktadır. Aynı şekilde liste içindeki String nesne adedini 600.000’e çıkardığımda hafıza yetersizliğinden dolayı OutOfMemoryError hatası oluşmaktadır.

Video ve oluşan Garbage Collection aktiviteleri hakkında detaya girmeden önce, değişik Garbage Collector mekanizmalarını ve algoritmalarını gözden geçirelim.

Generational Garbage Collection

Tipik bir Java uygulaması gözlemlendiğinde, uygulamanın hayatı süresince oluşan birçok nesnenin kısa ömürlü oldukları görülmektedir. Oluşturulan nesnelerin çogu bir metot son bulduktan sonra, sadece metot bünyesinde kullanıldıkları için hayata gözlerini yumarlar. Bunun yanısra bir Java uygulaması bünyesinde nesneler yaşlanarak belli bir yaşa erişebilirler. Bir nesnenin yaşlanma süreci uygulama bünyesinde kullanılma derecesini yansıtmaktadır.


Resim 1

Java uygulamarında oluşturulan nesnelerin hayat döngülerini yönetmek icin Sun firması tarafından JDK 1.3 versiyonu ile Generational Garbage Collection geliştirilmiştir. Resim 1’de görüldüğü gibi Generation Garbage Collection ile Java uygulamasının kullandığı hazıfa iki jenerasyona bölünmektedir. Yeni doğan nesneler genç jenerasyon (young generation), orta ve ileri yaşlı nesneler yaşlı jenerasyon (old generation) bölümünde konuşlandırılırlar.

Nesnelerin ait oldukları yaş gurubuna gore ihtiyaç duyulan hafıza yönetim mekanizmaları farklılık gösterir. Yeni jenerasyon bünyesinde nesne doğum ve ölümleri çok hızlı gerçekleşir. Bu bölümde kullanılan Garbage Collection algoritmasının yeni doğan nesnelere hızlı bir şekilde hazıfa alanı tahsis etmesi, ölen nesneleri ortadan kaldırması gerekmektedir. Bu jenerasyon için kullanılan Garbage Collection algoritması bu ihtiyaca cevap verecek şekilde geliştirilmiştir. Yaşlı jenerasyonda durum farklıdır. Bu bölümde Formel 1’de olduğu gibi hızlı haraket etme gerekliliği yoktur. Bu jenerasyonda nesneler daha yavaş bir hayat temposuna sahiptir. Bu jenerasyonda kullanılan Garbage Collection algoritması buna uygun şekilde geliştirilmistir.

Young Generation Garbage Collection

Young generation üç hafıza alanından oluşmaktadır. Bunlar Eden, Survivor 1 ve Survivor 2 hafıza alanlarıdır. Bir Java uygulamasında nesnelerin hepsi gözlerini Eden hafıza alanında dünyaya açarlar. Survivor 1 ve Survivor 2 alanları geçici ara hafıza alanı olarak kullanılır. Eden hafıza alanı dolduğunda, Garbage Collector hayatta kalan nesneleri önce boş olan Survivor alanlarından birisine kopyalar. Bu işlemin ardından Garbage Collector Eden ve kullanımda olan Survivor alanını boşaltır. Eden hafıza alanı tekrar dolduğunda, hayatta kalan nesneler ve dolu olan Survivor alanındaki nesnelerin hepsi tekrar boş olan Survivor alanına kopyalanır. Her zaman bir Survivor alanı boştur ve bir sonraki Garbage Collection işlemi sonunda hayatta kalan nesneleri bünyesine alacak şekilde pasif olarak bekler. Videoda da Eden ve Survivor hafıza alanlarının Garbage Collector tarafından doldurulma ve boşaltılma işlemleri görülmektedir. Young generation bünyesinde çalışan Garbage Collection mark & copy (işaretle ve kopyala) algoritmasını kullanmaktadır. Şimdi bu algoritmanın nasıl çalıştığını yakından inceleyelim.

Mark & Copy Algoritması

JVM bünyesinde kullanılan tüm Garbage Collection mekanizmaları hayattaki nesneleri işaretleme (mark) işlemi ile işe başlarlar. Garbage Collector hangi nesnelerin hayatta olduğunu anlamak için kök setini (root set) oluşturan nesnelerden yola çıkıp, diğer nesnelere olan bağlantıları takip eder. Bu işlemi bir ağacın kökünden yola çıkarak, dallarına kadar erişme olarak düşünebiliriz. Garbage Collector bu şekilde hayatta olan tüm nesneleri işaretler. Erişemediği diğer nesnelerin hepsini ölü kabul eder ve bir sonraki Garbage Collection ile ortadan kaldırılmalarını sağlar. Kök setinde kendisine doğru nesne referansı olmayan, ama kendisinden başka nesnelere referans giden nesneler yer alır. Bu tip nesneler genelde metot parametreleridir ya da global geçerliliği olan statik olarak tanımlanmış nesnelerdir.


Resim 2

Resim 2’de kök setin A ve E nesneslerinden oluştuğu görülmektedir. Garbage Collector işaretleme işine önce A nesnesinden yola çıkarak başlar. B, C ve dolaylı olarak D nesnelerine erişir ve bu nesne kümesini (A,B,C,D) hayatta olan nesneler olarak işaretler. İşaretleme işine E nesnesi ile devam eder. E nesnesi kök setinde yer alan bir nesnedir, lakin E’den yola çıkarak başka bir nesneye erişmek mümkün değildir. Ayrıca başka bir nesne de E nesnesine işaret etmemektedir. Bu yüzden E nesnesi artık ölmüş bir nesnedir ve bir sonraki Garbage Collection ile E nesnesinin işgal ettiği hafıza alanı temizlenir.

Garbage Collector işaretleme (mark) işlemini tamamladıktan sonra hayatta kalan nesneleri aktif olan Survivor alanına kopyalama işlemine başlar. Garbage Collector resim 2’de görüldüğü gibi hayatta kalan nesne kümesini (ABCD) aktif olan Survivor 1 alanına kopyalar. Nesneler, işgal ettikleri adres alanları arka arkaya gelecek şekilde Survivor 1’de konuşlandırılır. Garbage Collection işlemi sonunda hayatta kalan nesnelerin birbirlerine olan referansları ve sahip oldukları adres alanları değişeceği için Garbage Collector işaretleme ve kopyalama işlemi öncesinde uygulama bünyesinde çalışan tüm threadleri durdurmak zorundadır. Bu işlem JVM terminolojisinde dünyayı durdurma (stop the world) olarak adlandırılır. Belli bir süre boyunca uygulama durur ve uygulama bünyesinde aktif olan sadece Garbage Collector’dür. Eğer Garbage Collector işine başlamadan önce dünyayı durdurmamış olsa, aktif olan uygulama threadleri nesne referansları üzerinde değişiklik yaparak, Garbage Collector’ün yanlış nesneler üzerinde işlem yapmasına sebep olabilirler. Bu sebepten dolayı Garbage Collection öncesi dünyanın durması gerekmektedir.


Resim 3

Resim 3’de Garbage Collection sonrasınra Eden bünyesinde yeni nesnelerin oluştuğu görülmektedir. Kök set içinde X ve W nesnesi yer almaktadır. Garbage Collector X,Y,Z ve Survivor 1’de yer alan ABCD nesnelerini Survivor 2’ye kopyalar. Garbage Collection işlemi sonunda young generation hafıza alanındaki son durum resim 4’de yer almaktadır.


Resim 4

Görüldüğü gibi her Garbage Collection sonunda Survivor alanları rolleri değiştirmektedir. Survivor alanlarının her Garbage Collection sonrası yer degiştirmeleri resim 5’de de görülmektedir.


Resim 5

Resim 5’de yer alan birinci kolon Survivor 1’in, ikinci kolon Survivor 2’nin, üçüncü kolon Eden’in, dördüncü kolon Old hafıza alanının doluluk oranını göstermektedir. Görüldüğü gibi Garbage Collector düzenli olarak hayatta kalan nesneleri kopyalamak için pasif olan Survivor alanını kullanmaktadır.


Resim 6

Bir Java uygulaması -XX:+PrintHeapAtGC JVM parametresi ile çalıştırıldığv zaman, resim 6’da yer alan Garbage Collection log kayıtları oluşmaktadır. İlk kırmızı işaretli alan içinde Survivor 1 from, Survivor 2 ise to olarak isimlendirilmiştir. Survivor 1’in hafıza başlangıç adresi 0x30e40000, Survivor 2’nin hafıza başlangıç adresi 0x30c20000’dir. İkinci kırmızı işaretli alana baktığımızda from ve to’nun adres alanlarının değiştiğini görmekteyiz. Garbage Collection işlemi oluşmuş ve Survivor alanları yer degiştirmiştir. Garbage Collector tarafından hep bir Survivor alanı reserve durumunda bir sonraki Garbage Collection içinde kullanılmak üzere beklemede tutulur. Bu dogal olarak belirli bir hafıza alanının kullanılmayarak israf edildiği anlamına gelir. Ne yazık ki bu Mark & Copy algoritmasının bir dejavantajıdır. Bunun haricinde bu algoritma hızlı bir şekilde Eden bünyesinde olup bitenlere cevap verebilecek yetenekte ve kapasitededir.

Minor & Major Garbage Collection

Garbage Collection minor (küçük) ve major (büyük) olmak üzere ikiye ayrılır. Minor Garbage Collection’ı kendi odamızı, Major Garbage Collection’ı evimizi temizleme gibi düşünebiliriz. Young Generation bünyesinde yapılan Garbage Collection işlemine minor Garbage Collection, JVM kontrolündeki tüm hafıza alanlarının temizlenme işlemine major Garbage Collection ismi verilir.


Resim 7

Resim 7’de Garbage Collector tarafından yapılan minor ve major Garbage Collection işlemleri yer almaktadır. GC ile başlayan satırlar minor, Full GC ile başlayan satırlar major Garbage Collection yapıldığınin ibaresidir. Major Garbage Collection işlemi için ayrıca Full Garbage Collection ismi de kullanılmaktadır.

İlk satırda young generation tarafından işgal edilen hafıza alanı toplamda 11994 kilobytetır (~ 12 MB). Minor Garbage Collection işlemi sonunda ölen nesneler temizlenmiş ve hayatta kalan nesnelerin kapladığı alan 4048 (~ 4 MB) kilobyte olarak değişmiştir. Garbage Collector minor Garbage Collection işlemi ile 8 MB civarında ölü nesneyi hafıza alanından kaldırmıştır. Bu minor Garbage Collection işlemi 0.02 saniye sürmüştür. Bu zaman diliminde JVM bünyesinde sadece Garbage Collection işlemi yapılmıştır. Parantez içinde yer alan 63360K (~ 64 MB) JVM tarafından kullanılan tüm hafıza alanlarının toplamını yansıtmaktadır. JVM parametreleri olarak -Xms64m -Xmx64m kullandığımız için, programın işgal ettiği toplam hafıza alanı 64 MB’dir.

Eden hafıza alanı, Old hafıza alanına göre daha küçük olduğu için bu alanda minor Garbage Collection işlemleri Old generation bünyesinde olan major Garbage Collection işlemlerinden sayıca çok daha fazladır. Resim 7’de Garbage Collector 20 minor Garbage Collection, buna karşılık sadece 4 major Garbage Collection gerçekleştirmiştir.


Resim 8

Yedinci satırda major Garbage Collection yapıldığını görmekteyiz. Tüm JVM bünyesinde 56241 kilobyte (~56 MB) hafıza alanı kullanımdadır (young + s0 + s1 + old). Görüldüğü gibi bu değer JVM hafıza büyüklüğü olan 64 MB’ye yakın bir değerdir. Resim 8’de young ve old generation hafıza alanlarının dağılım oranları yer almaktadır. JVM Eden için 17 MB, S0 için 2 MB, S1 için 2 MB ve Old icin 42 MB alan ayırmıştır. JVM parametreleri ile hafıza alanlarının büyüklüklerini değiştirmek mümkündür. Buna yönelik bir ayarlama yapılmadığında JVM otomatik olarak hafıza alanlarının büyüklüğünü ayarlar. Full GC ardından hayatta kalan nesnelerin kapladığı alan 6011 kilobyte (~6MB) olarak değişmiştir. Bu major garbage collection işlemi ile 50 MB büyüklüğünde nesneler Garbage Collector tarafından ebediyete uğurlanmıştır.

Durdurun Dünyayı

“Durdurun dünyayı başım dönüyor” diyor Ferdi Tayfur şarkısında. JVM mucitleri Ferdi Tayfur’un bu şarkısından esinlenerek dünyayı durduran Garbage Collector algoritmaları geliştirmişlerdir. Arabesk müziğin Java üzerindeki etkilerini başka bir yazımda detaylı olarak ele alacağım. Arabeskin programcılık üzerinde etkileri düşünüldüğünden çok daha fazladır. (:)

Garbage Collector minor Garbage Collection için tek bir thread (Reaper Thread) kullanmaktadır. Garbage Collection işlemi esnasında Garbage Collector JVM bünyesinde çalışan tüm threadleri durdurur. Uygulama için dünya durmuş olur. Dünya ne kadar uzun durursa, uygulamanın kullanıcıya verdiği cevaplar o oranda gecikir. Buradan Garbage Collection işlemi esnasında verilen araların mümkün olduğunca kısa olması gerektiği sonucu çıkmaktadır. JVM mucitleri bunu göz önünde bulundurarak paralel çalışan minor Garbage Collection algoritması geliştirmişlerdir.

Paralel çalışan minor Garbage Collection algoritması, tekil threadle işini gören minor Garbage Collection ile teoride aynı yapıdadır. Aralarındaki tek fark, parelel minor Garbage Collection için birden fazla threadin çalışmasıdır. Bu şekilde stop the world duraklamaları daha kısaltılmış olur. Bu uygulamanın kullanıcıya karşı cevapkanlık oranını artırmaktadır.

Paralel minor Garbage Collection nasıl çalışmaktadır? Garbage Collector’ün temizleme işlemine hayatta kalan nesneleri işaretleyerek (mark) başladığını yazmıştım. Garbage Collector bu amaçla kök setinde yer alan nesneleri tarar ve bu nesnelerden yola çıkarak işaretleme işlemini yapar. Akabinde hayatta kalan tüm nesneleri aktif olan Survivor alanına kopyalar (copy). Paralel Minor Garbage Collection için kök setinde yer alan nesneler birden fazla Garbage Collection threadine atanır. Birden fazla thread paralel olarak kök setindeki bir nesneden yola çıkarak hayatta kalan nesneleri tararlar. Bu şekilde işaretleme performansı artırılır ve işaretleme için harcanan zaman azalır.

Hayatta kalan nesnelerin aktif olan Survivor alanına da kopyalanma işlemi buna benzer bir şekilde gerçekleşir. Her threade Survivor hafıza alanında PLAB (PLAB – Parallel Local Alocation Buffer) ismi verilen bir alan atanır. Garbage Collection threadleri birbirlerinden bağımsız olarak keşfettikleri ve hayatta kalmış olan nesneleri kendi PLAB alanlarına kopyalarlar.

Paralel Garbage Collection paralel çalışan threadlere rağmen dünyayı durduran bir algoritmadır. Tekil threadle çalışan Garbage Collection algoritmasına göre daha hızlı çalışır ve uygulamanın durma zamanlarını kısaltır. Birden fazla işlemcisi (CPU – Central Processing Unit) olan bir sunucu üzerinde paralel Garbage Collection algoritmasını kullanmak bu kazancı getirir. Lakin sadece bir işlemcisi olan sunucularda paralel Garbage Collection’ın kullanılması tam tersi bir etki yaratır.

Yazımın ikinci bölümünde major Garbage Collection’ın nasıl çalıştığını inceleyeceğim.

EOF (End Of Fun)
Özcan Acar

Share Button
0.00 avg. rating (0% score) - 0 votes

8 Comments

  • SBilge

    18 Mayıs 2012

    Abi diğer arkadasları bilmiyorum ama ben videoyu izleyemedim ses gelmiyor.. Görüntüde yok dersek yeridir.

  • Özcan Acar

    18 Mayıs 2012

    video sessiz zaten; sessiz sinema ;-)

  • Cihad

    18 Mayıs 2012

    Bir Rubyist olarak yazılarınızı dikkatle takip ediyorum. Teşekkür ederiz.

  • Muhammed

    27 Haziran 2012

    Teşekkürler Özcan Hocam. Benim için çok faydalı bir makale oldu.

  • Pingback: Garbage Collector nedir? | ozgrcn

  • emrkal

    24 Temmuz 2015

    Özcan Hocam Selamlar,

    Yazınızın ikinci bölümünü paylaşmışmıydınız.

  • Özcan Acar

    30 Temmuz 2015

    Henüz kaleme alamadim.

  • Hakan Akbulak

    13 Mayıs 2016

    Özcan Hocam Selamlar,

    Java Program Analizi hakkında makaleniz var mı ?

Bir cevap yazın