Java Virtual Machine(JVM) Architecture 3
Java Virtual Machine(JVM) Architecture 1 makalesine buradan erişebilirsiniz.
Java Virtual Machine(JVM) Architecture 2 makalesine buradan erişebilirsiniz.
Execution Engine(Yürütme Motoru)
Serinin 1 ve 2. makelelerinde “execution engine” öncesi süreçlerini işledik.
Bu süreçte ise byte kodu ana belleğe yüklendikten ve buna bağlı olarak ayrıntılar çalışma zamanı(runtime) veri alanında mevcut hale getirildikten sonra,bir sonraki adım olan,hazır hale getirilen bu programı çalıştırmak olacaktır.Dolayısıyla runtime veri alanına atanan byte kodu,execution engine tarafından yürütülecektir.
Ancak program çalıştırılmadan önce,byte kodunun makine dili talimatlarına dönüştürülmesi gerekir.JVM,execution engine için bir yorumlayıcı veya bir JIT(Just-in-time) derleyici kullanabilir.Aşağıdaki görünümde “Execution Engine” yapısı gösterilmektedir.
Execution engine bünyesinde 3 adet bölüm yer alır ve bu bölümler aşağıda incelenmektedir.
Interpreter(Yorumlayıcı)
Yorumlayıcı byte kodu talimatlarını satır satır okur ve yürütür.Satır satır işlemesi nedeniyle çalıştırma aşamasında nispeten yavaştır. Ayrıca bir kod birden fazla kez çağrıldığında,her seferinde yeni bir yorumun gerekli olması bir dezavantajdır.
JIT Compiler(Derleyici)
JIT derleyici,yorumlayıcının dezavantajlarını ortadan kaldırır.Çünkü yürütme motoru byte kodunu dönüştürmede yorumlayıcıyı kullanır. Ancak yukarıda bahsettiğimiz tekrarlanan kodlarla karşılaştığında,tüm byte kodunu derleyen ve native koduna dönüştüren JIT derleyicisini kullanır.
Dolayısıyla JIT tüm byte kodlarını derler ve native koduna dönüştürür.Böylelikle yorumlayıcı tekrarlanan kod çağrılarını gördüğünde-karşılaştığında,JIT bu tekrarlanan parça için doğrudan native kodu sağlar. Böylece tekrarlanan kısım için yeniden yorumlama gerekmez ve verimlilik artar. Dolayısıyla bu aşamada bir iyileştirme sözkonusudur.
JIT derleme süreci aşağıdaki görüntüde verilmektedir.
Yine aşağıdaki görüntüde,JIT’in Solaris JVM ve JRE ile işlevsel ilişkisi gösterilmektedir.
JIT derleyicisi aşağıdaki bileşenlere sahiptir.
- Intermediate Code Generator //Ara kod üretir.
- Code Optimizer //Daha iyi performans için ara kodu optimize eder.
- Target Code Generator //Ara kodu native makine koduna dönüştürür.
- Profiler //Tekrarlanan noktaları bulmaktan sorumlu özel bir bileşendir.
Interpreter(yorumlayıcı) ve JIT derleyicisi arasındaki farkı daha iyi anlamak,dolayısıyla tekrarlanan noktaların JIT tarafından nasıl optimize edildiğini anlamak için aşağıdaki kodu inceleyelim.
Yorumlayıcı döngüdeki her yineleme için “val”’in değerini bellekten alacak,i’nin değerini ekleyecek ve “val” değerini belleğe geri yazacaktır. Döngü her tekrarlandığından dolayı bu işlem oldukça maliyetlidir.
Ancak JIT derleyicisi bu kod parçasının bir tekrarlanma sürecine sahip olduğunu fark edecek ve üzerinde optimizasyonlar gerçekleştirecektir.
Optimizasyon süreci şu şekildedir:
1.val değerinin bir kopyasını PC register’a sakla.
2.Döngü her tekrarlandığında PC register’daki bu kopyaya i değerini ekle.
3.Döngü tamamlandığında PC register’daki bu değeri val’in belleğine geri yaz.
JIT oldukça faydalı bir araçtır ancak kodları derleme süresi biraz zaman alabilir.
Garbage Collector(GC)
Çöp toplayıcısı(gc),kullanılmayan nesneleri heap alanından toplar ve kaldırır.Dolayısıyla kullanılmayan runtime belleğini otomatik olarak yok ederek sisteme geri kazandırır.
Herhangi bir durumda çöp toplama sistemini çağırabiliriz. Bunun için “System.gc()” ile çöp toplayıcısını tetikleyebiliriz. Ancak bizim bu sistemi çağırmamız-tetiklememiz onun anında devreye gireceği anlamına gelmez.Biz onu çağırsak bile,sistemin devreye girmesi için bazı şartların sağlanıyor olması gerekir. Bu şartlar biz geliştiriciye yönelik değil,kendi algoritmik yapısıyla alakalıdır.
Gc ile ilgili kapsamlı bir yazı serisi veya vlog düşünüyorum. Ancak temel olarak bilmemiz(yeni başlayanlar) için çalışma mimarisini bir miktar detaylandıracağım.
Programcılar işe yaramaz,kullanılmayan(çöp) nesnelerin yok edilmesini ihmal edebilir.Bu ihmal nedeniyle belirli bir noktada yeni nesnelerin oluşturulması için yeterli bellek alanı kalmaz. Dolayısıyla bu noktada programımız “OutOfMemoryErrors” gibi hata fırlatmalarıyla sonlanır.
Java’da bu sorumluluğu programcının umuzlarından almak için GC sistemi tasarlanmıştır ve tüm bu süreci otomatik olarak yönetir. Dolayısıyla GC her zaman arka planda çalışır ve otomatik olarak çöp toplama işlemini gerçekleştirir.
GC’nin temel görevinin ulaşılamayan(çöp)/erişilemeyen nesnelerin yok edilmesi olduğunu belirtmiştik.
Ancak bir nesne nasıl çöp haline gelir bunu inceleyelim.
String str = new String("zamazingo");
Yukarıda bir nesne oluşturuldu ve bu nesneye “str” üzerinden ulaşılabilir.
String str = new String("zamazingo");
str = null;
Ancak daha sonra nesne “null” hale getirildi. Dolayısıyla str nesnesine bu noktadan sonra erişilemez ve o artık GC’nin hedefindedir.
Yukarıdaki örnekte söz konusu nesne programcının eliyle çöp hale getirildi. Ancak bir nesnenin çöp hale gelmesinin birkaç yolu ve durumu vardır.
Bunlar:
-Referans değişkeninin geçersiz kılınması.
-Referans değişkeninin yeniden atanması.
-Method ve iç kod blokları içinde oluşturulan nesneler. Bu durumda method sonuna varıldığında method içinde oluşturulan nesneler çöp hale gelir. Ayrıca iç kod blokları için de aynı durum söz konusudur. Aşağıda iç kod bloklarını gösteren bir kod parçasına yer verilmiştir.
GC mimarisini daha detaylı inceleyebiliriz,ancak yazı serimizin dışında kaldığı için fazla uzatmayacağım.Son olarak ulaşılamayan/erişilemeyen nesnelerin yok edilme aşamalarını inceleyip bu bölümü bitirelim.
Çöp toplama iki ana aşamadan oluşur.Bunlar:
1.Mark: Bu aşamada,GC hafızadaki kullanılmayan nesneleri tanımlar.
2.Sweep: Bu aşamada,GC önceki aşamada tanımlanan nesneleri kaldırır.
Çöp toplama işlemi düzenli aralıklarla JVM tarafından otomatik olarak yapılır ve programcının ayrıca bununla ilgilenmesi gerekmez. Ancak programcı dilediği zamanda-durumda “System.gc()” komutuyla GC’yi tetikleyebilir. Ancak programcının müdahalesiyle GC’nin çalıştırılacağı garanti edilmez.
JVM 3 farklı türde çöp toplayıcı içerir. Bunlar:
1.Serial GC: Bu GC’nin en basit halidir ve tek iş parçacıklı(Thread) ortamlarda çalışan küçük uygulamalar için tasarlanmıştır. Çöp toplama işlemi için tek bir iş parçacığı kullanır. Dolayısıyla çöp toplama işlemi devreye girdiğinde tüm uygulama temizleme sonlanana kadar duraklatılır. Serial GC’yi kullanmak için JVM komutu olarak “-XX:+UseSerialGC” kullanabilirsiniz.
2.Parallel GC: Bu GC’nin JVM’deki default uygulamasıdır.Çöp toplama işlemi için birden fazla iş parçacığı(multiple thread) kullanır. Ancak yine de çalışırken uygulamayı duraklatır. Paralel GC’yi kullanmak için JVM komutu olarak “-XX:+UseParallelGC” kullanabilirsiniz.
3.Garbage First (G1) GC: G1,büyük yığın boyutuna(4 GB’den fazla) sahip çok iş parçacıklı uygulamalar için tasarlanmıştır. Sözkonusu yığını bir dizi eşit boyutlu bölgeye böler ve bunları taramak için birden fazla iş parçacığı kullanır. G1,en fazla çöp içeren bölgeleri belirler ve çöp toplama işlemini önce bu bölgede gerçekleştirir. G1'i kullanmak için JVM komutu olarak
“-XX:+UseG1GC” kullanabilirsiniz.
NOT: Concurrent Mark Sweep (CMS) GC adında başka bir çöp toplayıcı türü vardır. Ancak Java 9'dan itibaren kullanımdan kaldırıldı ve Java 14'de tamamen kaldırıldı.
KAYNAKLAR:
https://docs.oracle.com/javase/specs/jvms
https://docs.oracle.com
-end of