JVM Nasıl Çalışır Yazı Serisi – JVM Stack Nedir ve Nasıl Çalışır?

Java sanal makine bünyesinde (JVM – Java Virtual Machine) vücut bulan her thread ile birlikte thread e özel ve stack ismini taşıyan bir hafıza alanı oluşturulur. Stack bünyesinde thread in içinde bulunduğu (koşturduğu) metotlardaki lokal değişkenler, yapılan işlemler için gerekli parametreler (operand), işlem sonuçları ve metot giriş (invocation) ve çıkış (return) bilgileri yer alır.

Bu bilgiler frame ismini taşıyan yapılar içinde tutulurlar. Her metot girişinde yeni bir frame oluşturulur. Aşağıda yer alan resimde program akışı main()->metot1()->metot2()-metot3() şeklindedir. Program metot3() bünyesinde bir breakpoint yardımı ile durdurulduğunda, main thread için dört adet frame in oluşturulduğu görülmektedir.

Aşağıdaki resimde stack yapısını görselleştirmeye çalıştım.

Güncel (current) thread in girdiği her yeni metot ile yeni bir frame oluşturulur demiştim. Her frame bünyesinde metot değişkenlerinin yer aldığı bir array, işlem parametrelerini (operand) ve sonuçlarını bünyesinde tutan bir LIFO (last in, first out) stack ve sınıfın runtime constant pool (sınıf bünyesinde kullanılan değişken isimleri, metot ve diğer sınıf referanslarının yer aldığı bir veri yapısı) yapısına bir referans yer alır.

JBE (Java Bytecode Editor) ile oluşturduğumuz Java sınıfını decompile ettiğimizde, lokal değişkenlerin derlenen sınıf bünyesinde bir array içinde tutuldukları ve index numarası üzerinden stack işlemlerinde bu lokal değişkenlere erişilebileceğini görmekteyiz.

Şimdi bir metot bünyesinde stack işlemlerinin nasıl işlediğini bir örnek üzerinde birlikte inceleyelim. Stack işlemlerini incelediğimiz sınıf aşağıda yer almaktadır:

public class Main {

	public static void main(final String[] args) {

		metot1();
	}

	private static void metot1() {
		metot2();
	}

	private static void metot2() {
		final int x = 1;
		metot3(x);
	}

	private static void metot3(final int x) {
		final int y = 6;
		final int i = x + y;
		System.out.println(i);
	}

}

Bu sınıfı decompile ettiğimizde, aşağıdaki bytecode yapısını elde edebiliriz:

* ******************************************** */
/* Generated by Dr. Garbage Bytecode Visualizer */
/* http://www.drgarbage.com                     */
/* Version: 4.4.1.201408050542                  */
/* Class retrieved from: Filesystem             */
/* Retrieved on: 2015-01-08 12:30:25.220        */
/* ******************************************** */

/* class file format version 50.0 (java 1.6) */
package com.kodkata.kata.jvm;

public class Main {

    /* compiled from Main.java */

    public Main() {
        /* L3 */
        0 aload_0;                /* this */
        1 invokespecial 8;        /* java.lang.Object() */
        4 return;
    }

    public static void main(java.lang.String[] args) {
        /* L7 */
        0 invokestatic 16;        /* void com.kodkata.kata.jvm.Main.metot1() */
        /* L8 */
        3 return;
    }

    private static void metot1() {
        /* L11 */
        0 invokestatic 21;        /* void com.kodkata.kata.jvm.Main.metot2() */
        /* L12 */
        3 return;
    }

    private static void metot2() {
        /* L15 */
        0 iconst_1;
        1 istore_0;               /* x */
        /* L16 */
        2 iconst_1;
        3 invokestatic 24;        /* void com.kodkata.kata.jvm.Main.metot3(int arg0) */
        /* L17 */
        6 return;
    }

    private static void metot3(int x) {
        /* L20 */
        0 bipush 6;
        2 istore_1;               /* y */
        /* L21 */
        3 iload_0;                /* x */
        4 bipush 6;
        6 iadd;
        7 istore_2;               /* i */
        /* L22 */
        8 getstatic 30;           /* java.lang.System.out */
        11 iload_2;               /* i */
        12 invokevirtual 36;      /* void println(int i) */
        /* L23 */
        15 return;
    }
}

metot3() bünyesinde yer alan sıfırıncı satırda bipush 6 komutu yer almaktadır. Bu komut operand stack e 6 değerini ekler (push). Frame içinde yer alan operand stack ve lokal değişkenlerin yapısı bu işlemden sonra aşağıdaki şekildedir:

İkinci satırda istore_1 komutu yer almaktadır. Bu komut lokal değişken tablosunda 1 numaralı index değerine sahip y değişkenine operand stack bünyesinde yer alan 6 rakamını pop işlemini gerçekleştirerek, atar. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

Üçüncü satırda yer alan iload_0 komutu ile 0 index numarasına sahip değişkenin, bu x isimli metot parametresidir, değeri push işlemi ile operand stack e eklenir. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

Operand stack bünyesinde 1 degeri yer almaktadir, çünkü bu deger metot2() bünyesinde olusturulup, metot3() metoduna parametre olarak verilmiştir.

Dördüncü satırda bipush 6 komutu ile y isimli değişkenin değeri push işlemi ile operand stack e eklenmektedir. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

Altıncı satırda yer alan iadd komutu operand stack bünyesinde yer alan iki değeri pop komutu ile alarak, toplama işlemini gerçekleştirdikten sonra işlem sonucunu push komutu ile tekrar operand stack e eklemektedir. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

Yedinci satırda yer alan istore_2 ile operand stack in en tepesinde yer alan değer 2 index numarasına sahip değişkene, yani i değişkenine atanmaktadır. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

Sekizinci satırda yer alan getstatic ile runtime constant pool bünyesinde yer alan 30 numaralı değişmezin (constant) değeri operand stack e eklenmektedir. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

On birinci satırda yer alan iload_2 komutu ile i isimli değişkenin sahip olduğu değer operand stack e eklenmektedir. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

On ikinci satırda yer alan invokevirtual runtime constant pool icinde yer alan 36 numaralı değişmeze işaret etmektir. Bu java.io.PrintStream sınıfında yer alan println metodudur. invokevirtual komutu ard, arda iki sefer pop işlemini gerçekleştirerek, operand stack üzerinde yer alan değerleri alır ve System.out.println(7) ile istenilen işlemi gerçekleştirir. Bu işlemin ardından frame aşağıdaki yapıya sahiptir.

Görüldüğü gibi JVM bir stack makinesidir, çünkü bytecode komutları ihtiyaç duydukları değerleri operand stack aracılığı ile elde etmektedirler. Bu komutların sade bir yapıda olmalarını mümkün kılmaktadır.

On beşinci satırda return komutunu görmekteyiz. Bu komut ile metot son bulmakta ve oluşturulan frame yok edilmektedir. Eğer metot herhangi bir değer geri veriyorsa, bu değer yine metodu koşturan diğer metodun operand stack ine eklenmektedir. Bu şekilde koşturucu metot koşturduğu metot bünyesinde oluşan değere kendi operand stack i aracılığı ile erişebilmektedir.

Her thread için oluşturulan stack belli bir hafıza alanını işgal etmektedir. Bu hafıza alanının büyüklüğü mikro işlemci mimarisine ve bus genişliğine göre 320K ile 1024K arasında değişmektedir.

Stack büyüklüğü şu şekilde ayarlanabilmektedir:

java -server -Xss64k

Stack için hafıza alanı yetmediği durumlarda StackOverflowException hatası oluşmaktadır. Bu tür hatalar genelde rekursiv çalışan ve son bulmayan metotlarda meydana gelmektedir. Eğer stack için bir hafıza limiti konulmamışsa, mevcut hafıza tüketilene kadar frame yapıları oluşturulabilir. Hafıza kalmaması durumunda OutOfMemoryError hatası oluşur.

Java dili call by value mantığı ile çalışmaktadır. Herhangi bir metot koşturulmadan önce, metoda gönderilen tüm parametreler koşturulacak yeni metodun stack ine kopyalanır. Bu işlem hem basit veri tipleri (int, long..) hem de referans veril tipleri için geçerlidir. Çoğu zaman Java dili için bir metoda parametre olarak gönderilen bir nesne referansının kopyalanmadığı, call by reference mantığı ile metoda aktarıldığı düşünülmektedir. Java’da referanslar bile metot girişlerinde kopyalanmaktadır.

Java’da referansların bile call by value mantığı ile işlediğini aşağıdaki örnekte görmekteyiz.

private static void metot2() {
	final Main m1 = new Main();
	System.out.println(m1);
	metot3(m1);
	System.out.println(m1);
}

private static void metot3(Main m) {
	m = new Main();
	System.out.println(m);
}

Bu metotları koşturduğumuzda, ekran çıktısı aşağıdaki şekilde olacaktır:

com.kodkata.kata.jvm.Main@7150bd4d
com.kodkata.kata.jvm.Main@6bbc4459
com.kodkata.kata.jvm.Main@7150bd4d

Eğer Java call by reference mantığı ile çalışmış olsaydı, ekran çıktısının şu şekilde olması gerekirdi, çünkü metot3() bünyesinde metot parametresi olan m isimli referansa yeni bir nesne atadık:

com.kodkata.kata.jvm.Main@7150bd4d
com.kodkata.kata.jvm.Main@6bbc4459
com.kodkata.kata.jvm.Main@6bbc4459

Lakin metot2() bünyesinde oluşturulan m1 isimli referans metot3() stack ine kopyalandığı için, metot3() bünyesinde bu referansa yaptığımız yeni bir atama metot2() bünyesindeki m1 referansını etkilememektedir.

Java uygulamalarında oluşan hataları java.lang.Throwable ya da türevleri temsil eder. Bu sınıf bünyesinde yer alan printStackTrace() metodu aracılığı ile hata oluşan yere kadar gelinen stack yapısına ulaşmak mümkündür. Bunun bir örneğini aşağıda görmekteyiz.

java.lang.ArrayIndexOutOfBoundsException: 3
  at com.kodkata.jvm.TestTry.execute(Testtry.java:17)
  at com.kodkata.jvm.TestTry.main(Testtry.java:11)

Bu stacktrace örneğinde iki stack frame yer almaktadır. İlk frame TEstTry sınıfının main() metodu için, ikinci frame bu metot bünyesinde execute() metodu koşturulduğu için oluşturulmuştur. Stacktrace bünyesinde oluşturulan her iki frame yer almaktadır. execute() metodunun on yedinci satırında ArrayIndexOutOfBoundsException hatasının oluştuğunu görmekteyiz.


EOF (End Of Fun)
Özcan Acar

JVM Nasıl Çalışır Yazı Serisi – JVM Stack Nedir ve Nasıl Çalışır?” hakkında 3 yorum

  1. misafir

    Java’yı derinlemesine öğrenmek isteyenler için güzel yazılardan biri daha. Elinize sağlık.

  2. Mustafa

    Özcan bey selamlar,

    java -server -Xss64k stack buyuklugu için yazmışsınız. burdaki -server option’da illa kullanılması gerekiyor mu?

    Oracle Hotspot dokumanlarından baktığım kadarıyla -server / -client stack ayarlamasıyla çok ilişkisi yok gibi.

    siz var demişssiniz diye sormuyorum.

    yazdığınız şeyler çok bilgi içeriyor. daha iyi ogrenmek için soruyorum.

    Ayrıca yazı içinde elinize sağlık , çok sade şekilde konuyu izah etmişsiniz.

    tşk.

  3. Özcan Acar Yazar

    -server kullanmak zorunda degilsiniz. -Xss parametresi ile jvm stack genisligini ayarlayabiliyorsunuz. Kullanmadiginiz taktirde default ayarlar ile calisiyor JVM. Yazimda da belirttigim gibi kullanilan isletim sistemine göre 320K ile 1024K arasinda degisiyor stack büyüklügü.

Yorumlar kapalı.