Java Sanal Makinesi - Hızlı Kılavuz

JVM bir spesifikasyondur ve spesifikasyonlara uydukları sürece farklı uygulamalara sahip olabilir. Özellikler aşağıdaki bağlantıda bulunabilir -https://docs.oracle.com

Oracle'ın kendi JVM uygulaması vardır (HotSpot JVM olarak adlandırılır), IBM'in kendine ait (örneğin J9 JVM) vardır.

Spesifikasyon içerisinde tanımlanan işlemler aşağıda verilmiştir (kaynak - Oracle JVM Spesifikasyonları, yukarıdaki bağlantıya bakınız) -

  • 'Sınıf' dosya biçimi
  • Veri tipleri
  • İlkel türler ve değerler
  • Referans türleri ve değerleri
  • Çalışma zamanı veri alanları
  • Frames
  • Nesnelerin temsili
  • Kayan nokta aritmetiği
  • Özel yöntemler
  • Exceptions
  • Talimat seti özeti
  • Sınıf kitaplıkları
  • Kamusal tasarım, özel uygulama

JVM, sanal bir makinedir, kendi ISA'sına, kendi belleğine, yığınına, yığınına vb. Sahip soyut bir bilgisayardır. Ana bilgisayar işletim sisteminde çalışır ve kaynak taleplerini ona yönlendirir.

HotSpot JVM 3'ün mimarisi aşağıda gösterilmiştir -

Yürütme motoru, çöp toplayıcı ve JIT derleyicisinden oluşur. JVM'nin iki çeşidi vardır -client and server. Bunların her ikisi de aynı çalışma zamanı kodunu paylaşır ancak JIT'in ne kullanıldığı konusunda farklılık gösterir. Bu konu hakkında daha sonra daha fazlasını öğreneceğiz. Kullanıcı, -client veya -server JVM bayraklarını belirterek hangi çeşidin kullanılacağını kontrol edebilir . JVM sunucusu, sunucularda uzun süre çalışan Java uygulamaları için tasarlanmıştır.

JVM, 32b ve 64b sürümlerinde gelir. Kullanıcı, VM argümanlarında -d32 veya -d64 kullanarak hangi sürümün kullanılacağını belirleyebilir. 32b sürümü yalnızca 4G belleğe kadar adresleyebilir. Bellekte büyük veri kümelerini tutan kritik uygulamalarla, 64b sürümü bu ihtiyacı karşılar.

JVM, sınıfları ve arabirimleri dinamik bir şekilde yükleme, bağlama ve başlatma sürecini yönetir. Yükleme işlemi sırasında,JVM finds the binary representation of a class and creates it.

Bağlama işlemi sırasında, loaded classes are combined into the run-time state of the JVM so that they can be executed during the initialization phase. JVM, temel olarak bağlama işlemi için çalışma zamanı sabit havuzunda depolanan sembol tablosunu kullanır. Başlatma aslında şunlardan oluşur:executing the linked classes.

Yükleyici Tipleri

BootStrapsınıf yükleyici, sınıf yükleyici hiyerarşisinin en üstündedir. Standart JDK sınıflarını JRE'nin lib dizinine yükler .

Extension sınıf yükleyici, sınıf yükleyici hiyerarşisinin ortasındadır ve önyükleme sınıfı yükleyicinin hemen alt öğesidir ve sınıfları JRE'nin lib \ ext dizinine yükler.

Applicationsınıf yükleyici, sınıf yükleyici hiyerarşisinin en altındadır ve uygulama sınıfı yükleyicinin hemen alt öğesidir. Tarafından belirtilen kavanozları ve sınıfları yükler.CLASSPATH ENV değişken.

Bağlanıyor

Bağlama süreci aşağıdaki üç adımdan oluşur -

Verification- Bu, oluşturulan .class dosyalarının (Bytecode) geçerli olduğundan emin olmak için Bytecode doğrulayıcı tarafından yapılır. Değilse, bir hata atılır ve bağlama işlemi durur.

Preparation - Bellek, bir sınıfın tüm statik değişkenlerine tahsis edilir ve bunlar varsayılan değerlerle başlatılır.

Resolution- Tüm sembolik bellek referansları orijinal referanslarla değiştirilir. Bunu başarmak için, sınıfın yöntem alanının çalışma zamanı sabit belleğindeki sembol tablosu kullanılır.

Başlatma

Bu, sınıf yükleme sürecinin son aşamasıdır. Statik değişkenlere orijinal değerler atanır ve statik bloklar çalıştırılır.

JVM özelliği, programın yürütülmesi sırasında ihtiyaç duyulan belirli çalışma zamanı veri alanlarını tanımlar. Bazıları JVM başlarken oluşturulur. Diğerleri evreler için yereldir ve yalnızca bir evre oluşturulduğunda oluşturulur (ve iş parçacığı yok edildiğinde yok edilir). Bunlar aşağıda listelenmiştir -

PC (Program Sayacı) Kaydı

Her evre için yereldir ve iş parçacığının halihazırda yürütmekte olduğu JVM talimatının adresini içerir.

Yığın

Her evre için yereldir ve yöntem çağrıları sırasında parametreleri, yerel değişkenleri ve dönüş adreslerini depolar. Bir iş parçacığı izin verilenden daha fazla yığın alanı talep ederse bir StackOverflow hatası oluşabilir. Yığın dinamik olarak genişletilebilirse, yine de OutOfMemoryError atabilir.

Yığın

Tüm evreler arasında paylaşılır ve çalışma zamanı sırasında oluşturulan nesneleri, sınıfların meta verilerini, dizileri vb. İçerir. JVM başladığında oluşturulur ve JVM kapandığında yok edilir. JVM'nizin işletim sisteminden talep ettiği yığın miktarını belirli bayrakları kullanarak kontrol edebilirsiniz (bu konu hakkında daha sonra daha fazlası). Önemli performans etkilerine sahip olduğu için hafızanın çok az veya çok fazlasını talep etmemeye özen gösterilmelidir. Ayrıca, GC bu alanı yönetir ve alanı boşaltmak için sürekli olarak ölü nesneleri kaldırır.

Yöntem Alanı

Bu çalışma zamanı alanı tüm iş parçacıkları için ortaktır ve JVM başladığında oluşturulur. Sabit havuz (bundan sonra daha fazlası), yapıcılar ve yöntemler için kod, yöntem verileri vb. Gibi sınıf başına yapıları depolar. JLS, bu alanın çöp toplanması gerekip gerekmediğini ve dolayısıyla JVM, GC'yi göz ardı etmeyi seçebilir. Ayrıca, bu, uygulamanın ihtiyaçlarına göre genişleyebilir veya genişlemeyebilir. JLS, bununla ilgili hiçbir şeyi zorunlu kılmamaktadır.

Çalışma Zamanı Sabit Havuzu

JVM, yüklenen sınıfları bağlarken sembol tablosu (birçok rolden biri) olarak görev yapan sınıf başına / tür başına veri yapısını korur.

Yerel Yöntem Yığınları

Bir iş parçacığı yerel bir yöntemi çağırdığında, Java sanal makinesinin yapılarının ve güvenlik kısıtlamalarının artık özgürlüğünü engellemediği yeni bir dünyaya girer. Yerel bir yöntem, sanal makinenin çalışma zamanı veri alanlarına erişebilir (yerel yöntem arabirimine bağlıdır), ancak istediği her şeyi de yapabilir.

Çöp toplama

JVM, Java'daki nesnelerin tüm yaşam döngüsünü yönetir. Bir nesne oluşturulduktan sonra, geliştiricinin artık onun için endişelenmesine gerek yoktur. Nesnenin ölmesi durumunda (yani, artık ona referans yok), birçok algoritmadan biri (seri GC, CMS, G1, vb.) Kullanılarak GC tarafından yığından çıkarılır.

GC işlemi sırasında nesneler belleğe taşınır. Bu nedenle, bu nesneler işlem devam ederken kullanılamaz. İşlem süresince tüm uygulama durdurulmalıdır. Bu tür duraklamalara 'dünyayı durdur' duraklamaları denir ve çok büyük bir ek yüktür. GC algoritmaları öncelikle bu süreyi azaltmayı amaçlamaktadır. Bunu ilerleyen bölümlerde ayrıntılı olarak tartışacağız.

GC sayesinde, Java'da bellek sızıntıları çok nadirdir, ancak olabilirler. Daha sonraki bölümlerde Java'da nasıl bellek sızıntısı yaratılacağını göreceğiz.

Bu bölümde, JIT derleyicisini ve derlenmiş ve yorumlanmış diller arasındaki farkı öğreneceğiz.

Derlenmiş ve Yorumlanmış Diller

C, C ++ ve FORTRAN gibi diller derlenmiş dillerdir. Kodları, temel makineye hedeflenen ikili kod olarak teslim edilir. Bu, yüksek seviyeli kodun, temeldeki mimari için özel olarak yazılmış statik bir derleyici tarafından bir kerede ikili koda derlendiği anlamına gelir. Üretilen ikili, başka herhangi bir mimaride çalışmayacaktır.

Öte yandan, Python ve Perl gibi yorumlanan diller, geçerli bir tercümana sahip oldukları sürece herhangi bir makinede çalışabilir. Üst düzey kod üzerinden satır satır üzerinden geçerek bunu ikili koda dönüştürür.

Yorumlanan kod genellikle derlenmiş koddan daha yavaştır. Örneğin, bir döngü düşünün. Yorumlanan, döngünün her yinelemesi için karşılık gelen kodu dönüştürür. Öte yandan, derlenmiş bir kod, çeviriyi yalnızca bir tane yapacaktır. Ayrıca, yorumlayıcılar bir seferde yalnızca bir satır gördüklerinden, derleyiciler gibi ifadelerin yürütme sırasını değiştirmek gibi önemli bir kod gerçekleştiremezler.

Aşağıda bu tür bir optimizasyon örneğini inceleyeceğiz -

Adding two numbers stored in memory. Belleğe erişim birden fazla CPU döngüsünü tüketebileceğinden, iyi bir derleyici verileri bellekten almak için talimatlar yayınlayacak ve eklemeyi yalnızca veri mevcut olduğunda gerçekleştirecektir. Beklemeyecek ve bu arada diğer talimatları uygulayacaktır. Öte yandan, yorumlayıcı herhangi bir zamanda tüm kodun farkında olmadığından, yorumlama sırasında böyle bir optimizasyon mümkün olmayacaktır.

Ancak, yorumlanmış diller, o dilin geçerli bir yorumlayıcısına sahip herhangi bir makinede çalışabilir.

Java Derlenmiş mi Yorumlanmış mı?

Java bir orta yol bulmaya çalıştı. JVM, javac derleyicisi ile temeldeki donanım arasında oturduğundan, javac (veya başka herhangi bir derleyici) derleyici, platforma özel bir JVM tarafından anlaşılan Bytecode'da Java kodunu derler. Daha sonra JVM, kod yürütülürken JIT (Just-in-time) derlemesini kullanarak Bytecode'u ikili olarak derler.

HotSpot'lar

Tipik bir programda, sıkça yürütülen yalnızca küçük bir kod bölümü vardır ve çoğu zaman, tüm uygulamanın performansını önemli ölçüde etkileyen bu koddur. Bu tür kod bölümleri denirHotSpots.

Kodun bir bölümü yalnızca bir kez çalıştırılırsa, bunu derlemek çaba israfı olur ve bunun yerine Bytecode'u yorumlamak daha hızlı olur. Ancak bölüm sıcak bir bölümse ve birden çok kez yürütülürse, JVM bunun yerine onu derler. Örneğin, bir yöntem birden çok kez çağrılırsa, kodu derlemek için gereken fazladan döngüler, üretilen daha hızlı ikili dosya tarafından dengelenecektir.

Dahası, JVM belirli bir yöntemi veya bir döngüyü ne kadar çok çalıştırırsa, daha hızlı bir ikili dosya oluşturmak için çeşitli optimizasyonları yapmak için o kadar fazla bilgi toplar.

Şu kodu ele alalım -

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

Bu kod yorumlanırsa, yorumlayıcı her yineleme için obj1 sınıflarının çıkarsaması yapacaktır. Bunun nedeni, Java'daki her sınıfın Object sınıfından genişletilen ve geçersiz kılınabilen bir .equals () yöntemine sahip olmasıdır. Yani obj1 her yineleme için bir dizge olsa bile, kesinti yine de yapılacaktır.

Öte yandan, gerçekte olan şey, JVM'nin her yineleme için obj1'in String sınıfından olduğunu fark etmesi ve dolayısıyla doğrudan String sınıfının .equals () yöntemine karşılık gelen kodu üretmesidir. Böylece hiçbir arama gerekmeyecek ve derlenen kod daha hızlı çalışacaktır.

Bu tür bir davranış ancak JVM kodun nasıl davrandığını bildiğinde mümkündür. Böylece kodun belirli bölümlerini derlemeden önce bekler.

Aşağıda başka bir örnek var -

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

Her döngü için bir yorumlayıcı hafızadan 'toplam' değerini alır, ona 'I' ekler ve hafızaya geri depolar. Bellek erişimi pahalı bir işlemdir ve genellikle birden çok CPU döngüsü gerektirir. Bu kod birden çok kez çalıştığı için bir HotSpot'tur. JIT bu kodu derleyecek ve aşağıdaki optimizasyonu yapacaktır.

'Toplam'ın yerel bir kopyası, belirli bir iş parçacığına özel bir kayıtta saklanacaktır. Tüm işlemler yazmaçtaki değere yapılır ve döngü tamamlandığında değer belleğe geri yazılır.

Ya başka iş parçacıkları da değişkene erişiyorsa? Güncellemeler başka bir iş parçacığı tarafından değişkenin yerel bir kopyasına yapıldığından, eski bir değer göreceklerdir. Bu gibi durumlarda iş parçacığı senkronizasyonu gereklidir. Çok temel bir ilkel senkronizasyon, 'toplamı' değişken olarak ilan etmek olacaktır. Şimdi, bir değişkene erişmeden önce, bir iş parçacığı yerel kayıtlarını temizler ve değeri bellekten alır. Erişim sağlandıktan sonra, değer hemen belleğe yazılır.

Aşağıda, JIT derleyicileri tarafından yapılan bazı genel optimizasyonlar bulunmaktadır -

  • Satır içi yöntem
  • Ölü kod eleme
  • Arama sitelerini optimize etmek için buluşsal yöntemler
  • Sabit katlama

JVM, beş derleme düzeyini destekler -

  • Interpreter
  • Tam optimizasyonlu C1 (profil oluşturma yok)
  • Çağırma ve arka kenar sayaçları ile C1 (ışık profili oluşturma)
  • Tam profilli C1
  • C2 (önceki adımlardaki profilleme verilerini kullanır)

Tüm JIT derleyicilerini devre dışı bırakmak ve yalnızca yorumlayıcı kullanmak istiyorsanız -Xint kullanın.

İstemci ve Sunucu JIT Karşılaştırması

İlgili modları etkinleştirmek için -client ve -server'ı kullanın.

İstemci derleyici (C1) kodu derlemeye sunucu derleyicisinden (C2) daha erken başlar. Dolayısıyla, C2 derlemeye başladığında, C1 zaten kod bölümlerini derlemiş olacaktı.

Ancak beklerken C2, kodu C1'den daha fazla bilmek için profiller. Bu nedenle, optimizasyonlarla dengelenmesi durumunda beklediği süre çok daha hızlı bir ikili oluşturmak için kullanılabilir. Bir kullanıcının bakış açısından, takas, programın başlangıç ​​zamanı ile programın çalışması için geçen süre arasındadır. Başlatma süresi premium ise, C1 kullanılmalıdır. Uygulamanın uzun bir süre çalışması bekleniyorsa (tipik olarak sunuculara yerleştirilen uygulamalar), herhangi bir ekstra başlatma süresini büyük ölçüde dengeleyen çok daha hızlı kod ürettiği için C2'yi kullanmak daha iyidir.

IDE'ler (NetBeans, Eclipse) ve diğer GUI programları gibi programlar için, başlatma süresi kritiktir. NetBeans'in başlaması bir dakika veya daha uzun sürebilir. NetBeans gibi programlar başlatıldığında yüzlerce sınıf derlenir. Bu gibi durumlarda, C1 derleyicisi en iyi seçimdir.

C1'in iki sürümü olduğunu unutmayın - 32b and 64b. C2 sadece gelir64b.

Katmanlı Derleme

Java'nın eski sürümlerinde, kullanıcı aşağıdaki seçeneklerden birini seçebilirdi -

  • Tercüman (-Xint)
  • C1 (-client)
  • C2 (-server)

Java 7'de geldi. Başlamak için C1 derleyicisini kullanır ve kod ısındıkça C2'ye geçer. Aşağıdaki JVM seçenekleriyle etkinleştirilebilir: -XX: + TieredCompilation. Varsayılan değerset to false in Java 7, and to true in Java 8.

Beş derleme katmanından, katmanlı derleme kullanımları 1 -> 4 -> 5.

32b'lik bir makinede, JVM'nin yalnızca 32b sürümü kurulabilir. Bir 64b makinesinde, kullanıcının 32b ve 64b versiyonu arasında bir seçeneği vardır. Ancak bunun Java uygulamalarımızın performansını etkileyebilecek belirli nüansları vardır.

Java uygulaması 4G'den daha az bellek kullanıyorsa, 64b makinelerde bile 32b JVM kullanmalıyız. Bunun nedeni, bu durumda bellek referanslarının yalnızca 32b olacağı ve bunları değiştirmenin 64b adreslerini değiştirmekten daha ucuz olacağıdır. Bu durumda, 64b JVM, OOPS (sıradan nesne işaretçileri) kullanıyor olsak bile daha kötü performans gösterecektir. OOPS kullanarak, JVM, 64b JVM'de 32b adresler kullanabilir. Bununla birlikte, altta yatan yerel referanslar hala 64b olacağından, bunları değiştirmek gerçek 32b referanslarından daha yavaş olacaktır.

Uygulamamız 4G'den fazla bellek tüketecekse, 32b referansları 4G'den fazla belleği ele alamayacağından 64b sürümünü kullanmamız gerekecek. Her iki sürümü de aynı makineye yükleyebiliriz ve PATH değişkenini kullanarak aralarında geçiş yapabiliriz.

Bu bölümde JIT Optimizasyonları hakkında bilgi edineceğiz.

Yöntem Satır İçi

Bu optimizasyon tekniğinde, derleyici işlev çağrılarınızı işlev gövdesi ile değiştirmeye karar verir. Aşağıda aynı şey için bir örnek var -

int sum3;

static int add(int a, int b) {
   return a + b;
}

public static void main(String…args) {
   sum3 = add(5,7) + add(4,2);
}

//after method inlining
public static void main(String…args) {
   sum3 = 5+ 7 + 4 + 2;
}

Bu tekniği kullanarak, derleyici makineyi herhangi bir işlev çağrısı yapma ek yükünden kurtarır (parametreleri yığına itip atmayı gerektirir). Böylece üretilen kod daha hızlı çalışır.

Yöntem satır içi işlemi yalnızca sanal olmayan işlevler (geçersiz kılınmayan işlevler) için yapılabilir. Bir alt sınıfta 'add' yönteminin üzerine basıldıysa ve yöntemi içeren nesnenin türü çalışma zamanına kadar bilinmiyorsa ne olacağını düşünün. Bu durumda, derleyici hangi yöntemi satır içi yapacağını bilemez. Ancak, yöntem 'son' olarak işaretlenmişse, derleyici, herhangi bir alt sınıf tarafından geçersiz kılınamayacağı için satır içi olabileceğini kolayca bilirdi. Son bir yöntemin her zaman sıralı olacağının hiçbir şekilde garanti edilmediğini unutmayın.

Ulaşılamayan ve Ölü Kod Eliminasyonu

Ulaşılamayan kod, herhangi bir olası yürütme akışıyla erişilemeyen koddur. Aşağıdaki örneği ele alacağız -

void foo() {
   if (a) return;
   else return;
   foobar(a,b); //unreachable code, compile time error
}

Ölü kod da erişilemez koddur, ancak derleyici bu durumda bir hata verir. Bunun yerine, sadece bir uyarı alıyoruz. Yapıcılar, işlevler, try, catch, if, while, vb. Gibi her kod bloğunun JLS'de (Java Dil Spesifikasyonu) tanımlanan erişilemez kod için kendi kuralları vardır.

Sabit Katlama

Sabit katlama konseptini anlamak için aşağıdaki örneğe bakın.

final int num = 5;
int b = num * 6; //compile-time constant, num never changes
//compiler would assign b a value of 30.

Bir Java nesnesinin yaşam döngüsü JVM tarafından yönetilir. Programcı tarafından bir nesne oluşturulduktan sonra, yaşam döngüsünün geri kalanı için endişelenmemize gerek yoktur. JVM, artık kullanılmayan nesneleri otomatik olarak bulacak ve hafızalarını yığından geri kazanacaktır.

Çöp toplama, JVM'nin yaptığı önemli bir işlemdir ve bunu ihtiyaçlarımıza göre ayarlamak, uygulamamıza büyük bir performans artışı sağlayabilir. Modern JVM'ler tarafından sağlanan çeşitli çöp toplama algoritmaları vardır. Hangi algoritmanın kullanılacağına karar vermek için uygulamamızın ihtiyaçlarının farkında olmamız gerekir.

C ve C ++ gibi GC olmayan dillerde yapabildiğiniz gibi Java'da bir nesneyi programlı olarak serbest bırakamazsınız. Bu nedenle, Java'da sarkan referanslara sahip olamazsınız. Bununla birlikte, boş referanslarınız olabilir (JVM'nin hiçbir zaman nesneleri depolamayacağı bir bellek alanına atıfta bulunan referanslar). Boş bir başvuru kullanıldığında, JVM bir NullPointerException oluşturur.

GC sayesinde Java programlarında bellek sızıntıları bulmak nadir olsa da, bunların meydana geldiğini unutmayın. Bu bölümün sonunda bir bellek sızıntısı yaratacağız.

Aşağıdaki GC'ler modern JVM'lerde kullanılmaktadır

  • Seri toplayıcı
  • Verim toplayıcı
  • CMS toplayıcı
  • G1 toplayıcı

Yukarıdaki algoritmaların her biri aynı görevi yerine getirir - artık kullanımda olmayan nesneleri bulmak ve yığın içinde kapladıkları belleği geri kazanmak. Buna naif yaklaşımlardan biri, her bir nesnenin sahip olduğu referansların sayısını saymak ve referans sayısı 0'a döner dönmez onu serbest bırakmak olacaktır (bu aynı zamanda referans sayımı olarak da bilinir). Bu neden saf? Dairesel bağlantılı bir liste düşünün. Düğümlerinin her birinin kendisine bir referansı olacaktır, ancak tüm nesneye herhangi bir yerden referansta bulunulmamaktadır ve ideal olarak serbest bırakılmalıdır.

JVM sadece belleği serbest bırakmakla kalmaz, aynı zamanda küçük bellek aynalarını daha büyük olanlarla birleştirir. Bu, bellek parçalanmasını önlemek için yapılır.

Basit bir not olarak, tipik bir GC algoritması aşağıdaki etkinlikleri gerçekleştirir -

  • Kullanılmayan nesneleri bulma
  • Yığın içinde işgal ettikleri hafızayı serbest bırakmak
  • Parçaları birleştirme

GC, çalışırken uygulama iş parçacıklarını durdurmalıdır. Bunun nedeni, çalışırken nesneleri hareket ettirmesi ve bu nedenle bu nesnelerin kullanılamamasıdır. Bu tür duraklamalar 'dünyayı durdurma duraklamaları' olarak adlandırılır ve bu duraklamaların sıklığını ve süresini en aza indirmek, GC'mizi ayarlarken hedeflediğimiz şeydir.

Bellek Birleştirme

Bellek birleştirme işleminin basit bir gösterimi aşağıda gösterilmiştir

Gölgeli kısım, serbest bırakılması gereken nesnelerdir. Tüm alan geri alındıktan sonra bile, yalnızca maksimum boyut = 75Kb olan bir nesne tahsis edebiliriz. Bu, aşağıda gösterildiği gibi 200Kb boş alanımız olsa bile

Çoğu JVM, yığını üç nesle böler - the young generation (YG), the old generation (OG) and permanent generation (also called tenured generation). Böyle düşünmenin arkasındaki sebepler nelerdir?

Ampirik çalışmalar, yaratılan nesnelerin çoğunun çok kısa ömürlü olduğunu göstermiştir -

Kaynak

https://www.oracle.com

Gördüğünüz gibi, zamanla daha fazla nesne tahsis edildikçe, hayatta kalan bayt sayısı (genel olarak) azalır. Java nesneleri yüksek ölüm oranına sahiptir.

Basit bir örneğe bakacağız. Java'daki String sınıfı değişmezdir. Bu, bir String nesnesinin içeriğini her değiştirmeniz gerektiğinde, tamamen yeni bir nesne oluşturmanız gerektiği anlamına gelir. Aşağıdaki kodda gösterildiği gibi bir döngüde dizide 1000 kez değişiklik yaptığınızı varsayalım -

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

Her döngüde, yeni bir dizgi nesnesi yaratırız ve önceki yineleme sırasında oluşturulan dizge işe yaramaz hale gelir (yani, herhangi bir referans tarafından referans alınmaz). Bu nesnenin T ömrü yalnızca bir yinelemeydi - GC tarafından hiçbir zaman toplanmayacaklar. Bu tür kısa ömürlü nesneler, yığının genç nesil alanında tutulur. Genç nesilden nesnelerin toplanma sürecine küçük çöp toplama adı verilir ve her zaman bir 'dünyayı durdur' duraklamasına neden olur.

Genç nesil doldukça, GC küçük bir çöp toplama işlemi yapar. Ölü nesneler atılır ve canlı nesneler eski nesle taşınır. Bu işlem sırasında uygulama konuları durur.

Burada böyle bir nesil tasarımın sunduğu avantajları görebiliriz. Genç nesil, yığının yalnızca küçük bir parçasıdır ve hızla doldurulur. Ancak işlemek, tüm yığını işlemek için geçen süreden çok daha az zaman alır. Dolayısıyla, bu durumda 'dünyayı durdur' duraklamaları, daha sık olmakla birlikte çok daha kısadır. Daha sık olsalar bile, her zaman daha uzun duraklamalar yerine daha kısa duraklamaları hedeflemeliyiz. Bunu, bu eğitimin sonraki bölümlerinde ayrıntılı olarak tartışacağız.

Genç nesil iki alana bölünmüştür - eden and survivor space. Eden koleksiyonu sırasında hayatta kalan nesneler hayatta kalan alana, hayatta kalanlar ise eski nesile taşınır. Genç nesil toplanırken sıkıştırılır.

Nesneler eski nesle taşınırken, sonunda dolar ve toplanıp sıkıştırılması gerekir. Farklı algoritmalar buna farklı yaklaşımlar getirir. Bazıları uygulama iş parçacığını durdurur (eski nesil genç nesile kıyasla oldukça büyük olduğu için uzun bir 'dünyayı durdur' 'duraklamasına yol açar), bazıları ise uygulama iş parçacığı çalışmaya devam ederken aynı anda yapar. Bu işleme tam GC adı verilir. Bu tür iki koleksiyonerCMS and G1.

Şimdi bu algoritmaları ayrıntılı olarak analiz edelim.

Seri GC

istemci sınıfı makinelerde (tek işlemcili makineler veya 32b JVM, Windows) varsayılan GC'dir. Tipik olarak, GC'ler çok iş parçacıklıdır, ancak seri GC değildir. Yığını işlemek için tek bir iş parçacığı vardır ve küçük bir GC veya büyük bir GC yaptığında uygulama iş parçacıklarını durdurur. JVM'ye bayrağı belirterek bu GC'yi kullanması için komut verebiliriz:-XX:+UseSerialGC. Farklı bir algoritma kullanmasını istiyorsak, algoritma adını belirtin. Eski neslin büyük bir GC sırasında tamamen sıkıştırıldığını unutmayın.

Verimlilik GC

Bu GC, 64b JVM'lerde ve çok CPU'lu makinelerde varsayılandır. Seri GC'den farklı olarak, genç ve eski nesli işlemek için birden çok iş parçacığı kullanır. Bu nedenle, GC aynı zamandaparallel collector. JVM'imize bu toplayıcıyı şu bayrağı kullanarak kullanması için komut verebiliriz:-XX:+UseParallelOldGC veya -XX:+UseParallelGC(JDK 8 sonrası için). Uygulama iş parçacıkları, büyük veya küçük bir çöp toplama işlemi yaparken durdurulur. Seri toplayıcı gibi, büyük bir GC sırasında genç nesli tamamen sıkıştırır.

Aktarım hızı GC, YG ve OG'yi toplar. Eden dolduğunda, toplayıcı canlı nesneleri ondan OG'ye veya hayatta kalan alanlardan birine (aşağıdaki diyagramda SS0 ve SS1) çıkarır. Ölü nesneler, işgal ettikleri alanı boşaltmak için atılır.

YG'nin GC'sinden önce

YG'nin GC'sinden sonra

Tam bir GC sırasında, aktarım hızı toplayıcısı tüm YG, SS0 ve SS1'i boşaltır. Operasyondan sonra OG yalnızca canlı nesneleri içerir. Yukarıdaki toplayıcıların her ikisinin de yığını işlerken uygulama iş parçacıklarını durdurduğuna dikkat etmeliyiz. Bu, büyük bir GC sırasında uzun "dünyayı durdur" duraklamaları anlamına gelir. Sonraki iki algoritma, daha fazla donanım kaynağı pahasına bunları ortadan kaldırmayı hedefliyor -

CMS Toplayıcı

"Eşzamanlı işaret taraması" anlamına gelir. İşlevi, eski nesli periyodik olarak taramak için bazı arka plan konuları kullanması ve ölü nesnelerden kurtulmasıdır. Ancak küçük bir GC sırasında, uygulama iş parçacıkları durdurulur. Ancak, duraklamalar oldukça azdır. Bu, CMS'yi düşük duraklamalı bir toplayıcı yapar.

Bu toplayıcı, uygulama iş parçacıkları çalıştırılırken yığın içinde tarama yapmak için ek CPU zamanına ihtiyaç duyar. Ayrıca, arka plan iş parçacıkları yalnızca yığını toplar ve herhangi bir sıkıştırma gerçekleştirmez. Yığının parçalanmasına neden olabilirler. Bu devam ederken, belirli bir süre sonra, CMS tüm uygulama iş parçacıklarını durduracak ve tek bir iş parçacığı kullanarak yığını sıkıştıracaktır. JVM'ye CMS toplayıcısını kullanmasını söylemek için aşağıdaki JVM bağımsız değişkenlerini kullanın -

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC” JVM argümanları olarak CMS toplayıcısını kullanmasını söyler.

GC'den önce

GC'den sonra

Koleksiyonun eşzamanlı olarak yapıldığını unutmayın.

G1 GC

Bu algoritma, yığını birkaç bölgeye bölerek çalışır. CMS toplayıcı gibi, küçük bir GC yaparken uygulama iş parçacıklarını durdurur ve uygulama iş parçacıklarını devam ettirirken eski nesli işlemek için arka plan iş parçacıklarını kullanır. Eski nesli bölgelere ayırdığı için, nesneleri bir bölgeden diğerine taşırken onları sıkıştırmaya devam ediyor. Bu nedenle parçalanma minimumdur. Bayrağı kullanabilirsiniz:XX:+UseG1GCJVM'nize bu algoritmayı kullanmasını söylemek için. CMS gibi, aynı zamanda öbeği işlemek ve uygulama iş parçacıklarını aynı anda çalıştırmak için daha fazla CPU zamanına ihtiyaç duyar.

Bu algoritma, birkaç farklı bölgeye bölünmüş daha büyük yığınları (> 4G) işlemek için tasarlanmıştır. Bu bölgelerin bir kısmı genç nesli, geri kalanı ise yaşlıları oluşturuyor. YG, geleneksel olarak kullanılarak temizlenir - tüm uygulama iş parçacıkları durdurulur ve eski nesil veya hayatta kalan alan için hala canlı olan tüm nesneler.

Tüm GC algoritmalarının yığını YG ve OG'ye böldüğünü ve YG'yi temizlemek için bir STWP kullandığını unutmayın. Bu süreç genellikle çok hızlıdır.

Son bölümde, çeşitli Generational GC'leri öğrendik. Bu bölümde, GC'nin nasıl ayarlanacağını tartışacağız.

Yığın Boyutu

Yığın boyutu, Java uygulamalarımızın performansında önemli bir faktördür. Çok küçükse, sık sık doldurulacak ve sonuç olarak, GC tarafından sık sık toplanması gerekecektir. Öte yandan, yığın boyutunu artırırsak, daha az sıklıkta toplanması gerekmesine rağmen, duraklamaların uzunluğu artacaktır.

Ayrıca, yığın boyutunu artırmanın temel işletim sistemi üzerinde ciddi bir cezası vardır. Sayfalamayı kullanarak, işletim sistemi uygulama programlarımızın gerçekte mevcut olandan çok daha fazla bellek görmesini sağlar. İşletim sistemi bunu, diskte biraz takas alanı kullanarak, programların etkin olmayan kısımlarını diske kopyalayarak yönetir. Bu bölümlere ihtiyaç duyulduğunda, işletim sistemi bunları diskten belleğe geri kopyalar.

Bir makinenin 8G belleğe sahip olduğunu ve JVM'nin 16G sanal bellek gördüğünü varsayalım, JVM aslında sistemde yalnızca 8G'nin mevcut olduğunu bilemez. Sadece işletim sisteminden 16G isteyecek ve bu belleği aldığında kullanmaya devam edecek. İşletim sistemi birçok veriyi içeri ve dışarı takas etmek zorunda kalacak ve bu sistem için büyük bir performans cezasıdır.

Ve sonra bu tür sanal belleğin tam GC'si sırasında meydana gelebilecek duraklamalar gelir. GC, toplama ve sıkıştırma için tüm yığın üzerinde hareket edeceğinden, sanal belleğin diskten çıkarılması için çok beklemesi gerekecektir. Eşzamanlı bir toplayıcı olması durumunda, arka plan iş parçacıkları verilerin takas alanından belleğe kopyalanması için çok beklemek zorunda kalacaktır.

İşte burada optimum yığın boyutuna nasıl karar vermemiz gerektiği sorusu geliyor. İlk kural, işletim sistemine hiçbir zaman gerçekte mevcut olandan daha fazla bellek talep etmemektir. Bu, sık sık değiştirme problemini tamamen önleyecektir. Makinede kurulu ve çalışan birden çok JVM varsa, hepsinin bir araya getirdiği toplam bellek talebi, sistemde mevcut olan gerçek RAM'den daha azdır.

İki bayrak kullanarak JVM tarafından yapılan bellek talebinin boyutunu kontrol edebilirsiniz -

  • -XmsN - İstenen ilk belleği kontrol eder.

  • -XmxN - Talep edilebilecek maksimum belleği kontrol eder.

Her iki işaretin de varsayılan değerleri, temeldeki işletim sistemine bağlıdır. Örneğin, MacOS'ta çalışan 64b JVM'ler için, -XmsN = 64M ve -XmxN = minimum toplam fiziksel belleğin 1G'si veya 1 / 4'ü.

JVM'nin iki değer arasında otomatik olarak ayarlanabileceğini unutmayın. Örneğin, çok fazla GC olduğunu fark ederse, -XmxN'nin altında olduğu ve istenen performans hedeflerine ulaşıldığı sürece bellek boyutunu artırmaya devam edecektir.

Uygulamanızın tam olarak ne kadar belleğe ihtiyacı olduğunu biliyorsanız, -XmsN = -XmxN ayarlayabilirsiniz. Bu durumda, JVM'nin yığının "en uygun" değerini bulmasına gerek yoktur ve bu nedenle, GC süreci biraz daha verimli hale gelir.

Nesil Boyutları

YG'ye yığının ne kadarını ayırmak istediğinize ve bunun ne kadarını OG'ye ayırmak istediğinize karar verebilirsiniz. Bu değerlerin her ikisi de uygulamalarımızın performansını aşağıdaki şekilde etkiler.

YG'nin boyutu çok büyükse, o zaman daha az sıklıkta toplanacaktır. Bu, OG'ye yükseltilen daha az sayıda nesne ile sonuçlanacaktır. Öte yandan, OG'nin boyutunu çok fazla artırırsanız, toplamak ve sıkıştırmak çok fazla zaman alır ve bu da uzun STW duraklamalarına neden olur. Dolayısıyla kullanıcının bu iki değer arasında bir denge bulması gerekir.

Bu değerleri ayarlamak için kullanabileceğiniz bayraklar aşağıdadır -

  • -XX:NewRatio=N: YG'nin OG'ye oranı (varsayılan değer = 2)

  • -XX:NewSize=N: YG'nin başlangıç ​​boyutu

  • -XX:MaxNewSize=N: YG'nin maksimum boyutu

  • -XmnN: Bu bayrağı kullanarak NewSize ve MaxNewSize'ı aynı değere ayarlayın

YG'nin başlangıç ​​boyutu, verilen formülle NewRatio değerine göre belirlenir -

(total heap size) / (newRatio + 1)

NewRatio'nun başlangıç ​​değeri 2 olduğundan, yukarıdaki formül YG'nin başlangıç ​​değerini toplam yığın boyutunun 1 / 3'ü olarak verir. NewSize bayrağını kullanarak YG'nin boyutunu açıkça belirterek bu değeri her zaman geçersiz kılabilirsiniz. Bu bayrağın herhangi bir varsayılan değeri yoktur ve açıkça belirtilmezse, YG'nin boyutu yukarıdaki formül kullanılarak hesaplanmaya devam edecektir.

Permagen ve Metaspace

Permagen ve meta alan, JVM'nin sınıfların meta verilerini tuttuğu yığın alanlarıdır. Boşluğa Java 7'de 'permagen', Java 8'de ise 'metaspace' olarak adlandırılır. Bu bilgiler derleyici ve çalışma zamanı tarafından kullanılır.

Aşağıdaki bayrakları kullanarak permajenin boyutunu kontrol edebilirsiniz: -XX: PermSize=N ve -XX:MaxPermSize=N. Metaspace'in boyutu aşağıdakiler kullanılarak kontrol edilebilir:-XX:Metaspace- Size=N ve -XX:MaxMetaspaceSize=N.

Bayrak değerleri ayarlanmadığında, permagen ve metaspace'in nasıl yönetildiği konusunda bazı farklılıklar vardır. Varsayılan olarak, her ikisinin de varsayılan bir başlangıç ​​boyutu vardır. Ancak metaspace, ihtiyaç duyulduğu kadar yığın işgal edebilirken, permagen varsayılan başlangıç ​​değerlerinden fazlasını işgal edemez. Örneğin 64b JVM, maksimum permajen boyutu olarak 82M yığın alanına sahiptir.

Metaspace, belirtilmediği sürece sınırsız miktarda bellek işgal edebileceğinden, yetersiz bellek hatası olabileceğini unutmayın. Bu bölgeler yeniden boyutlandırıldığında tam bir GC gerçekleşir. Bu nedenle, başlatma sırasında, yüklenen çok sayıda sınıf varsa, metaspace her seferinde tam bir GC ile sonuçlanacak şekilde yeniden boyutlandırmaya devam edebilir. Bu nedenle, ilk metaspace boyutunun çok düşük olması durumunda büyük uygulamaların başlaması çok zaman alır. Başlatma süresini kısalttığı için başlangıç ​​boyutunu artırmak iyi bir fikirdir.

Permaajen ve meta alan sınıf meta verilerini tutsa da kalıcı değildir ve alan nesnelerde olduğu gibi GC tarafından geri kazanılır. Bu, tipik olarak sunucu uygulamaları durumundadır. Sunucuya yeni bir dağıtım yaptığınızda, yeni sınıf yükleyiciler artık alana ihtiyaç duyacağından eski meta verilerin temizlenmesi gerekir. Bu alan, GC tarafından serbest bırakılır.

Bu bölümde Java'daki bellek sızıntısı kavramını tartışacağız.

Aşağıdaki kod, Java'da bir bellek sızıntısı yaratır -

void queryDB() {
   try{
      Connection conn = ConnectionFactory.getConnection();
      PreparedStatement ps = conn.preparedStatement("query"); // executes a
      SQL
      ResultSet rs = ps.executeQuery();
      while(rs.hasNext()) {
         //process the record
      }
   } catch(SQLException sqlEx) {
      //print stack trace
   }
}

Yukarıdaki kodda, yöntem çıktığı zaman bağlantı nesnesini kapatmadık. Böylece, fiziksel bağlantı, GC tetiklenmeden önce açık kalır ve bağlantı nesnesini erişilemez olarak görür. Şimdi, bağlantı nesnesindeki son yöntemi çağıracak, ancak uygulanamayabilir. Dolayısıyla, nesne bu döngüde çöp olarak toplanmayacaktır.

Uzak sunucu bağlantının uzun süredir açık olduğunu görene ve zorla sonlandırana kadar aynı şey bir sonraki işlemde de gerçekleşecektir. Böylece referansı olmayan bir nesne hafızada uzun süre kalır ve bu da sızıntıya neden olur.