Thursday, April 30, 2015

Java Sanal Makinası ve HotSpot

Java platformunun en güçlü bileşeni yazdığımız Java uygulamalarını çalıştıran Java Sanal Makinasıdır. Hatta son dönemde ortaya çıkan programlama dillerinin çoğunun Java Sanal Makinası (JSM) üzerinde çalışması rastlantısal olamaz! JSM etrafında kümelenmiş bu dilleri ortak olarak "JSM dilleri" olarak adlandırıyoruz. JSM kapalı bir kutu gibi görünse de bu kutuyu açıp içini kurcalamak, davranışını izlemek, başarımını iyileştirmek mümkündür. JSM gerçek bir işlemci tanımlar. Bu işlemcinin komut kümesi, adresleme kipi, saklayıcıları, aritmetik ve lojik işlem birimi ve bellek modeli bulunur (Şekil-1).  Bu işlemcinin standart tanımına bu adresten ulaşabilirsiniz. JSM fiziksel olarak üretilebilecek bir işlemci tanımlar. Ancak biz uzun süredir JSM'yi yazılımsal olarak ediniyoruz. 
Şekil-1 JSM'nin iç yapısı
Farklı işletim sistemlerinde farklı JSM'lerle karşılaşabilirsiniz. Oracle'ın HotSpot'u en çok kullanılan JSM'dir ve hemen hemen her işletim sitemi için HotSpot bulabilirsiniz. Oracle'ın sahip olduğu ama artık geliştirmesini durdurduğu bir JSM daha var: JRockit. Oracle'ın Sun firmasını satın almasından sonra HotSpot ile ilerlemeye karar verdi ve JRockit'in geliştirmesini durdurdu. Onun iyi özelliklerini HotSpot'a aktardığını ve HotSpot'u ticarileştirdiğini görüyoruz. JDK 7u40 sürümü ile birlikte gelen jmc (Mision Control) ve Flight Recorder yeteneğini JRockit'dan almıştır. jmc'nin kişisel kullanımı ücretsizdir, ancak ticari kullanımı için "Oracle ile gelirinizi paylaşmanız" gerekir. Azul Systems'ın biri OpenJDK tabanlı olan Zulu adında destek alabileceğiniz bir JSM'si ve diğeri Zing isimli, büyük Heap alanlarıyla, donma olmadan çalışmayı vaat eden, C4 (Continuously Concurrent Compacting Collector) adında bir çöp toplayıcısı barındıran bir JSM'si daha bulunuyor. IBM'in kendi AIX ve Linux tabanlı sistemleri için geliştirdiği J9 adında bir JSM'si bulunuyor.
Java uygulamalarını çalıştırdığınız ortamlarda, farklı mimariler ile karşılaşabilirsiniz. Java SE platformunda uygulama çalıştıracak iseniz tipik yapı Şekil-2'de verilmiştir.
 
Şekil-2 Java SE Uygulamalarının çalıştığı tipik mimari
Burada en altta tüm işi asıl yapan, fiziksel olarak dokunabileceğimiz donanım yer alır. Onun üzerinde donanım kaynaklarını yönetmekle sorumlu olan işletim sistemi çekirdeği yer alır. JSM ise işletim sistemi üzerinde oturur. Java uygulamalarının platform bağımsız olmasını sağlayan JSM'dir. JSM geliştirdiğimiz uygulamanın byte kodlarını alıp, işlemcinin ve  işletim sisteminin anlayacağı makine komutlarına ve sistem çağrılarına dönüştürür. Kurumsal uygulama geliştirmek için ise Java EE platformunu kullanıyoruz. Java EE platformunda uygulama geliştirmek için ise Java EE uyumlu bir uygulama sunucusuna ihtiyaç bulunmaktadır. Uygulama sunucusunun kendisi de bir Java uygulamasıdır ve bu nedenle JSM üzerinde çalışır. Bu durumda Java EE uygulamaları Şekil-3'deki gibi bir mimari üzerinde koşarlar. Java EE uygulama sunucularının bir kısmı açık kaynak kodlu (TomEE, GF, JBoss AS, Wildfly), bir kısmı ücretli (JBoss EAP, Weblogic, WebSphere) ve bir kısmı da kapalı kodludur (Weblogic, WebSphere). 
Şekil-3 Java EE Uygulamalarının çalıştığı tipik mimari
Son dönemde güçlü donanımlar ile karşılaşıyoruz. Bu güçlü donanımlar üzerinde, sistem kaynaklarının, uygulamalar arasında daha iyi paylaştırılabilmesi için çeşitli çözümler geliştirildiğini görüyoruz:
Zone ve Control Groups çözümleri Kap (=Container) teknolojisini kullanırlar. Ev sahibi işletim sisteminden, biri birinden yalıtılmış klonlar yaratılır. Bu yaratılan klonlara ihtiyaca göre kaynak atanarak, kaynak kullanımı planlanır. Sanallaştırma çözümünde ise donanım üzerinde ince bir sanallaştırma katmanı oturur. Bu katman üzerinde sanal makinalar tanımlanır. Bu sanal makinalar üzerine ihtiyaca göre farklı işletim sistemleri kurulur. JSM bu işletim sistemleri üzerinde çalışır (Şekil-4). 
Şekil-4 Sanallaştırma çözümü ve JSM
JSM'nin İşletim Sistemine ihtiyaç duymayan, doğrudan Sanal Makina üzerinde oturan türleri de mevcuttur (Şekil-5): JRockit VE (Virtual Edition) ve WebSphere HE (Hypervisor Edition).
Şekil-5 Doğrudan sanal makina üzerinde çalışan JSM
HotSpot
Bu bölümde HotSpot marka Java Sanal Makinasını inceleyeceğiz. HotSpot ona sorulduğunda kendisini tanıtır:
c:\opt32\java\jdk1.7.0_71\bin>java -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) Client VM (build 24.71-b01, mixed mode, sharing)
Burada JSM'nin HotSpot marka olduğunu, dağıtım sürümünün 24.71-b01, JDK sürümünün ise 1.7u71 olduğunu anlıyoruz. Ancak burada açıklamam gereken Client VM, mixed mode ve sharing tanımlamaları var. JSM hem 32-bit hem de 64-bit olabilir. Eğer bize açıkça 64-bit olduğunu söylemiyor ise 32-bittir. 64-bitlik bir HotSpot bize kendini aşağıdaki gibi tanıtacaktır:
c:\opt64\java\jdk1.7.0_71\bin>java -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)
Evet, HotSpot kendini 64-bit olarak tanıttı. Bir değişiklik daha dikkatimizi çekiyor: Client VM yazan yerde şimdi Server VM yazıyor! Henüz ne olduklarını bilmiyoruz ama en azından varsayılan olarak, 32-bitlik HotSpot, Client VM özelliğinde ve 64-bitlik HotSpot ise Server VM özelliğinde olduğunu biliyoruz. Önce "mixed mode" özelliğini açıklayalım. HotSpot, uygulamaları başlangıçta yorumlamalı olarak çalıştırır. Basit bir şekilde açıklamak gerekirse, HotSpot çalıştırılacak metodun byte kodlarının x86 karşılıklarını, byte kod-x86 çizelgesinden öğrenir ve x86 komutlarını işlemciye çalıştırması için gönderir. Bu çalışma şekli yavaştır. "mixed mode" çalışma şeklinde uygulama başlangıçta bu yüzden yavaş çalışır. Bir süre sonra HotSpot uygulamanın sıkça çalışan ve ısınan metotlarını JIT (=Just-in-time)  derleyicisine gönderir. JIT'lenen metot bundan sonra doğal işlemci komutları olarak çalışacağından, daha hızlı çalışır. Bir süre sonra bakıldığında, çalışan metotların bir kısmı yorumlamalı bir kısmı ise işlemcinin doğal komutlarında çalışır. Java uygulamamız başlangıçta yavaş çalışırken, yavaş yavaş hızlanmaya başlayacaktır. JIT derleyici sadece ana işlemci komutlarına dönüştürme yapmaz, aynı zamanda dinamik en iyileme yöntemlerini uygular. Hızlanmanın asıl kaynağı da buradan gelir. C/C++ ve Java'da yazılmış çözümlerin karşılaştırılmasında, Java'nın bazen daha üstün başarım sergilemesinin nedeni, JIT derleyicisinin uyguladığı bu iyileştirmelerdir. Şekil-6'da JIT derleyicinin basit bir şeması verilmiştir. JIT derleyici içinde basit bir kesit çıkarıcı (=Profiler) vardır ve yazılımın şeklini belirlemeye çalışır. En iyileme teknikleri, yazılımın çalışma zamanında aldığı şekle göre uygulanır. Her en iyileme tekniği her durumda geçerli değildir. JIT derleyici önce JIT'lenecek metodun ya da döngü bloğunun byte kodlarını ara bir gösterime dönüştürür. En iyileyici, yazılımın kesit bilgisinden yararlanarak, yazılımın şekline uygun en iyileme tekniklerini uygular. En iyileme, her durumda ara gösterime uygulanır. Kod üreteci bu ara gösterimden x86 kodu üretir. Kesit çıkarıcı koşturulan kodun davranışından, kesit bilgisini günceller.
Şekil-6 JIT Derleyici
HotSpot içinde iki farklı karakteristikte JIT derleyici bulunur: C1 ve C2. Client VM, C1 derleyicisini ve Server VM ise C2 derleyicisini kullanır. Java uygulamalarının başarımını ölçmek için farklı metrikler kullanarak ölçümler yapabiliriz. Açılış zamanı ve cevap süresi başarımı ölçmek için kullanılabilecek iki ölçüttür. Masaüstü uygulamaları için açılış süresi ve web uygulamaları için ise cevap süresi daha önemlidir. Masaüstü uygulamaları kullanıcı arayüzünü bir an önce kullanıcıya sunmalıdır. Örneğin, e-posta göndermek istiyorsunuz, bunun için favori e-posta istemcinizin  (örneğin, Thunderbird) masaüstü simgesine tıkladınız. Pencerenin beş dakika sonra açılması sizi hiç memnun etmez. Pencerenin hemen açılmasını ve iletinizi bir an önce yazabilmeyi istersiniz. Nasıl olsa yazmaya başladığınızda, işlemci ile karşılaştırıldığında kaplumbağa hızında yazıyor olacaksınız. Yazılımın çok da hızlı çalışmaya ihtiyacı yok. Buna karşılık web uygulamalarında ise uygulama sunucusunun biraz geç açılmasına tahammül edebilirsiniz ama bir istek geldiğinde ona en hızlı sürede yanıt vermek ve cevabı dönebilmek istersiniz. İşte C1 derleyicisi açılış süresini iyileştirmek ve C2 derleyicisi ise hızlı yanıt süresini iyileştirmek üzere en iyilenmiştir. 32-bitlik HotSpot hem Client VM hem de Server VM olarak çalışabilirken, 64-bitlik HotSpot sadece Server VM olarak çalışır. -client ve -server seçenekleri ile tercihimizi HotSpot'a bildirebiliyoruz:

c:\opt64\java\jdk1.7.0_71\bin>java -client -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)

c:\opt64\java\jdk1.7.0_71\bin>java -server -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)

c:\opt32\java\jdk1.7.0_71\bin>java -client -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) Client VM (build 24.71-b01, mixed mode, sharing)

c:\opt32\java\jdk1.7.0_71\bin>java -server -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) Server VM (build 24.71-b01, mixed mode)

C1 en iyileme için acele eder. Amacı açılış süresini iyileştirmek olduğu için çok fazla en iyileme yapmaz. C2 ise JIT'lemek için acele etmez, önce uygulamanın şeklini alması bekler ve daha çok en iyileme tekniklerini uygular. Bu nedenle C1'in JIT'leme zaman maliyeti düşüktür ama buna paralel olarak, üretilen kodun kalitesi de düşüktür. C2'nin JIT'leme zaman maliyeti yüksektir ancak üretilen kodun kalitesi yüksektir. C2'nin ürettiği kod C1'in ürettiği koddan yaklaşık iki kat daha hızlı çalışır.
Java 7'de her iki JIT derleyicinin iyi özelliklerini birleştiren yeni bir JIT derleyicisi var: Katmanlı JIT derleyici (=Tiered Compiler). Hem açılış zamanını hem de cevap süresini iyileştirmeye çalışıyor. Şekil-7'de katmanlı JIT derleyicinin basit akış şemasını bulabilirsiniz.
Şekil-7 Katmanlı JIT Derleyici
Ancak Java 7'de bu özellik kapalıdır. Açmak için JSM'yi -XX:+TieredCompilation seçeneği ile başlatmanız gerekir. Java 8'de ise varsayılan davranış budur, bu nedenle herhangi bir tanımlama yapmanıza gerek yoktur. 
JIT derleyicinin davranışını izleyebilirsiniz. Hatta üretilen x86 kodunu da izlemek mümkündür. Bunun için HotSpot'u aşağıdaki gibi başlatmalısınız:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintAssembly
Ayrıca Windows platformu için hsdis-amd64.dll dosyasını java.exe ile aynı dizine koymanız gerekir. Bu dll dosya, x86 komutlarının sembolik dilde göstermesini sağlar. Ekran çıktısına ilişkin bir örneği aşağıda bulabilirsiniz:
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
    142    1             java.lang.String::hashCode (55 bytes)
Loaded disassembler from hsdis-amd64.dll
Decoding compiled method 0x00000000024ebd10:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Constants]
  # {method} 'hashCode' '()I' in 'java/lang/String'
  #           [sp+0x30]  (sp of caller)
  0x00000000024ebe40: mov    0x8(%rdx),%r10d
  0x00000000024ebe44: cmp    %r10,%rax
  0x00000000024ebe47: jne    0x00000000024b7a60  ;   {runtime_call}
  0x00000000024ebe4d: data32 xchg %ax,%ax
[Verified Entry Point]
  0x00000000024ebe50: mov    %eax,-0x6000(%rsp)
  0x00000000024ebe57: push   %rbp
  0x00000000024ebe58: sub    $0x20,%rsp         ;*synchronization entry
                                                ; - java.lang.String::hashCode@-1 (line 1446)
  0x00000000024ebe5c: mov    %rdx,%r13
  0x00000000024ebe5f: mov    0x10(%rdx),%eax    ;*getfield hash
                                                ; - java.lang.String::hashCode@1 (line 1446)
  0x00000000024ebe62: test   %eax,%eax
  0x00000000024ebe64: jne    0x00000000024ebf44  ;*ifne
                                                ; - java.lang.String::hashCode@6 (line 1447)
  0x00000000024ebe6a: mov    0xc(%rdx),%esi     ;*getfield value
                                                ; - java.lang.String::hashCode@10 (line 1447)
  0x00000000024ebe6d: mov    0xc(%rsi),%r10d    ;*arraylength
                                                ; - java.lang.String::hashCode@13 (line 1447)
                                                ; implicit exception: dispatches to 0x00000000024ebf65
  0x00000000024ebe71: xor    %edi,%edi
  0x00000000024ebe73: test   %r10d,%r10d
  0x00000000024ebe76: jle    0x00000000024ebf50  ;*ifle
                                                ; - java.lang.String::hashCode@14 (line 1447)
  0x00000000024ebe7c: test   %r10d,%r10d
  0x00000000024ebe7f: jbe    0x00000000024ebf54
  0x00000000024ebe85: mov    %r10d,%r8d
  0x00000000024ebe88: dec    %r8d
  0x00000000024ebe8b: cmp    %r10d,%r8d
  0x00000000024ebe8e: jae    0x00000000024ebf54
  0x00000000024ebe94: xor    %ebp,%ebp          ;*imul
                                                ; - java.lang.String::hashCode@36 (line 1451)
  0x00000000024ebe96: movzwl 0x10(%rsi,%rdi,2),%r9d
  0x00000000024ebe9c: add    %r9d,%ebp          ;*iadd
                                                ; - java.lang.String::hashCode@40 (line 1451)
  0x00000000024ebe9f: mov    %ebp,%ecx
  0x00000000024ebea1: shl    $0x5,%ecx
  0x00000000024ebea4: mov    %ecx,%eax
  0x00000000024ebea6: sub    %ebp,%eax          ;*imul
                                                ; - java.lang.String::hashCode@36 (line 1451)
  0x00000000024ebea8: inc    %edi               ;*iinc
                                                ; - java.lang.String::hashCode@42 (line 1450)
  0x00000000024ebeaa: cmp    $0x1,%edi
  0x00000000024ebead: jge    0x00000000024ebeb3  ;*if_icmpge
                                                ; - java.lang.String::hashCode@30 (line 1450)
JIT'lenen metod ve döngü blokları ile ilgili ekrana dökülen satırlara bir bakalım:
154    1             java.lang.String::hashCode (55 bytes)
    203    2             java.lang.String::indexOf (70 bytes)
    226    3             sun.java2d.loops.GraphicsPrimitive::getUniqueID (5 bytes)
    249    4             java.lang.Object::<init> (1 bytes)
    257    5             java.lang.String::charAt (29 bytes)
    341    6             java.lang.String::equals (81 bytes)
    343    7             java.util.Arrays::binarySearch0 (95 bytes)
    383    8 %           sun.awt.image.PNGImageDecoder::update_crc @ 3 (41 bytes)
    383    9 % !         sun.awt.image.PNGImageDecoder::produceImage @ 960 (1920 bytes)
    391   10             sun.awt.image.PNGImageDecoder::update_crc (41 bytes)
    419    9 % !         sun.awt.image.PNGImageDecoder::produceImage @ -2 (1920 bytes)   made not entrant
    420   11 % !         sun.awt.image.PNGImageDecoder::produceImage @ 960 (1920 bytes)
    420   12     n       java.lang.System::arraycopy (native)   (static)
    421   13             sun.awt.image.PNGImageDecoder::filterRow (459 bytes)
    461   14             java.util.Properties$LineReader::readLine (452 bytes)
    568   15             sun.security.provider.SHA::implCompress (491 bytes)
    575   16             java.lang.String::lastIndexOf (52 bytes)
    581   17             sun.nio.cs.SingleByte$Encoder::encode (114 bytes)
    586   18             sun.nio.cs.SingleByte$Encoder::encode (32 bytes)
    596   19             java.lang.Math::min (11 bytes)
    604   20             java.nio.HeapByteBuffer::_get (7 bytes)
    610   21             java.io.BufferedInputStream::getBufIfOpen (21 bytes)
    610   22  s          java.io.BufferedInputStream::read (49 bytes)
    611   23             java.io.FilterInputStream::read (8 bytes)
    613   24             java.io.DataInputStream::readChar (40 bytes)
    614   25 %           sun.text.normalizer.CharTrie::unserialize @ 29 (74 bytes)
    622   26             sun.text.normalizer.CharTrie::unserialize (74 bytes)
    637   27             java.nio.Buffer::nextGetIndex (31 bytes)
    651   28             java.lang.String::length (6 bytes)
    656   29             java.awt.geom.Path2D$Float::append (216 bytes)
    656   30             java.awt.geom.Path2D$Iterator::isDone (20 bytes)
    657   31             java.awt.geom.Path2D$Float::needRoom (123 bytes)
    661   32             java.awt.geom.Path2D$Float$CopyIterator::currentSegment (39 bytes)
    663   33             java.awt.geom.Path2D$Iterator::next (35 bytes)
    718   34             java.util.HashMap::indexFor (6 bytes)
    724   35             java.nio.Bits::getCharB (16 bytes)
    724   36             java.nio.Bits::makeChar (12 bytes)
    725   37             java.nio.ByteBufferAsCharBufferB::get (16 bytes)
    725   38             java.nio.ByteBufferAsCharBufferB::ix (9 bytes)
    738   39             java.lang.String::indexOf (166 bytes)
    738   40             java.lang.String::replace (127 bytes)
    755   41             java.util.HashMap::hash (55 bytes)
    769   42             java.lang.String::startsWith (72 bytes)
    772   43             java.util.HashMap::getEntry (86 bytes)
    783   44             java.util.concurrent.atomic.AtomicInteger::get (5 bytes)
Burada birinci sütun JIT'leme zamanını, ikinci sütun ise JIT'leme numarasını ifade eder. Üçüncü sütunda çeşitli semboller yer alır:

  • %: Döngü bloğu JIT'lendi. Metod içinde birden fazla döngü varsa hangi döngü olduğunu @ sembolünden sonra yazan rakamı kullanarak belirleyebilirsiniz. Bu rakam döngünün başlangıç byte kod sırasını verir. 
  • s: synchronized metod JIT'lendi
  • !: Exception bloğu JIT'lendi 
  • n: native metod

Dördüncü sütunda ise JIT'lenen metodun paket ile birlikte adı yer alır. Ne yazık ki işlev yüklenen metodları ayırt etmek her zaman kolay olmayabilir. Parantez içinde metodun gövdesinin kaç byte koddan oluştuğu bilgisi bulunur. İşlev yüklenen metodları ayırt etmek için bu değerden yararlanabilirsiniz.
HotSpot'un varsayılan çalışma kipi "mixed"'dir. İki farklı kipi daha bulunur:

  • Compiled: Tüm metodlar JIT'lenerek çalıştırılır. Bu kipte çalışmak için HotSpot'u -Xcomp seçeneği ile çalıştırmalısınız:

c:\opt64\java\jdk1.7.0_71\bin>java -Xcomp -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, compiled mode)
Uygulama sunucularını üretim ortamında bu kipte çalışan HotSpot üzerinde çalıştırmanız uygun olur. 

  • Interpreted: JIT derleyiciler devre dışıdır. Tüm kod yorumlamalı olarak çalıştırılır. Zamanda yolculuk yapmak için bu kipi kullanabilirsiniz: Java ilk çıktığında çok yavaştı, çünkü sadece bu kipte çalışıyordu. Bu yavaşlığı deneyimlemek ve 1996'ya gitmek için -Xinterp seçeneği ile çalıştırmanız yeterli olacaktır:

c:\opt64\java\jdk1.7.0_71\bin>java -Xinterp -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, interpreted mode)

JSM Ergonomisi
Java uygulamalarını çalıştırmak çok kolaydır. jar olarak dağıtılan bir uygulamayı komut satırından çalıştırmak için "java -jar" yazmak yeterlidir. Ancak JSM'nin komut satırından verebileceğimiz çok sayıda seçeneği bulunur. HotSpot için bu seçeneklerin listesini almak üzere, java'yı aşağıdaki seçeneklerle birlikte yazıp, çalıştırın:
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version
Bu komut ekranda yüzlerce satırdan oluşan bir çıktı yaratır:
[Global flags]
    uintx AdaptivePermSizeWeight                    = 20              {product}
    uintx AdaptiveSizeDecrementScaleFactor          = 4               {product}
    uintx AdaptiveSizeMajorGCDecayTimeScale         = 10              {product}
    uintx AdaptiveSizePausePolicy                   = 0               {product}
    uintx AdaptiveSizePolicyCollectionCostMargin    = 50              {product}
    uintx AdaptiveSizePolicyInitializingSteps       = 20              {product}
    uintx AdaptiveSizePolicyOutputInterval          = 0               {product}
    uintx AdaptiveSizePolicyWeight                  = 10              {product}
    uintx AdaptiveSizeThroughPutPolicy              = 0               {product}
    uintx AdaptiveTimeWeight                        = 25              {product}
     bool AdjustConcurrency                         = false           {product}
     bool AggressiveOpts                            = false           {product}
     intx AliasLevel                                = 3               {C2 produc
     bool AlignVector                               = false           {C2 produc
     intx AllocateInstancePrefetchLines             = 1               {product}
     intx AllocatePrefetchDistance                  = 192             {product}
     intx AllocatePrefetchInstr                     = 0               {product}
     intx AllocatePrefetchLines                     = 4               {product}
     intx AllocatePrefetchStepSize                  = 64              {product}
     intx AllocatePrefetchStyle                     = 1               {product}
     bool AllowJNIEnvProxy                          = false           {product}
     bool AllowNonVirtualCalls                      = false           {product}
     bool AllowParallelDefineClass                  = false           {product}
     bool AllowUserSignalHandlers                   = false           {product}
     bool AlwaysActAsServerClassMachine             = false           {product}
     bool AlwaysCompileLoopMethods                  = false           {product}
     bool AlwaysLockClassLoader                     = false           {product}
     bool AlwaysPreTouch                            = false           {product}
     bool AlwaysRestoreFPU                          = false           {product}
     . . . . . . . . . . . .
64-bitlik Server VM HotSpot için tam olarak 758 seçenek vardır. 32-bitlik Client VM HotSpot için 685 seçenek ve 32-bitlik Server VM HotSpot için ise 756 seçenek bulunuyor. Bu JDK 7u71 için böyle. JDK 8u40 için ise seçenek sayılarında farklılıklar var:
64-bitlik Server VM HotSpot 790 seçenek, 32-bitlik Client VM HotSpot için 709 seçenek ve 32-bitlik Server VM HotSpot için ise 787 seçenek bulunuyor. Bu çıktıda birinci satırda seçeneğin tipi var. bool true/false değer alabilen seçeneği ifade eder. intx tamsayı, uintx pozitif tamsayıyı ve double ise kayan nokta sayıları ifade eder. İkinci sütunda değişkenin adı ve üçüncü sütunda ise varsayılan değeri bulunur. Eğer değişkenin değeri := sembolü ile verilmiş ise bu seçeneğin değeri kullanıcı tarafından ezilmiş demektir:
     intx CICompilerCount                          := 3                                   {product}
Seçeneklerin değerini değiştirmek için komut satırında seçeneğin önüne "-XX:" koyup yeni değerini "=" sembolünden sonra tanımlıyoruz:
-XX:CompileThreshold=2000
Tek bir özel durum var. bool tipindeki seçenekler için true ve false yerine + ve - sembollerini seçenek adından hemen önce kullanıyoruz:
-XX:+AggressiveOpts
ya da 
-XX:+AggressiveOpts
Dördüncü sütunda ise seçeneğin sınıfı yer alır. 
HotSpot seçeneklerini oluşturan kümeye JSM ergonomisi diyoruz. JSM, ergonomisini üzerinde çalıştığı platform kaynaklarının (bellek ve işlemci gibi) kapasitesine göre belirler. Bazı seçeneklerinin değerlerini kaynakların durumuna göre otomatik olarak uyarlayarak saptar. Diğer seçeneklerin ise varsayılan sabit başlangıç değerleri vardır. JSM'nin otomatik olarak belirlediği bu seçenekler, JSM'yi akort etmek için kullanılabilir. JSM'nin nasıl akort edileceği ve HotSpot ile ilgili detay bilgi edinmek isteyenler için Java Performance Tuning and Optimization eğitimini tavsiye ederim.
Aslında HotSpot'un bu listenenenden daha da fazla seçeneği bulunur. Tüm seçeneklerin listesini almak için HotSpot'u aşağıdaki seçeneklerle çalıştırmalısınız:
java -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+AggressiveOpts -XX:+PrintFlagsFinal -version
JDK 8u40 için tüm seçenek sayıları:
64-bitlik Server VM HotSpot 834 seçenek, 32-bitlik Client VM HotSpot için 751 seçenek ve 32-bitlik Server VM HotSpot için ise 831 seçenek bulunuyor.

Sunday, April 26, 2015

Java'da Diziler


Java'da değişkenleri çeşitli biçimlerde sınıflandırabiliriz:
  • Yerel değişken Bir metod bloğunda ya da metodun parametre listesinde tanımlanan değişkenler, yerel değişken olarak adlandırılırlar. Yığında saklanırlar. Bu nedenle bazen yığın değişkeni olarak da adlandırılır. Otomatik olarak yaratılır ve yine erimi dışına çıkınca otomatik olarak yok edilirler. Bu nedenle de bazen otomatik değişken olarak da isimlendirildiği de olur. Okumadan önce mutlaka ilklendirmek gerekir. Derleyici bu durumun takipçisidir!
  • Durum değişkeni Sınıf için tanımladığımız öznitelikler durum değişkeni olarak adlandırılır. Nesnenin durumunu oluştururlar. Bir sınıftan, her nesne yaratıldığında durum değişkenleri için prosesin Heap alanında yer ayrılır:
public class Circle {
    private double x;
    private double y;
    private double radius;
    private byte thickness;  
    private String color;

    . 
    . 
    .
}
Durum değişkenlerinin bellekte nasıl sıralanacaklarına Java Sanal Makinası (JSM) karar verir. Örneğin Circle sınıfı için CompressedOops özelliğinin açık olduğu 64-bitlik bir JSM'de aşağıdaki gibi bir dizilim gerçekleşir:
offset  size   type description
      0    12        (assumed to be the object header + first field alignment)
     12     1   byte Circle.tickness
     13     3        (alignment/padding gap)
     16     8 double Circle.x
     24     8 double Circle.y
     32     8 double Circle.radius
     40     4 String Circle.color
     44    20        (loss due to the next object alignment)
     64              (object boundary, size estimate)
Durum değişkenleri otomatik olarak ilklendirilir. İlklendirme değeri tamsayı ve kayan noktalı sayılar için 0, boolean için false ve tipi sınıf olan tüm değişkenler için ise null değeridir:
Veri Tipi
Varsayılan Değeri
boolean
false
char
\u0000
int,short,byte / long
0 / 0L
float /double
0.0f / 0.0d
Tüm referans tipler
null
  • Değer tipli değişken Tipi Java'daki temel tiplerden seçilmiş olan değişkenlerdir. Değişkenleri bellekte bir kutu olarak düşünebiliriz. Değer saklayan değişkenlerde, kutuyu açtığımızda, içinden tanımlandığı tipte bir değer ile karşılaşırız. Kutunun boyu, temel tipin tanımladığı uzunluktadır. Örneğin byte için bir sekizlik (1-Byte), long için 8 sekizlik (8-Byte), char için ise iki sekizlik olacaktır.
  • Referans değişkeni Temel tipler dışında, tipi bir sınıf olan değişkenler referans değişkenidir. Bu değişkenler için kutuyu açtığımızda içinden referans ettiği nesnenin adresi çıkar. 32-bit JSM için 32-bit ve 64-bit JSM için ise Heap alanının boyuna göre 32-bitten 64-bite kadar değişken uzunlukta olabilir.
  • Skalar değişken Skalar değişkenler sadece tek bir değer ya da nesnenin referansını saklayabilen değişkenlerdir:
int x= 42;
String name= "Jack";
double pi= 3.1415;
Circle blueUnitCircle= new Circle(0.,0.,1.,"Blue");
  • Dizi değişkeni Bazen bir değişkende birden fazla değer ya da nesne saklamak isteriz. Bu durumda dizilerden yararlanılır. Dizi tanımlamak kolaydır. Tek boyutlu bir dizi köşeli parantez çifti kullanılarak tanımlanabilir:

int lost[];
int []array;
Köşeli parantez çifti ([]) değişkenin önünde ya da arkasında kullanılabilir. Dizinin tanımını yaptık ama henüz bellek gözleri için yer almadık. Dizinin gözlerini yaratmak için farklı yöntemler kullanılabilir:
int lost[] = { 4, 8, 15, 16, 23, 42 };
int[] array;
int []dizi;
array = new int[] { 4, 8, 15, 16, 23, 42 };
dizi= new int[6];
dizi[0]= 4;
dizi[1]= 8;
dizi[2]= 15;
dizi[3]= 16;
dizi[4]= 23;
dizi[5]= 42;
int []lottery;
lottery= new Random().ints(1,49).distinct().limit(6).sorted().toArray();
int []sequence;
sequence= IntStream.range(1, 100).toArray();
Dizi elemanları bellekte ardışıl gözlerde saklanırlar. Dizi eleman sayısını bilir:
Şekil-1 Java'da diziler
Dizi gözlerine, gözün indisini köşeli parantez içerisinde vererek erişiyoruz. Dizi indisleri sıfırdan başlayarak adreslenir. Tamsayı değerler ile dolu bir dizinin elemanlarının toplamı için aşağıdaki çözümleri kullanabiliriz:
int[] sequence = IntStream.range(1, 100).toArray();
long sum = 0;
for (int i = 0; i < sequence.length; ++i) {
    sum += sequence[i];
}

int[] sequence = IntStream.range(1, 100).toArray();
long sum = 0;
for (int cell : sequence)
    sum += cell;

int[] sequence = IntStream.range(1, 100).toArray();
long sum = Arrays.stream(sequence).sum();
Dizinin gözlerine erişirken indisin sınırlar dışına çıkmadığından emin olun. Aksi halde yürütme zamanında IndexOutOfBoundsException hatası ile karşılaşırsınız. Neden böyle oldu? sorusunun yanıtını bulup, kodun bu hatayı üretmemesini sağlamalısınız! for-each (for (int cell : sequence)) yapısını kullanırsanız, zaten bu hata ile hiçbir zaman karşılaşmazsınız. Ancak bu yapının salt okunur olduğuna dikkat etmelisiniz: Mevcut dizide bir değişiklik yapamazsınız!
Circle[] circles = new Circle[10];
for (Circle circle: circles){
    circle= new Circle(0.,0.,1.,"Blue");
}
for (Circle circle: circles){
    System.err.println(circle.area());
}
Yukarıdaki kodu çalıştırırsanız, java.lang.NullPointerException hatasını alırsınız! Dizi gözlerinde değişiklik yapmak için her zaman indis üzerinde dönen bir döngü kurmanız gerekir:
Circle[] circles = new Circle[10];
for (int i=0;i<circles.length;++i){
    circles[i] = new Circle(0.,0.,1.,"Blue");
}
for (Circle circle: circles){
    System.err.println(circle.area());
}
Eğer temel tipler üzerinde aritmetik işlem yapmak üzere bir dizi oluşturacaksanız, diziyi mutlaka temel tipten tanımlayın, sakın sarmal sınıftan tanımlamayın! 
Şekil-2 Integer tipinden bir dizinin bellekteki yerleşimi
Şekil-3 int tipinden bir dizinin bellekteki yerleşimi

Hem bellekte hem de çalışma süresinde kaybedersiniz:
package com.example.test;

public class TestWrapperPrimitiveArrays {
 private static final int LOOP_COUNT = 50;
 private static Integer[] bigIntegerArray = new Integer[1_000_000];
 private static int[] bigIntArray = new int[1_000_000];
 static {
  for (int i = 0; i < bigIntegerArray.length; ++i)
      bigIntegerArray[i] = new Integer(1);
  for (int i = 0; i < bigIntegerArray.length; ++i)
      bigIntArray[i] = 1;
 }

 public static void main(String[] args) {
  System.err.println("Wrapper");
  for (int i = 0; i < LOOP_COUNT; ++i)
      runWrapper();
  System.err.println("Primitive");
  for (int i = 0; i < LOOP_COUNT; ++i)
      runPrimitive();
 }

 public static void runWrapper() {
  long startTime = System.nanoTime();
  long sum = 0;
  for (Integer x : bigIntegerArray)
      sum += x;
  long stopTime = System.nanoTime();
  System.err.println("Sum: " + sum + " @ " + (stopTime - startTime));
 }

 public static void runPrimitive() {
  long startTime = System.nanoTime();
  long sum = 0;
  for (int x : bigIntArray)
      sum += x;
  long stopTime = System.nanoTime();
  System.err.println("Sum: " + sum + " @ " + (stopTime - startTime));
 }

}
Bir Integer tipinde nesne bellekte 16 Byte yer kaplar, buna karşılık olarak int tipi ise sadece 4 Byte! 1 milyon elemanlı dizi için toplam harcanan bellek miktarı, referanslar da dahil olmak üzere, Integer tercihi (Şekil-2) için 20 milyon Byte ve int tercihi (Şekil-3) için ise 4 milyon Byte olacaktır. int tipini tercih ederseniz kapladığı alan beşte birine düşüyor! Çalışma zamanları karşılaştırıldığında ise int tipi dizi üzerinde elemanları toplamak için geçen süre, Integer tipindeki dizinin elemanlarını toplamak için geçen sürenin  üçte biri kadardır. Kazanan yine temel tip: int!
İki Boyutlu Diziler 
İki boyutlu dizileri matris olarak adlandırıyoruz. Matris tanımlamak, tek boyutlu dizi tanımlamaktan çok farklı değil. İki boyutlu olduğunu belirtmek için köşeli parantez çiftinden iki tane kullanıyoruz: 
int [][]unitMatrix= {
 {1, 0, 0}, 
 {0, 1, 0}, 
 {0, 0, 1} 
};
Genel olarak n-boyutlu bir dizi tanımlamak için n adet köşeli parantez çifti kullanılır. Çok boyutlu diziler bellekte satır düzeninde yerleşirler. Bu nedenle, unitMatrix[0] birinci satırı, unitMatrix[1] ikinci satırı ve elbette unitMatrix[2] üçüncü satırı tanımlar. unitMatrix.length matrisin satır sayısını verir. Matris kare matris ise satır sayısı sütun sayısına eşit olacaktır. Bu durumda, unitMatrix.lengthunitMatrix[0].lengthunitMatrix[1].lengthunitMatrix[2].length değerlerinin hepsi biri birine eşit olacaktır. Java'da matrisin satırlarında farklı sayılarda eleman bulunabilir. Matrisi farklı şekillerde tarayabiliriz:
int[][] unitMatrix = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
for (int i=0;i<unitMatrix.length;++i){
   int []row= unitMatrix[i];
   System.err.println(Arrays.toString(row));
}

int[][] unitMatrix = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
for (int[] row : unitMatrix) {
    System.err.println(Arrays.toString(row));
}

int[][] unitMatrix = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
Arrays.stream(unitMatrix)
      .map(Arrays::toString)
      .forEach(System.err::println);
Yukarıdaki her üç kod parçası için de ekran çıktısı aşağıdaki gibi gerçekleşir:
[1, 0, 0]
[0, 1, 0]
[0, 0, 1]
Diziler Üzerinde Paralel İşlemler
Java 8 ile çok çekirdekli platformlar üzerinde paralel koşan uygulamalar geliştirebiliyoruz. Bu bölümde, büyük veriler işlenirken sıklıkla kullanılan temel vektör ve matris işlemlerinin, Stream API ve Lambda ifadeleri kullanılarak, nasıl gerçekleştirildiğini çalışacağız. Önce vektör iç çarpımını ele alalım.
Vektör İç Çarpımı
u ve v gibi iki vektörün iç çarpımı aynı indisli elemanlarının çarpımının toplamı olarak tanımlanır:
$$\langle u , v \rangle = \sum_{i=1}^{n}u_i v_i$$. Bu tanım hem seri olarak hem de Java 8'de gelen paralel stream kullanılarak gerçeklenebilir:

package com.example.test;

import java.util.Random;
import java.util.function.BiFunction;
import java.util.stream.IntStream;

public class DotProductExample {
 private static final int ARRAY_SIZE = 300_000_000;
 private static final int LOOP_COUNT = 50;
 static int[] u;
 static int[] v;
 static {
  System.err.print("Populating u...");
  u = new Random().ints(0, 2).limit(ARRAY_SIZE).toArray();
  System.err.print("Done.\nPopulating v...");
  v = new Random().ints(0, 2).limit(ARRAY_SIZE).toArray();
  System.err.print("Done.\n");
 }

 public static void main(String[] args) {
  for (int i = 1; i <= LOOP_COUNT; ++i) {
   run(i, "Serial", DotProductExample::dotProductSerial);
  }
  for (int i = 1; i <= LOOP_COUNT; ++i) {
   run(i, "Stream", DotProductExample::dotProductStream);
  }
 }

 private static void run(int runNo, String method,
   BiFunction<int[], int[], Integer> productFunction) {
  long startTime = System.nanoTime();
  int product = productFunction.apply(u, v);
  long stopTime = System.nanoTime();
  System.err.println(String.format("[%-8s][%3d]:[%10d]@[%10d]", method,
    runNo, product, stopTime - startTime));
 }

 private static Integer dotProductSerial(int[] u, int[] v) {
  int sum = 0;
  for (int i = 0; i < u.length; ++i)
   sum += u[i] * v[i];
  return sum;
 }

 private static Integer dotProductStream(int[] u, int[] v) {
  return IntStream.range(0, u.length).parallel().map(i -> u[i] * v[i])
    .sum();
 }

}
Matris Çarpımı
A ve B gibi iki matrisin çarpımı,
$$
C_{m,n} = A_{m,s} \times B_{s,n}
$$
$$
A_{m,s} =
\begin{pmatrix}
a_{1,1} & a_{1,2} & \cdots & a_{1,s} \\
a_{2,1} & a_{2,2} & \cdots & a_{2,s} \\
\vdots & \vdots & \ddots & \vdots \\
a_{m,1} & a_{m,2} & \cdots & a_{m,s}
\end{pmatrix},
B_{s,n} =
\begin{pmatrix}
b_{1,1} & b_{1,2} & \cdots & b_{1,n} \\
b_{2,1} & b_{2,2} & \cdots & b_{2,n} \\
\vdots & \vdots & \ddots & \vdots \\
b_{s,1} & b_{s,2} & \cdots & b_{s,n}
\end{pmatrix},
C_{m,n} =
\begin{pmatrix}
c_{1,1} & c_{1,2} & \cdots & c_{1,n} \\
c_{2,1} & c_{2,2} & \cdots & c_{2,n} \\
\vdots & \vdots & \ddots & \vdots \\
c_{m,1} & c_{m,2} & \cdots & c_{m,n}
\end{pmatrix}
$$
$$
c_{i,j} = \sum_{k=1}^{k=s} a_{i,k} b_{k,j}
$$
biçiminde tanımlanır. Bu tanıma uygun olarak çarpımı hem seri olarak hem de Stream API kullanarak hesaplayan kodu aşağıda bulabilirsiniz.
package com.example.test;

import java.util.Arrays;
import java.util.stream.IntStream;

public class MatrixProductExample {
 private static final int LOOP_COUNT = 25;
 private static final int A_ROW_SIZE = 3000;
 private static final int A_COL_SIZE = 400;
 private static final int B_ROW_SIZE = 400;
 private static final int B_COL_SIZE = 5000;
 private static final int C_ROW_SIZE = 3000;
 private static final int C_COL_SIZE = 5000;
 private static final int[][] A;
 private static final int[][] B;
 private static final int[][] C;

 static {
  A = new int[A_ROW_SIZE][A_COL_SIZE];
  B = new int[B_ROW_SIZE][B_COL_SIZE];
  C = new int[C_ROW_SIZE][C_COL_SIZE];
  System.err.print("Populating A...");
  populateMatrix(A);
  System.err.print("Done.\nPopulating B...");
  populateMatrix(B);
  System.err.print("Done.\n");
 }

 private static void populateMatrix(int[][] A) {
  int rowSize = A.length;
  int colSize = A[0].length;
  IntStream.range(0, rowSize * colSize)
           .map(i -> {
                int row = i / colSize;
                int col = i % colSize;
                A[row][col] = row + col;
                return 0;
             }).sum();
 }

 public static void main(String[] args) {
  System.err.println("Matrix multiplication using Stream API.");
  for (int i = 1; i <= LOOP_COUNT; ++i)
      runStream();
  System.err.println("Matrix multiplication using 3x \"for\" loops.");
  for (int i = 1; i <= LOOP_COUNT; ++i)
      runStream();
 }

 public static void runSerial() {
  long startTime = System.nanoTime();
  productSerial(A, B, C);
  long stopTime = System.nanoTime();
  System.err.println(stopTime - startTime);
 }

 public static void runStream() {
  long startTime = System.nanoTime();
  productStream(A, B, C);
  long stopTime = System.nanoTime();
  System.err.println(stopTime - startTime);
 }

 public static void productStream(int[][] A, int[][] B, int[][] C) {
  int rowSizeA = A.length;
  int colSizeA = A[0].length;
  int colSizeB = B[0].length;
  IntStream.range(0, rowSizeA * colSizeB)
           .parallel()
           .map(i -> {
                  int row = i / colSizeB;
                  int col = i % colSizeB;
                  C[row][col] = 0;
                  for (int j = 0; j < colSizeA; ++j)
                      C[row][col] += A[row][j] * B[j][col];
                  return 0;
            }).sum();
 }

 public static void productSerial(int[][] A, int[][] B, int[][] C) {
  int rowSizeA = A.length;
  int colSizeA = A[0].length;
  int colSizeB = B[0].length;
  for (int i = 0; i < rowSizeA; ++i) {
      for (int j = 0; j < colSizeB; ++j) {
          C[i][j] = 0;
          for (int k = 0; k < colSizeA; ++k)
              C[i][j] += A[i][k] * B[k][j];
      } 
   }
 }

 private static void print(int[][] A) {
  Arrays.stream(A).map(Arrays::toString).forEach(System.err::println);
 }
}
i7 işlemcili dizüstü bilgisayarımda yukarıdaki uygulamayı çalıştırdığımda, seri çözüm ortalama 80 saniyede, Stream API tabanlı çözüm ise 25 saniyede tamamlandı. Çekirdek sayısına yakın bir hızlanmaya işaret ediyor. Yaşasın yeni kral: Java 8!
Metod Parametresi Olarak Diziler
Dizileri metotlara parametre olarak geçebiliriz. Bunun için metod parametresini dizi türünden tanıtmamız yeterli olacaktır:
public static long sumIf(int[] values, Predicate<Integer> constraint) {
 long sum = 0;
 for (int value : values) {
  if (constraint.test(value))
   sum += value;
 }
 return sum;
}
Stream API kullanarak bu çözümü daha yalın olarak tanımlayabiliriz:
public static long sumIf(int[] values, IntPredicate constraint) {
 return Arrays.stream(values).filter(constraint).sum();
}
sumIf fonksiyonunu çağırmak için önce bir dizi yaratmamız gerekiyor:
public static void main(String[] args) {
 int[] bigData = new Random().ints(1, 10000)
                       .limit(SIZE)
                       .toArray();
 sumIf(bigData, i -> i > 0 && i < 100);
}
Fonksiyona dizi türünde parametre geçmek için Java SE 5 ile bir diğer gösterim daha bulunuyor. Üç nokta (...) gösterimini, C programlama dilinden de biliyoruz. Metodun değişken sayıda parametre alacağını ifade ediyor. Gerçekte, fonksiyona verdiğimiz parametreler, derleyici tarafından diziye dönüştürülür ve dizi olarak çağrılır. Dizi oluşturmak için kod yazmakla gereksiz vakit kaybedilmemiş olur. Bu özellik, sadece gösterim kolaylığı sağlar. Üç nokta ile tanımlanan parametreyi dizi ile de doğrudan çağırabilirsiniz.
private static final int SIZE = 5000;

public static long sumIf(IntPredicate constraint, int... values) {
 return Arrays.stream(values).filter(constraint).sum();
}

public static void main(String[] args) {
 int[] bigData = new Random().ints(1, 10000).limit(SIZE).toArray();
 sumIf(i -> i > 0 && i < 100, bigData);
 sumIf(i -> i / 2 != 0, 4, 8, 15, 16, 23, 42);

}
Uymanız gereken iki basit temel kural var:
1. Üç nokta mutlaka metodun parametre listesinde sonda yer almalıdır
2. Birden fazla üç nokta tanımlaması yer alamaz.
Java 9+'da Diziler ile İlgili Yenilikler
Java 9 ve sonrasında diziler ile ilgili bizi bekleyen önemli iki gelişme bulunuyor. Bunlardan ilki sadece değer taşıyan durumu değiştirilemez (=Immutable) nesnelerin belleğe etkin yerleşimi ile ilgilidir. Örneğin, görüntüyü oluşturan benekleri Pixel adlı bir sınıfta modelleyelim:
public class Pixel {
    int red;
    int green;
    int blue; 
    double alpha; // 0 <= alpha <= 1
}
Pixel tipinden bir dizi tanımlarsak, belleğe Şekil-4'deki gibi yerleşecektir ve bu yerleşimin hiç verimli olmadığı açıktır. Hem kapladığı alan gereğinden fazladır hem de bellekte ardışıl gözlere yerleşmedikleri için cep bellekte çok sayıda ıskalama yapacaktır.
Şekil-4 Pixel tipinden bir dizinin bellekteki yerleşimi
Bu problemleri çözmek için Pixel sınıfını "value class" olarak tanımlıyoruz:
value public class Pixel {
    final int red;
    final int green;
    final int blue; 
    final double alpha;
    . . . 
}
Şimdi yerleşim bir öncekinden farklı olarak ardışıl olacaktır:
Şekil-5 Pixel tipinden bir dizinin "value class" olarak bellekteki yerleşimi
Elbette "value class"'larda hala çok şekillilik çalışmaya devam ediyor! Generics çatısında buna paralel bir değişiklik bekliyoruz:
new ArrayList<int>()
Evet, artık tip parametresi olarak temel tip verebiliyoruz! Burada JSM, yürütme zamanında yeni bir sınıf yaratıyor: ArrayList${T=int}.class. Bu durum bana hiç yabancı gelmedi: C++'da üretken programlama zaten bu şekilde çalışıyor!
Bu konudaki gelişmeleri aşağıdaki bağlantılardan takip edebilirsiniz: