Java 6'dan önce, HotSpot JVM her nesneyi ömrüne bakılmaksızın yığın üzerinde tahsis etti. Server Compiler (C2)'nin tanıtılmasıyla, JVM Kaçış Analizi (EA) adı verilen, bir nesne referansının mevcut yöntem veya iş parçacığını aşıp aşmadığını belirleyen bir statik analiz tekniğine sahip oldu. EA, bir nesnenin yönteme özgü kalacağını kanıtlarsa, Ölçek Değişimi saldırgan bir optimizasyon olarak etkinleşir.
Bu optimizasyon, nesneyi bileşen ölçek alanlarına ayırarak yığın yerine yığındaki CPU kayıtlarına tahsis edilmesini sağlar. Bu, tahsis maliyetini ve ilişkili GC baskısını tamamen ortadan kaldırır. Ancak, optimizasyon senkronize bloklarla karşılaştığında katı bir sınıra çarpar çünkü izleyiciler, çatışma kuyruklarını yönetmek için yığında kararlı bir nesne başlığı gerektirir.
public int calculate() { Point p = new Point(1, 2); // Ölçek değişimi uygulanabilir return p.x + p.y; }
Saniyede milyonlarca piyasa olayını işleyen yüksek frekanslı bir ticaret motorunda, sipariş eşleme mantığı fiyat eğimlerini hesaplamak için milyonlarca geçici Koordinat nesnesi üretti. Bu tahsisler, genç nesil koleksiyonlarını sık sık tetikledi ve zirve oynaklığı sırasında kabul edilemez mikro saniye düzeyinde duraksamaya neden oldu. Mühendislik ekibi, bu tahsisleri ortadan kaldırmak için kod okunabilirliği veya güvenlik garantilerinden ödün vermeden bir çözüm bulmak zorundaydı.
İlk yaklaşım, hesaplamalar arasında Koordinat örneklerini yeniden kullanmak için ThreadLocal kullanarak bir nesne havuzu oluşturmayı düşünmekti. Bu, yığın üzerindeki döngüyü azalttı, ancak birden fazla iş parçacığı komşu ThreadLocal harita girişlerine eriştiğinde önbellek satırı çatışması oluşturarak iş parçacığı sonlandırma temizliğini yönetmek için karmaşık bir mantık gerektirdi. Ayrıca, senkronize edinme mantığı, her operasyonda ölçülebilir nanosenye ek maliyet ekleyerek performans kazançlarını geçersiz kıldı.
Başka bir alternatif, koordinatları yığın dışı bellek üzerinden ByteBuffer veya Unsafe kullanarak depolamaktı ve GC'den tamamen kaçınmak için bayt ofsetlerini manuel olarak yönetmekti. Bu yaklaşım yığın üzerindeki baskıyı ortadan kaldırdı ancak tür güvenliğinden ödün vermek, manuel sınır kontrolü gerektirmek ve yığın dökümleri koordinat durumunu artık açığa çıkarmadığı için hata ayıklamayı karmaşık hale getirmek gerekti. Bakım yükü, kritik bir ticaret sistemi için çok yüksek bulundu.
Ekip, nihayetinde Koordinat sınıfını değiştirerek değişmez hale getirmeye ve tüm hesaplama yöntemlerinin senkronizasyon içermesini sağlamaya karar verdi, bu da C2'nin ölçek değişiminin çalışmasını sağladı. -XX:+PrintEscapeAnalysis ile çalıştırarak optimizasyonu doğruladılar ve günlüklerde "Ölçek değişimi yapıldı" mesajlarını onayladılar. Bu, daha önce yığın tahsisini zorlayan savunmacı kopyalamaların kaldırılmasını gerektirdi, ancak iş parçacığına özgü hesaplamalar için gereksizdi.
Ayrıca, dağıtım, duraksama sürelerini %40 azaltarak ve verimliliği %15 artırarak, sağlam çalışma sırasında en sıcak yol için sıfır tahsis sonuçladı. Kod, güvensiz yapılar olmadan saf Java olarak kaldığı için, çözüm tam hata ayıklanabilirliği ve JVM sürümleri arasında taşınabilirliği korudu. Bu deneyim, derleyici optimizasyonlarını anlamanın genellikle manuel bellek yönetiminden daha üstün olduğunu gösterdi.
Bir nesne başka bir nesnenin alanına atandığında, o kapsayıcının asla kaçmadığı durumlarda neden ölçek değişimi başarısız olur?
Kaçış Analizi, yöntem düzeyinde ayrıntı ile çalışır ve her zaman küresel alan görünürlüğünü kanıtlayamaz. Bir nesne putfield bytecode aracılığıyla bir alana kaydedildiğinde, derleyici referansın kaçabileceğini varsayarak, dış nesnenin tüm olası kod yollarında yığın ile sınırlı kaldığını kanıtlamadıkça, ihtiyatlıdır. Bu kısıtlama, derleyicinin alanın diğer iş parçacıkları veya yöntem tekrarları tarafından erişilmeyeceğinden emin olamaması nedeniyle ölçek değişiminin engellenmesine neden olur, bu da bellek tutarlılığını sağlamak için yığın tahsisini zorunlu kılar.
finalize() yöntemi bir sınıf için ölçek değişimini tamamen nasıl devre dışı bırakır?
Finalizer mekanizması, nesnelerin, belirli bir sistem iş parçacığı tarafından izlenen küresel bir referans kuyruğuna kaydolmasını gerektirir. Bu kayıt, nesne inşası sırasında bir yerel çağrı aracılığıyla gerçekleşir ve bu hemen nesne referansını yığına yayınlayarak yerel kapsamdan kaçmasına neden olur. Çünkü ölçek değişimi, nesnenin asla yığın varlığı olarak oluşmamasını gerektirir, Object.finalize() yöntemini geçersiz kılan herhangi bir sınıf, bu optimizasyondan koşulsuz olarak hariç tutulur, finalizer boş olsa bile.
C1 derleyicisi tarafından derlenmiş yöntemlerde ölçek değişimi olabilir mi?
Ölçek değişimi, C1'den ziyade C2 (Server) Derleyicisi'ne özeldir çünkü C1 hızlı derleme hızını derin statik analize tercih eder. C1 yalnızca sabit katlama ve iç içe alma gibi temel optimizasyonlar gerçekleştirir ve nesne bağlılığını kanıtlamak için gereken karmaşık Kaçış Analizi çerçevesine sahip değildir. Sonuç olarak, 1 ile 3 arasındaki derleme seviyelerinde kalan yöntemlerde kısa ömürlü nesneler her zaman yığın tahsislerine neden olacak ve C2 seviye 4 derlemesi tamamlanmadan JVM ısınma sırasında tahsis zirveleri oluşturacaktır.