JavaProgramlamaKıdemli Java Geliştirici

Derleyici, çağrı yeri ile ilgili yöntem tanımlarını geçersiz kılmayı sağlayan polymorphic signature yöntemlerini tanımak için hangi özel JVM düzeyindeki sözleşmeyi kullanır?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

Sorunun tarihçesi

Java 7'de JSR 292 aracılığıyla invokedynamic'in tanıtılması, JVM üzerinde dinamik dil uygulamalarını desteklemek için MethodHandle API'sını getirdi. Zorluk, MethodHandle.invoke'nin, binlerce aşırı yük bildirmeden herhangi bir argüman türü ve dönüş türü kombinasyonunu kabul etmesi gerektiğiydi. JVM mimarları bunu, java.lang.invoke paketindeki @PolymorphicSignature anotasyonu ile içsel olarak işaretlenmiş polymorphic signature yöntemleri kavramını tanıtarak çözdü.

Problem

Standart Java yöntem çağrısı, derleyicinin, yöntemin beyan edilen imzasıyla tam olarak eşleşen belirli bir yöntem tanımını referans alan bir invokevirtual (veya benzeri) talimatı üretmesini gerektirir. Eğer MethodHandle.invoke Object... argüman alacak şekilde bildirilseydi, her çağrı yeri kutulama (boxing) ve dizi tahsisi gerektirecekti, bu da performans hedeflerini bozacaktı. Tersine, her olası imza kombinasyonu için aşırı yük bildirmek imkânsızdır ve bu, Class dosyasını sonsuz bir şekilde şişirecektir.

Çözüm

JVM, @PolymorphicSignature ile anotelenmiş yöntemleri özel şekilde ele alır. Derleyici böyle bir yönteme yapılan bir çağrıyı gördüğünde, bildirilen imzayı görmezden gelir ve bunun yerine, çağrı yerindeki argümanların ve dönüş türlerinin silinmiş (erased) türleri ile tam olarak eşleşen bir invokevirtual talimatı üretir. Bu, MethodHandle.invokeExact'in kaynak kodunda (Object)Object alır gibi görünmesine ancak belirli bir çağrı yerinde (String)int olacak şekilde derlenmesine olanak tanır. JVM daha sonra bu çağrıyı doğrudan hedef yöntemin giriş noktasına bağlar, adaptör yükü olmaksızın.

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class PolymorphicExample { public static void main(String[] args) throws Throwable { MethodHandle handle = MethodHandles.lookup() .findVirtual(String.class, "length", MethodType.methodType(int.class)); // Derleyici, bytecode'da invokeExact (Object)Object olarak bildirilmiş olmasına rağmen (String)int tanımı ile invokevirtual üretir int result = (int) handle.invokeExact("hello"); System.out.println(result); // Çıktı: 5 } }

Gerçek hayattan bir durum

Problem tanımı

Finansal tick verileri için yüksek verimli bir olay işleme çerçevesi oluştururken, gelen mesajları kaydedilmiş işleyicilere yansıtmak için, kısmi yansıma benzeri bir esneklikle ancak sıfır tahsis yüküyle çalışmamız gerekiyordu. Her işleyici yöntemi farklı imzalara sahipti - bazıları long zaman damgaları alırken, diğerleri BigDecimal fiyatları alıyordu - bu, kutulama (boxing) gerektirmeden genel yansıtmayı zorlaştırıyordu.

Değerlendirilen farklı çözümler

Dinamik bytecode üretimi, kayıt zamanında her işleyici imzası için proxy sınıfları oluşturmak için ASM veya ByteBuddy kullanmayı içeriyordu. Bu yaklaşım, ısınma sonrası neredeyse yerel performans sunmasına rağmen önemli miktarda Metaspace tüketiyor ve sınıf yükleme ve JIT derleme sırasında uygulama başlatma gecikmesini birkaç saniye artırıyordu. Ayrıca, oluşturulan kodu hata ayıklamayı karmaşık hale getirdi.

Yöntem tutucularıyla yansıma, standart Method.invoke'yi kullanarak ardından unreflect ile MethodHandle'leri elde etmeyi içeriyordu. Uygulaması daha basit olmasına rağmen, bu, ilkel argümanlar için kutulama maliyetleri getirdi ve HotSpot'un yansıtıcı katman üzerinden içermesini (inlining) engelledi. Performans testleri, doğrudan çağrılara göre 10-15 kat daha yavaş bir yansıtma sağladı, bu da gecikme gereksinimlerimizi ihlal ediyordu.

Polymorphic signature kullanımı, invokeExact'i çağırmadan önce argümanları tam olarak beklenen türlere dikkatlice dönüştürmeyi gerektiriyordu. Bu, derleyicinin her çağrı yeri için imzaya özgü invokevirtual talimatları üretmesini sağladı, bu da MethodHandle'in bir tür işlev işaretçisi olarak işlenmesini etkili hale getirdi. Bunun karşılığında, derleme zamanında tür titizliği gerektiriyordu - işleyici imzalarını kayıt sırasında doğrulamamız gerekti, aksi takdirde tür güvenliğini sağlamak için kod derlenmeyecekti.

Seçilen çözüm ve nedeni

Polymorphic signature yaklaşımını, bir kayıt zamanı doğrulama katmanı ile birleştirerek seçtik. Tam olarak MethodHandle imzalarını eşleştiren hafif adaptör lambdaları (kullanarak LambdaMetafactory ve invokedynamic) oluşturarak, doğrudan çağrı performansını elde ettik ve tür güvenliğini koruduk. JVM, MethodHandle üzerinden gerçek işleyici yöntemine içerebildiği için, tüm çağrı yükünü ortadan kaldırdı.

Sonuç

Sistem, alt mikro saniye gecikmeyle saatte 2,5 milyon olayı işledi ve el yazısı ile yazılmış yönlendirme kodu performansına ulaştı. GC baskısı, yansıma tabanlı prototipe göre %98 düştü, çünkü ilkel argümanlar artık çağrı yolu sırasında kutulama gerektirmedi. Çözüm, tür hatalarının çalışma zamanı yerine derleme zamanında yakalandığı için sürdürülebilir kalmaya devam etti.

Adayların sıkça kaçırdığı noktalar

Neden MethodHandle.invoke() tür dönüşümüne izin verirken invokeExact() kesin imza eşleşmesi gerektiriyor, her ikisi de polymorphic signature'lara sahip olmasına rağmen?

Her iki yöntem de @PolymorphicSignature anotasyonunu taşır, ancak invokeExact, JVM düzeyinde sıkı imza kontrolü yapar. Derleyici, invokeExact için invokevirtual talimatını üretirken, çağrı yerindeki tam silinmiş türleri kullanır. Daha sonra JVM, bu türlerin hedef MethodType ile tam olarak eşleştiğini doğrular. Tersine, invoke (Exact olmadan) çağrı yeri türlerini hedef türle uyumlu hale getirmek için MethodHandle.asType adaptörleri ile birlikte bir mantık içerir; bu da kutulama, kutudan çıkarma ve ilkel dönüşümleri gerçekleştirir. Bu uyarlama, yansıma katmanında değil, MethodHandle uygulamasında gerçekleşir, bu da invoke'yi daha esnek hale getirir ancak adaptör zincir yükü nedeniyle yavaşlayabilir.

JVM, polymorphic signature yöntemleri keyfi yöntem tanımlamalarına izin verse bile, tür güvenliği ihlallerini nasıl önlüyor?

JVM, kaynak düzeyinde tür güvenliğini sağlamak için Java derleyicisine güveniyor. @PolymorphicSignature yalnızca java.base modül sınıflarına (örneğin MethodHandle ve VarHandle) kısıtlandığından, kullanıcı kodu yeni polymorphic yöntemler bildiremez. Derleyici, yalnızca, çağrı yerinde beklenen imzaya karşı argüman türlerini doğrulayabildiği yerlerde polymorphic çağrılara izin verir. invokeExact için derleyici, oluşturulan tanımın programcının niyetine uygun olduğundan emin olmak için örtük dönüşümler ekler. JVM, derleyicinin bu doğrulamayı gerçekleştirdiğini düşünüyor, bu nedenle çağrılar sırasında çalışma zamanı tanım kontrollerini atlayabilir, böylece sıfır yük ile çalışma güvenliğini de derleme zamanı kısıtlamalarıyla sağlayabilir.

Neden polymorphic signature yöntemleri, yığın izlerinde ve hata ayıklamalarda Object türlerine siliniyormuş gibi görünüyor, ama belirli ilkel türlerle çalıştırılıyor?

javac derleyicisi, bu yöntemler için class dosyasında @PolymorphicSignature özniteliğini çıkarır. JVM, böyle bir yönteme bir çağrıyı çözdüğünde, çağrı yerinin sabit havuz girişindeki tanımı için bildirilen tanımı değiştirmek üzere önlem alır. Bu, gerçek bytecode yürütmesinin belirli türleri (int, long, vb.) kullandığı anlamına gelir, ancak Class nesnesindeki yöntemin meta verisi, yansıma amaçları için bildirilen imzayı (genellikle (Object...)Object) korur. Sonuç olarak, yığın izleri silinmiş şekli gösterir, çünkü Throwable.fillInStackTrace, yöntemin meta verisinden gelen sembolik tanımı kullanır; dinamik tanım, gerçek çağrı sırasında kullanılanıdır. Bu ayrım, geliştiricilerin hata ayıklayıcılarda kesin parametre türlerini göreceklerini beklemeleri nedeniyle kafalarını karıştırır.