Java Memory Model

ümit Samimi
Finartz
Published in
8 min readDec 30, 2020

Uzun zamandır değinmeyi planlandığım fakat vakit ayıramadığım bir konuyu bugün elimden geldiğince anlaşılabilir şekilde yazacağım.

Yazı biraz uzun, bu sebeple konu başlıklarını önden listelemek gerekirse

  • JVM nedir, nasıl çalışır?
  • ClassLoader nedir, nasıl çalışır?
  • Runtime Data Areas nedir, nasıl çalışır?
  • Execution Engine nedir, nasıl çalışır?
  • Garbage Collector nedir, nasıl çalışır?

JVM nedir?

JVM ( Java Virtual Machine ) bytecode’ları çalıştıran bir sanal makinadır. C++ dili ile yazılmıştır. Biraz konuyu açmak gerekirse, bizim yazdığımız .java uzantılı dosyalar, derleyici tarafından derlendikten sonra .class uzantılı hale getirilir. Yani byte code’a dönüştürülür. JVM’de bu byte code’ları çalıştırır.

“Write once, run anywhere” sloganının sebebi de budur. Her platform için bir JVM sürümü vardır ve yazdığınız kod, o platformlar üzerinde JVM sayesinde çalıştırılabilir.

JVM’i temel anlamda 3 bileşenden oluşur diyebiliriz.

  • Class loader : Byte code ların belleğe(Runtime Data Areas) yüklendiği yer.
  • Runtime data Areas ( Heap, Stack, Method Area, PG Register, Native Method Stack)
  • Execution Engine : Belleğe yüklenen byte code ları çalıştıran yer diyebiliriz.

Aslında basit bir senaryo ile sürecin üzerinden geçmiş olduk. Developer kodu yazar. Derleyici kod derledikten sonra .java olan uzantıları .class’a yani byte code a çevirir. JVM içerisinde class loader bu byte code’ları alır ve belleğe yükler. Execution Engine de çalıştırır.

Biraz detaylandıralım öyle ise.

Class Loader

Class loader, JVM ilk çalıştığıda sınıfları belleğe yükleme işlemini yapar. Kabaca 3 bileşenden oluşur diyebiliriz.

  • Load
  • Link
  • Initialize

Load

Load süreci de kendi içerisinde 3 bölümden oluşur. Bootstrap Class Loader, Extension Class Loader, Application Class Loader.

  1. Bootstrap Class Loader, java core api diye isimlendirebileceğimiz “primary packages and classes” altındaki sınıfları yükler. Bu sınıflar $JAVA_HOME/jre/lib dizinin altında yer alır.
  2. Extension Class Loader ise Bootstrap Class Loader’ın bir alt öğesidir. $JAVA_HOME/lib/ext dizini altındaki sınıfları yükler. (Ya da java.ext.dirs system property dosyasının içindekileri)
  3. System/Application Class Loader ise Extension Class Loader’ın bir alt öğesidir. Uygulama seviyesindeki sınıfları ( classpath, environment variable) yüklemek ile sorumludur.

Bir sınıfa erişmek istediğinizde, JVM erişmek istediğiniz sınıfın heap içerisinde olup olmadığını kontrol eder. Eğer varsa, sınıfı döner. Eğer heap içerisinde bulamazsa, “ClassLoader” bu sınıfı arar. Bulabilirse, heap içerisinde bu sınıftan oluşturur. Bulamazsa “ClassNotFoundException” fırlatır.

Bir sınıf nesnesi heap içerisinde bulunamadığında, ilk iletişim Application Class Loader ile olur. Eğer Application Class Loader sınıf nesnesini bulamazsa, Extention Class Loader sınıf nesnesini arar. Eğer o da bulamazsa Bootstrap Class Loader bu sınıf nesnesini arar. O da bulamazsa ClassNotFoundException fırlatılır.

Link

Sınıfların ve arayüzlerin doğrulama (verification), hazırlanma (preparration) ve çözümlenme(resolution) işlemlerinin yapıldığı yerdir.

  1. Verification: Bytecode (.class) geçerli olup olmadığı, yapısının doğru olup olmadığı, JVM ile uyumlu olup olmadığı, geçerli bir derleyici tarafından derlenip derlenmediği bu aşamada kontrol edilir. Eğer bir şeyler yolunda gitmezse ve bir problem çıkarsa java.lang.VerifyError fırlatılacaktır.
  2. Preparation: Sınıf seviyesinde veya arayüz seciyesinde static değişkenler, varsayılan değerler için bellekte yer açılması işlemi bu aşamada yapılır. Eğer bellekte yeteri kadar alan yoksa java.lang.OutOfMemoryError fırlatılır
  3. Resolution : Çözümlenme aşamadır. Sınıf referanslarının belleğe yüklenmesi ve sembolik referansların güncel bellek adreslerini kullanarak çözümlenmesi yapılır.

Initialize

Static kod blokların ve static değişkenlerini ilk değer atamalarının yapıldığı aşamadır. Java’da superclass dediğimiz hiyerarşinin en üstündeki sınıftan, en alta doğru initialize işlemi yapılır. Buna “Top-Down” denir.

Runtime Data Areas

Runtime Data Areas 5 temelde bileşenden oluşur.

  1. Method Area
  2. Heap
  3. Stack
  4. Pc Registers
  5. Native Method Stacks

Heap — Stack

Uygulamada çalıştırıldığında, bilgisayarın belleğini kullanmaya ihtiyaç duyar. Bu belleği işleyiş açısından ikiye ayrılır : Heap ve Stack

Peki neden ikiye ayrılır. Çünkü bellekte ayrılan yerin boyutu bazen bellidir bazen kullanıcının çalışma zamanındaki davranışana göre değişebilir.

Boyutlarını bildiğimiz değerler-değişkenler (int, short, byte, long, decimal, double, float) stack’te tutulur, çalışma zamanında değişkenlik gösterebilecek(nesneler) olanlar ise heap’te.

Stack ve Heap RAM’de tutulur.

Class type değişkenler referans tiplerdir ve referansları stack de değerleri ise heap de saklanır.

Stack
Bir java uygulamasında tek bir stack olmaz. Her thread için ayrı bir stack oluşturulur ve o thread o stack’i kullanır. Veri yapısı olarak stack oldukça verimlidir ve JVM tarafından yönetilir. Bir kurala göre içerisine veriler eklenir ve çıkartılır. Bu kural LIFO yani “Last In First Out” ‘dır. Yani thread tamamlandığında stack otomatik olarak kapanır.

Stack’e yeni bir nesnenin adresi eklendiğinde, nesne adresi listenin en üstüne eklenir. Bu şekilde bir yığın oluşturulur. Çıkartılmak istendiğinde ise, son eklenen nesne ilk çıkar. Bu yüzden bu kurala “Last in first out” ( bazen First in last out da denir) denir. Yukarıdaki örnek üzerinden gidecek olursa, önce “var 3” yığından çıkar, sonra “var 2” ve en son “var 1”.

public class StackTest {
public static void main(String[] args) { // POINT 1
int number = 12; // POINT 2
number = calculate(number); // POINT 3
}
public static int doSomething(int value){ // POINT 4
int plusOne= value + 1;
int return = plusOne + 1;
return return; // POINT 5
} // POINT 6
}

Bir örnek vererek stack’in çalışma mantığını inceleyelim. StackTest isimli bir sınıfımız olsun. Kodun çalışma zamanındaki akışını temsilen, bazı satırlara noktalar (Point1 -2 -3 ) yerleştirip, o noktalardaki stack durumunu inceleyelim.

Point 2 : number isimli primitive bir değişken tanımlanıp, 12 değeri atanıyor.
Point 3 : number değişkeninin yeni değeri bir başka metodun geri dönüş değerine göre değişecek. Çağrılan methodun input parametresi stack’e ekleniyor.
Point 4 : yeni bir metodun içine girildiği için number ve args değişkenleri scope dışında kalıyor.
Point 5 : iki yeni değişken stack’e ekleniyor.
Point 6 : yeni metodun dışına çıkıldı için, yeni metod içerisindeki değişkenler stack’ten çıkarılıyor.
Point 7 : Main metod içerisindeki number değişkenin değeri güncelleniyor.

Heap
Stack’in aksine bir uygulamada sadece 1 tane heap vardır. Tüm objeler heap’te tutulur. Stack’e kıyasla yavaş çalışır. İçerisindeki değişkenlerin kaldırılması işi Garbage Collector’a aittir.

int year = 2023;
String model = "TOGG";

Yularıda bahsettiğimiz gibi ilkel değişken(year) ve referans değişkeni stackte tutulurken, referans değişkenine(Class type / reference variable) karşılık gelen değer heap’te tutulmaktadır.

Heap’te tutulan değerler, kendisine işaret eden herhangi bir referans değişkeni kalmadığında Garbage Collector tarafından temizlenir. Heap konusunun detayına Garbage Collector başlığında tekrar değineceğiz.

Execution Engine

Execution Engine’in temelde 3 önemli işlevi bulunmaktadır.

  1. Interpreter : Byte code içerisindeki talimatların yorumlanması
  2. Garbage Collector : Yazının alt başlığında detaylıca işlenecektir.
  3. JIT Compiler : Açılımı “Just in Time” dır. JIT Compiler, sık çalıştırılan kod bloklarını tespit eder ve bu sık çalıştırılan kod bloklarını native koda çevirerek sonraki çağrımlarda daha performanslı çalışmasını sağlar.

Garbage Collector

Garbage Collection, arkaplanda çalışan otomatik bellek yönetim aracıdır. İsminden de anlaşılacağı üzere bir nesne scope dışına çıktığında, referansı null olarak atandığında ya da referansı başka bir yeni nesneye atandığında (yani çöp olduğunda) artık o nesnenin çağrılma/kullanılma durumu ortadan kalkar var. Heap içerisinde gereksiz yere yer kaplar. Kapladığı yerin yeniden açılması için Garbage Collector (GC) devreye girer ve o nesneyi heap’ten kaldırır. Yani çöpü toplamış olur. Bu sayede geliştirici endişe duymadan yeni nesler oluşturabilir.

Basit anlamda, GC 2 adımla çalışır. Mark and Sweep

  • Mark : Bellek içerisinde kullanılan nesnelerin işaretlendiği yerdir
  • Sweep : “Mark” yani işaretleme adımında kullanılmadığı anlaşılan nesnelerin temizlendiği aşamadır.

Garbage Collector çeşitleri

Serial Collector : Tek bir thread ile çalışır ve en yalın GC implementasyonudur. Serial GC çalıştığında, uygulamanın tüm threadlerini durdurur. Bu sebeple multi-thread uygulamalarda kullanılması tavsiye edilmez. Eğer serial collector kullanılmasını istiyorsanız, kullanmanız gereken argüman :

java — XX:+UseSerialGC -jar Application.java

Parallel Garbage Collector : Eğer herhangi bir argüman ile GC seçimi yapmazsanız, varsayılan olarak kullanılan garbage collector’dur. Serial Garbage Collector’un aksine, multiple thread kullanır fakat yine de uygulama threadleri, gc çalışma zamanında durdurulur.Eğer Parallel Garbage Collector kullanılmasını istiyorsanız, kullanmanız gereken argüman :

java -XX:+UseParallelGC -jar Application.java

Concurrent Collector (CMS) : CMS, birden fazla GC thread’i kullanır. Küçük duraksamalarla GC’nin çalışmasını istiyorsanız, sizin için ideal GC türüdür fakat java9 ile birlikte deprecated olmuştur.

Eğer Concurrent Garbage Collector kullanılmasını istiyorsanız, kullanmanız gereken argüman :

java -XX:+UseParNewGC -jar Application.java

G1 (Garbage First) Collector : JDK7 ‘nin dördüncü güncellemesi ile birlikte gelmiştir. Çok işlemcili ( multi-processor) ve büyük bellek alanına sahip uygulamalar için tasarlanmıştır. Diğer GC’lerin aksine partition yapısına sahiptir. Heap eşit parçalara bölünmüştür.

Oldukça performanslıdır. Eğer G1 Garbage Collector kullanılmasını istiyorsanız, kullanmanız gereken argüman :

java -XX:+UseG1GC -jar Application.java

Garbage Collection Algoritmaları

Tüm GC algoritmalarının amacı aslında terk edilmiş nesneleri tespit etmek ve onları bellekten çıkarmaktır.

4 meşhur GC algoritması vardır.

  1. Mark-Sweep
  2. Mar-Sweep-Compact
  3. Mark & Copy
  4. Concurrent Mark-Sweep

GC çalışmaya başladığında, uygulamadaki diğer tüm thread’leri durdurur. Bu durum “Stop the world” olarak bilinir.

Neden duraksamaya ihtiyaç vardır? Duraksama olmadan GC’nin çalıştığını düşünün. “Dead” durumundaki nesneleri tespit ediyorsunuz. Bunun için zamana ihtiyacınız var. Bu kısa zaman içerisinde sizin “alive” olarak işaretlediğiniz bir nesneye “null” ataması yapılabilir. Bu ve benzeri belirsizliklerin yaşanmaması için, “Stop the world” yapılır.

Heap Yapısı

Heap ile ilgili temel bilgileri yukarıda değinmiştim fakat bazı detayla GC ile doğrudan ilişkili. Bu sebeple heap ile ilgili detaylara burada işleyeceğiz.

Yaratılan tüm nesneler, heap içerisinde yaşlandırmaya tabi tutulur. Bir nesne yaratılmışsa, bir ömrü olmalı öyle değil mi? Ömrü uzadıkça, heap içerisinde farklı bölümlerde aktarımı sağlanır.

Yeni oluşturulan tüm nesneler “Eden” alanında tutulur. Survivor 1 ve 2 nolu space’ler boştur. Tabi “Eden” alanı belli bir aşama sonrası dolar ve yer kalmaz. Bu aşamada “Minor Garbage Collection” işlemi tetiklenir ve “Eden” alanında hala yaşamı devam eden(referansı olan) nesneler “Survivor 0” alanına taşınır. Kullanılmayan nesneler ise GC tarafından silinir.

Yukarıdaki işlem aslında bir “Minor Garbage Collection” cycle’ına denk gelmektedir. Bir sonraki cycle’da yine aynı işlemler gerçekleşir fakat bu kez “Survivor 0” alanı yerine “Survivor 1” alanına taşınır. Bir önceki “Survivor 0” alanına taşınmış olan nesnelerin yaşları arttırılır. Yaş arttırımı sonrasında “Survivor 0” alanındaki nesneler de “Survivor 1” alanına taşınır. Tüm bu işlemler tamamlandıktan sonra da “Eden” ve “Survivor 0” alanları temizlenir.

Bir sonraki cycle’da aynı adımlar tekrarlanır. “Survivor 1” alanındakiler eğer hala yaşamaya devam ediyorlarsa yaşları artırılarak “Survivor 0” alanına taşınır. “Survivor 0” alanındakiler eğer hala yaşıyorlarsa yaşları artırılarak “Survivor 1” alanına taşınır. Yaşamayan yani garbage collector için “eligible” olan nesneler ise GC tarafından temizlenir.

Aslında yaş arttırma dediğimiz işlemin gerçek adı “cycle count”. Her cycle sonrası bu değer artırılır.

Bahsettiğimiz cycle devam ettikçe, kullanılmaya devam eden uzun ömürlü nesnelerin “cycle count” değerleri de artacaktır. Belli bir threshold ( varsayılan değer 15'tir) değerinden sonra bu nesneler “Old Generation” alanına aktarılacaktır. (XX:MaxTenuringThreshold komutu ile varsayılan değer değiştirilebilir.)

“Old generation” için sokaktaki çöp kutusu, “young generation” için ise odandaki çöp kutusu örneği verilir. Odandaki çöp kutusunu belki her gün hatta bazen günde birkaç kere temizlerken, sokaktaki çöp kutusu haftada iki defa belediye tarafından temizlenir.

Umarım okuduğunuza değişmiştir :)

Not : Bir sonraki yazımızda yeni nesil GC’leri işliyor olacağız.

--

--