JVM Nasıl Çalışır Yazı Serisi – Java Hotspot, Assembly Kod, Hafıza Bariyerleri ve Volatile Analizi

Java kodu Java derleyicisi javac (compiler) tarafından bytekoduna dönüştürülür. Bytekod JVM e has bir yapıya sahiptir ve Assembly dili gibi düşünülebilir. Oluşan bytecode dogrudan işlemci üzerinde koşturulamaz. Derlenmiş Java kodunu koşturabilmek için mikroişlemci ile Java bytekodu arasında, bytekodunu mikroişlemci makine koduna dönüştürebilecek bir ara katmana daha ihtiyaç duyulmaktadır. Bu JVM (Java Virtual Machine) ismini taşıyan sanal makinedir. Sanal makine Java bytekodunu mikroişlemi gibi koşturur. Java bytekodu için mikroişlemci sanal makinedır. JVM bünyesinde Java bytekodu yorumlanarak, mikroişlemci üzerinde koşturulur. Bu yazımda Assembly makine koduna dönüştürülmüş Java kodundan örnek sunmak istiyorum.

JVM bünyesinde bytekodu doğrudan makine koduna çeviren birim Hotspot JIT (Just In Time) derleyicisidir. JIT olmadan JVM sadece Java bytekod yorumlayıcısıdır (interpreter). Hotspot çok sık kullanılan kod bölümlerini, mikroişlemci üzerinde daha hızlı koşturulabilmeleri için doğrudan Assembly koduna dönüştürür. Bu işlemi yapabilmesi için belli bir süre kodu analiz etmesi (profiling) gerekmektedir. Bu sebepten dolayı Java uygulamaları belli bir ısınma aşamasından sonra daha hızlı çalışmaya başlarlar, çünkü byte kodun belli bir kısmı ya da hepsi JIT tarafından Assembly oradan da makine koduna dönüştürülmüştür.

JIT tarafından oluşturulan Assembly kodunun çıktısını alabilmek için bir Hotspot Disassembler plugin kullanmamız gerekmektedir. Kenai base-hsdis projesi bünyesinde böyle bir plugin yer almaktaktadır. Ben bu yazımdaki örnekler için Linux altında 64 bit JDK7 (jdk1.7.0_45) kullandım. Kullandığım disassembler plugin linux-hsdis-amd64.so ismini taşıyor. Bu dosyanın libhsdis-amd64.so ismini taşıyacak şekilde jdk1.7.0_45/jre/lib/amd64/server dizinine kopyalanması gerekiyor. Bu işlem ardından çalışan bir Java uygulamasının makine kodu çıktısını alabiliriz.

Önce koşturmak istediğimiz Java koduna bir göz atalım. Main sınıfı bünyesinde volatile olan volatileCounter ve counter değişkenleri yer almaktadır. count() metodu bünyesinde bir for döngüsünde bu değişkenlerin değerleri artırılmaktadır. For döngüsü volatileCounter değişkeni 100000 değerine ulaştığında son bulmaktadır. Hotspot JIT kod 100000 sefer koşturulduğundan dolayı makine koduna dönüştürmektedir. Bu uygulamanın çok sıkca kullanılan alanlarının (hotspot; sıcak alan anlamında) makine koduna dönüştürülerek, daha da hızlı koşturulabilmeleri için gerekli bir işlemdir. Hotspot sadece sıkca koşturulan kodları makine koduna dönüştürür. Sıkça kullanılmayan kod bloklarının JVM tarafından yorumlanma hızı yeterlidir. Bu tür kodlar için makine kodunun oluşturulması çok maliyetli bir işlemdir. Elde edilecek kazanç çok az olacağından, sıkça kullanılmayan kod blokları makine koduna dönüştürülmez.

Eğer count() metodunda bir for döngüsü kullanılmasaydı, JIT tarafından daha sonra göreceğimiz Assembly kodu oluşturulmazdı. Bunu sağlayan döngünün 100000 adet olmasıdır. JIT hangi kod bloğunun ne kadar koşturulduğunu takip ettiği için hangi kod birimi için makine kodu oluşturacağına karar verebilmektedir.

public class Main {

	private volatile int volatileCounter;
	private int counter;

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

		new Main().count();
	}

	private void count() {

		for (; this.volatileCounter < 100000;) {
			this.volatileCounter++;

			synchronized (this) {
				this.counter++;
			}
		}
	}
}
&#91;/source&#93;

Java derleyicisi (javac) tarafından oluşturulan byte kodunu aşağıdaki resimde görmekteyiz.

<a href="http://www.kurumsaljava.com/wp-content/uploads/2013/11/bytecode2.jpg"><img src="http://www.kurumsaljava.com/wp-content/uploads/2013/11/bytecode2.jpg" alt="" title="bytecode2" width="628" height="756" class="aligncenter size-full wp-image-3400" /></a>

Şimdi gelelim Hotspot JIT tarafından oluşturulan Assembly koduna. Assembly kodunu görebilmek için JVM'i aşağıdaki şekilde çalıştırmamız gerekiyor:

java -cp . -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Main

Aşağıda oluşan Assembly kodu yer almaktadır:


1   0x00007f206906015c: jne    0x00007f2069060202  ;*monitorenter
                                                ; - Main::count@16 (line 16)
2   0x00007f2069060162: incl   0x10(%r13)
3   0x00007f2069060166: mov    $0x7,%r10d
4   0x00007f206906016c: and    0x0(%r13),%r10
5   0x00007f2069060170: cmp    $0x5,%r10
6   0x00007f2069060174: jne    0x00007f2069060226  ;*monitorexit
                                                ; - Main::count@28 (line 16)
7   0x00007f206906017a: mov    0xc(%r13),%r11d    ; OopMap{rbp=NarrowOop r13=Oop r14=Oop off=222}
                                                ;*if_icmplt
                                                ; - Main::count@41 (line 13)
8   0x00007f206906017e: test   %eax,0xa91ee7c(%rip)        # 0x00007f207397f000
                                                ;   {poll}
9   0x00007f2069060184: cmp    $0x186a0,%r11d
10  0x00007f206906018b: jge    0x00007f20690602ef  ;*aload_0
                                                ; - Main::count@3 (line 14)
11  0x00007f2069060191: mov    0xc(%r13),%r11d
12  0x00007f2069060195: inc    %r11d
13  0x00007f2069060198: mov    %r11d,0xc(%r13)
14  0x00007f206906019c: lock addl $0x0,(%rsp)     ;*putfield volatileCounter
                                                ; - Main::count@10 (line 14)
15  0x00007f20690601a1: mov    0x0(%r13),%rax
16  0x00007f20690601a5: mov    %rax,%r10
17  0x00007f20690601a8: and    $0x7,%r10
18  0x00007f20690601ac: cmp    $0x5,%r10
19  0x00007f20690601b0: jne    0x00007f2069060106
20  0x00007f20690601b6: mov    0xb0(%r12,%rbp,8),%r10
21  0x00007f20690601be: mov    %r10,%r11
22  0x00007f20690601c1: or     %r15,%r11
23  0x00007f20690601c4: mov    %r11,%r8
24  0x00007f20690601c7: xor    %rax,%r8
25  0x00007f20690601ca: test   $0xffffffffffffff87,%r8
26  0x00007f20690601d1: je     0x00007f2069060162
27  0x00007f20690601d3: test   $0x7,%r8
28  0x00007f20690601da: jne    0x00007f2069060100
29  0x00007f20690601e0: test   $0x300,%r8
30  0x00007f20690601e7: jne    0x00007f20690601f6
31  0x00007f20690601e9: and    $0x37f,%rax
32  0x00007f20690601f0: mov    %rax,%r11
33  0x00007f20690601f3: or     %r15,%r11
34  0x00007f20690601f6: lock cmpxchg %r11,0x0(%r13)
35  0x00007f20690601fc: je     0x00007f2069060162
36  0x00007f2069060202: mov    %r14,0x8(%rsp)
37  0x00007f2069060207: mov    %r13,(%rsp)
38  0x00007f206906020b: mov    %r14,%rsi
39  0x00007f206906020e: lea    0x10(%rsp),%rdx
40  0x00007f2069060213: callq  0x00007f206905e320  ; OopMap{rbp=NarrowOop [0]=Oop [8]=Oop off=376}
                                                ;*monitorenter
                                                ; - Main::count@16 (line 16)
                                                ;   {runtime_call}
                                                

Java kodu sequentially consistent değildir, yani Java byte kodu sahip olduğu sıraya göre satır satır koşturulmaz. Hem Java derleyicisi (JIT) hem de mikroişlemci performansı artırmak için birbirine bağımlı olmayan kod satırlarının yerlerini değiştirerek işlem yapabilirler. Bu aslında oluşan Java kodunun programcının yazdığı şekilde koşturulmadığı anlamına gelmektedir. Paralel çalışan programlarda bunun çok ilginç bir yan etkisi mevcut: bir threadin bir değişken üzerinde yaptığı değişikliği başka bir çekirdek üzerinde koşan başka bir thread göremeyebilir. Örneğin t1 (thread 1) a isimli değişkenin değerini bir artırdı ise ve t2 a belli bir değere sahip iken bir for döngüsünü terk etmek istiyorsa, t2 belki bu for döngüsünden hiçbir zaman çıkamayabilir, çünkü a üzerinde t1 tarafından yapılan değişiklikleri göremeyebilir. Bunun sebebi t1 in üzerinde çalıştığı çekirdeğin (core) a üzerinde yaptığı değişiklikleri kendi ön belleginde (L1/L2 cache) tutmasıdır. Mikroişlemciler yüksek performansta çalışabilmek için hazıfa alanları üzerinde yaptıkları değişiklikleri ilk etapta kendi ön belleklerinde tutarlar. Gerek duymadıkça da bu değişiklikleri diğer çekirdeklerle paylaşmazlar. Her çekirdek sahip olduğu önbelliği tüm hafiza alanıymış (RAM) gibi gördüğü için kendi performansını artırmak adına kod sırasını değiştirebilir. Bu belli bir sıraya bağımlı olan diğer threadlerin düşünüldükleri şekilde çalışmalarını engelleyebilir. Bu genelde paralel programlarda program hatası olarak dışarıya yansır.

Bu tür sorunları aşmanın bir yolu volatile tipinde değişkenler kullanmaktır. Volatile tipinde olan değişkenler üzerinde işlem yapıldığında mikroişlemci kodu programcının yazdığı sırada koşturmaya zorlanır. Bunu gerçekleştirmek için hafıza bariyerleri (memory barrier) kullanılır. Mikroişlemci bir hazıfa bariyeri ile karşılaştığında bir çekirdeğin sahip olduğu önbellekteki değişiklikleri doğrudan hafızaya geri yazar (write back) ve bu hazıfa alanını (cache line) kendi önbelleklerinde tutan diğer çekirdeklere mesaj göndererek bu hafıza alanını silmelerini (cache invalidate) talep eder. Böylece herhangi bir çekirdek üzerinde koşan bir threadin yaptığı değişiklik hemen hafızaya, oradan da diğer çekirdeklerin önbelleklerine yansır. Bu t1 tarafından a üzerinde yapılan bir değişikliğin t2 tarafından anında görülmesi anlamına gelmektedir. Bunun gerçekleşmesi için a isimli değişkenin volatile olması gerekmektedir.

Main sınıfında yer alan this.volatileCounter++; satırı ile bahsettiğim hafıza bariyerinin kullanımı gerekmektedir. JVM bunu sağlamak için Assemby kodunun 14. satırında mikroişlemci için lock addl komutunu kullanmaktadır. 13. satırda yer alan mov komutuyla %r11d registerinde yer alan volatileCounter isimli değişkenin değeri doğrudan hafızaya (RAM) aktarılır. Hafiza alanının adresi %r13 registerinde yer almaktadır. 14. satırda yer alan lock addl ile tüm mikroişlemci bünyesinde global bir hafıza transaksiyonu gerçekleştirilir. Atomik olan bu işlem ile tüm çekirdeklerin üzerinde değişiklik yapılan hafiza alanını önbelleklerinden silmeleri ve yeniden yüklemelerini sağlanır. Böylece diğer threadler yapılan değişiklikleri anında görmüş olurlar.

Java kodunu sequentially consistent yapmanın diğer bir yolu synchronized kelimesinin kullanımıdır. Synchronized kullanılması durumunda mikroişlemci değişikliğe uğrayan değeri önbellekten alıp hafızaya geri aktararak, diğer çekirdeklerin kendi önbelleklerini tazelemelerini sağlar.

Main sınıfında yer alan counter isimli değişken volatile olmadığı için üzerinde yapılan değişiklikler diğer çekirdeklere yansımaz. Bunu sağlamak için count() metodunda değer atamasını synchronized bloğunda yaptım. Bu JVM tarafından tekrar bir hafıza bariyeri kullanımı gerektiren bir işlemdir. JIT makina kodunun 34. satırında lock cmpxchg ile gerekli hafıza bariyerini oluşturmaktadır. Bu üzerinde işlem yapılan çekirdeğin counter isimli değişkenin değerini tekrar hafızaya geri aktarmasını ve diğer çekirdeklerin bu değeri tekrar hafızadan kendi önbelleklerine çekmelerini sağlamaktadır.


EOF (End Of Fun)
Özcan Acar

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

4 Comments

  • Enes

    09 Kasım 2013

    Merhabalar, güzel bir yazı olmuş elinize sağlık. ilk cümlede geçen
    “…Bu makina kodu Assembler değildir…” kısmında sanırım bir kavram karmaşası var. Assembler zaten makine kodu değildir.Assembler direk mikroişlemci üzerinde koşamaz. makine koduna dönüştürülmesi gerekir. Bildiğiniz üzere makine kodunda programlamanın zorluğunu tatmış öncül programcıların geliştirdiği bir üst seviye dildir. ayrıca bytecode da bir makine kodu değildir.
    yanlışım varsa düzeltin
    sağlıcakla.

  • Özcan Acar

    11 Kasım 2013

    Daha cok kavram karmasasi olmasin diye Assembler kodunu makina seviyesinde kosturulan kod olarak baz aldim. Dediginiz gibi Assembler tekrar derleyicisi tarafindan hex ve mikro koda dönüstürülüyor. Ama genel olarak Assembler yazilimcilar arasinda makina kodu olarak görülen en alt seviye.

    >bytecode da bir makine kodu değildir.

    Yazimda byte kodun makina kodu oldugu yazmiyor.

  • Muharrem Taç

    01 Ocak 2014

    Hocam örnekler Linux’da demişsiniz ama ekran görüntüsü Windows’a benziyor. Windows’da bu plugin var mı?

  • Özcan Acar

    01 Ocak 2014

    hdis’in windows sürümü yok hocam. Resimde gördügüm Windows isletim sistemimde kullandigim bytecode bakicisi (http://set.ee/jbe/). Gerisi linux isletim sisteminde edindigim veriler.

Bir Cevap Yazın