JavaProgramlamaKıdemli Java Geliştirici

StackWalker API's tembel malzeme stratejisi, seçici yığın çerçevesi incelemesi sağlamak için ne kullanıyor ve bu, Throwable.fillInStackTrace'in anında anlık görüntüleme anlamlarından nasıl temelde farklıdır?

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

Cevap

Java 9'dan önce, yürütme yığınına programatik erişim sağlamak için ya bir Throwable nesnesi oluşturmak (bu, tüm yığın izini bir diziye hevesle yakaladı) ya da SecurityManager.getClassContext() metodunu kullanmak gerekiyordu (bu, güvenlik politikalarıyla kısıtlandı ve benzer şekilde pahalıydı). Bu yaklaşımlar, sadece en üst çerçeveye veya belirli bir çağırıcıya ihtiyacınız olduğunda bile, geliştiricilerin yığın yürüyüşünün tam maliyetini ödemeye zorladı ve performans açısından kritik kod yollarında çağrıcıya duyarlı API'lerin uygulanabilirliğini ciddi şekilde kısıtladı.

Hevesli yakalamanın temel sorunu, yığın derinliği ile ilişkili O(n) karmaşıklığı ve StackTraceElement dizilerinin zorunlu tahsisidir; bu, sıklıkla arama noktalarını inceleyen günlükleme çerçeveleri, serileştirme kitaplıkları ve hata ayıklama araçları üzerinde önemli GC baskısı oluşturur. Ayrıca, Throwable.fillInStackTrace gizli çerçeveleri (yerel yöntemler, yansıtma altyapısı) yakaladığı için, uygulama kodunun tipik olarak görmezden gelmek istediği, önceden materyalize edilmiş veriler üzerinde ek filtreleme yükü gerektirir. Bu hevesli gerçekleştirme, JVM'in uygulama tarafından asla incelenmeyen çerçeveleri optimize etmesini engeller.

StackWalker (Java 9'da tanıtıldı), yığın çerçevelerini yalnızca akış boru hattının son işlemi talep ettiğinde tembel bir şekilde materyalize eden Stream<StackFrame> soyutlamasını ortaya çıkarır; bu, Object tahsisi öncesinde VM düzeyinde çalışan predikat tabanlı filtreleme ile birleştirilmiştir. Uygulama, yığın çerçevesini çerçeve bazında geçirerek iç çerçeve geçişi ilkelinden yararlanır ve kullanıcı tarafından sağlanan Predicate<StackFrame> yanlış döndüğünde anında durur; böylece atlanan çerçeveler için tahsisat önlenir ve denetlenen çerçeve sayısı k olurken toplam derinlik yerine O(k) karmaşıklığı sağlanır. Throwable aksine, oluşturulduğu anda sabit bir anlık görüntü oluşturan StackWalker, akış geçişi anında iş parçacığının yığınının tam durumunu yansıtan bir canlı görünüm sağlar.

Hayattan bir durum

Büyük bir RPC çerçevesi geliştirirken her gelen isteğin, argümanları serileştirmeden önce çağıran sınıfın onaylı bir modülden geldiğini doğrulaması gerektiğini hayal edin. İlk uygulama, hemen çağıranı belirlemek için new Throwable().getStackTrace() kullandı, ancak 10.000 eşzamanlı istekle yük testi yapıldığında, hizmet ciddi gecikme dalgalanmaları ve sık sık OutOfMemoryError'larla karşılaştı, çünkü devasa iz dizileri tahsis ediliyordu. Profiling, tahsis edilen baytların neredeyse %40’inin bu güvenlik kontrollerinden kaynaklandığını ortaya koydu, bu da yaklaşımın üretim dağıtımı için sürdürülebilir olmadığını gösteriyordu.

Ekip ilk olarak SecurityManager.getClassContext() kullanmayı düşündü; bu, dize ayrıştırma yükü olmadan doğrudan sınıf bağlamı dizisini döndürür. Bu, yığın izlerini doldurma maliyetinden kaçınmayı sağlar, ancak yine de SecurityManager'ın yükseltilmiş ayrıcalıklarla kurulmasını gerektirir ve sıkı güvenlik politikaları olan ortamlarda dağıtımını zorlaştırır; ayrıca, ihtiyaç olmaksızın tüm sınıf dizisini yakalar ve O(n) karmaşıklığı sorununu çözmez. Ayrıca, bu yaklaşım günümüz Java sürümlerinde kaldırılması için kaldırılmıştır, bu da onu kod tabanı için uzun vadede kötü bir yatırım haline getirir.

Başka bir alternatif, çalışma zamanında iç gözlemden tamamen kaçınmak için başlangıçta sınıf yol taraması ile doldurulmuş bir statik Map<Class<?>, Boolean> sürdürmeyi içeriyordu. Bu strateji, her istek başına tahsisi ortadan kaldırır ve O(1) arama performansı sunar, ancak Proxy veya MethodHandle aracılığıyla dinamik kod oluşturmayı dikkate almaz; bu, çalışma zamanı başlangıç zamanında bilinmeyen meşru çağıran sınıflara yol açar ve yanlış güvenlik redlerine neden olarak karmaşık önbellek geçersiz kılma mantığı gerektirir. Ayrıca, her olası çağıran sınıfı önbelleğe almak, binlerce yüklü sınıfın olduğu büyük uygulamalarda bellek ayak izinin aşırı olması anlamına gelir.

Mühendisler sonunda StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk(stream -> stream.skip(2).findFirst().map(StackFrame::getDeclaringClass).orElse(null)) seçeneğini seçti; bu, yalnızca ilk iki çerçeveyi tembel bir şekilde değerlendirir ve ara diziler oluşturmadan sınıf referansını döndürür. Bu yaklaşım, dinamik olarak oluşturulan sınıfları önceden kayıt olmadan doğru bir şekilde işlemekle birlikte, düşük kod karmaşıklığı ile optimal performans dengesi sağladığı için tercih edildi ve tamamen standart API'ler içinde çalışarak güvenlik yöneticisi bağımlılıklarını ortadan kaldırdı; bu da Java'nın en az ayrıcalıklı güvenlik modellerine doğru devam eden evrimi ile ileriye dönük uyumluluğu sağlar.

Dağıtımdan sonra, çağıran doğrulama için her istekteki yük, yaklaşık 450 bayt tahsisat ve 2 mikro saniyeden neredeyse sıfır tahsise ve 20 nanosaniyeye düştü, güvenlik sıcak yolundan GC baskısını etkili bir şekilde ortadan kaldırdı. Yük testleri, hizmetin gecikme dalgalanmaları olmadan tüm 10.000 eşzamanlı istek yükünü sürdürebileceğini doğruladı ve yığın dökümleri, StackTraceElement dizisi birikiminin yokluğunu doğruladı. Çözüm, uygun filtreleme predikatları ile yapılandırıldığında, yansıtıcı ve MethodHandle tabanlı çağrılar da dahil olmak üzere çeşitli çağrı istiflerinde sağlam ve güvenilir oldu.

Adayların genellikle atladığı noktalar

StackWalker neden yürütme yöntemi içinde yalnızca bir kez geçilebilir bir Akış döndürür ve bu akışı birden çok çağrı arasında önbelleklemeye çalıştığınızda hangi eşzamanlılık tehlikesi ortaya çıkar?

StackWalker.walk tarafından döndürülen Stream, yalnızca walk geri çağırma yürütme süresince geçerli olan, mevcut iş parçacığının yığınının canlı, değişken bir görünümü ile desteklenir. Geri çağırma döndüğünde, JVM yerel çerçeve tamponunu serbest bırakır, böylece önbelleğe alınmış akış referansı kullanılamaz hale gelir ve sonraki erişimde IllegalStateException fırlatır. Adaylar genellikle StackWalker'ın Throwable gibi bir anlık görüntü oluşturduğunu varsayıyor, ancak aslında iş parçacığının mevcut yürütme durumu üzerinde geçici bir görüş sağladığından, akış başka bir iş parçacığına geçirilirse veya bir alanda saklanırsa, eşzamanlı yığın değişiklikleri tutarsız çerçeve durumlarını ortaya çıkarabilir veya VM'yi çökertir; bu, katı kapsam tanımlamaları olmadan mümkün olmayabilir.

RETAIN_CLASS_REFERENCE seçeneği, iç çerçeve temsilini nasıl değiştirir ve bunun yokluğu, çerçeve incelemesi sırasında bağlantı hatalarıyla birlikte Class.forName kullanılmasını neden zorunlu kılar?

RETAIN_CLASS_REFERENCE olmadan, StackWalker yalnızca sınıf adını, yöntem adını ve satır numarasını saklayarak optimizasyon yapar; bu, Class nesnesini çözmeyi gerektirmediği için; bu, sınıf yükleme veya başlatma uyarısını tetikleyebilir. Ancak, bu, StackFrame.getDeclaringClass()'ın desteklenmemesi anlamına gelir ve çağırıcıların Class.forName(frame.getClassName()) kullanması gerekir; bu, yürüyen çerçevenin sınıf yükleyicisi çağıranın yükleyicisi değilse ClassNotFoundException veya NoClassDefFoundError fırlatabilir. RETAIN_CLASS_REFERENCE belirtildiğinde, VM yürüyüş sırasında Class nesnelerini kilitler; bu, erişilebilir kalmalarını sağlar ve arama maliyetini ortadan kaldırır, ancak bu, yürüyen için yüklenemeyen sınıfları referans alabilecek yansıtıcı çerçevelerin atlanmasına engel olur.

StackWalker.walk ile Thread.getStackTrace arasında yerel yöntemlerin ve yansıtma stub'larının dahil edilmesi ile ilgili hangi ince davranış farklılığı vardır ve SHOW_HIDDEN_FRAMES seçeneği, MethodHandle çağrılarıyla nasıl etkileşir?

Thread.getStackTrace ve Throwable.getStackTrace, temiz bir uygulama görünümü sunmak için varsayılan olarak gizli uygulama çerçevelerini (örneğin, MethodHandle adaptörleri, yansıtma köprüleri ve yerel metod stub'ları) filtreler. Varsayılan seçeneklerle StackWalker de bu çerçeveleri gizler, ancak SHOW_HIDDEN_FRAMES sağlar ve böylece MethodHandle bağlantı çerçevelerini içeren, yığın boyunca geçerken izinleri doğrulamak için kritik olan tam fiziksel yığını açığa çıkarır. Adaylar genellikle SHOW_HIDDEN_FRAMES'ı atlamanın, eğer çağrı zinciri dolaylılık içeriyorsa gerçek güvenlik duyarlı çağıranı atlayabileceğini; oysaki dahil etmenin, yanlış bir biçimde çağıranı tanımlamamak için sentetik çerçeveleri açıkça filtrelemek için predikat mantığı gerektirdiğini tanımak için başarısız olurlar.