パフォーマンス、リフレクション、およびjava.lang.Booleanに関する架空の物語

かつて、寒い冬の季節(3月は庭にいたのですが)に、私は(あなたが考えたことではなく、ヒープダンプと呼ばれるものの)ヒープを掘らなければなりませんでした。 VisualVMを発見し、目的のファイルを開いてOQLコンソールに切り替えました。 裁判所と事件の間、私の注意は箱から出して利用できる要求に引き付けられました。 特に印象的だったのは、「ブール値が多すぎます」というタイトルです。 彼の説明では、白の英語は次のように述べています。


ヒープにBooleanのインスタンスが2つ以上あるかどうかを確認します(Boolean.TRUEとBoolean.FALSEのみが必要です)。

感じますか? だから私はインスピレーションを受けました。


Javaが長い間独立して単純型をラッパーでラップすることができた場合、またはその逆の場合、余分な「大きな」 Booleanどこから来るのでしょうか。 コードが正しく記述されている場合、オブジェクトへのbooleanキャストはすべて、 java.lang.Booleanクラスに最初にアクセスしたときに作成されたBoolean.TRUE/Boolean.FALSEを使用します。 私が気づいたのは、このことからです。


 select toHtml(a) + " = " + a.value from java.lang.Boolean a where objectid(a.clazz.statics.TRUE) != objectid(a) && objectid(a.clazz.statics.FALSE) != objectid(a) 

驚いたことに、それを実行すると、 jlBooleanクラスの多くの個別のオブジェクトが見つかりました。 山はその起源について何も言わなかったので、私はそれらがどこから来たのかを知りたかった。 メモリからのプロファイリングは興味深い画像を示しました。新しいBoolean-絶えず出現し、蓄積され、しばらくしてからGCの口に消えました。 ある特定の時点で、彼らのスコアは数万になり、約1 MBのメモリを占有しました。



厳密に言えば、リークは発生せず、すぐにクリーニングされたため、問題はありませんでした。最近の1 MBは何ですか? しかし、新しいオブジェクトが出現するメカニズム自体が興味深いので、掘り始めました。


最初に、クラスBooleanオブジェクトを取得する方法を見てみましょう。 JDKは次の機能を提供します。


 /*1*/ Boolean b1 = new Boolean(true); //@Deprecated   Java 9 /*2*/ Boolean b2 = new Boolean("true"); //@Deprecated   Java 9 /*3*/ Boolean b3 = true; /*4*/ Boolean b4 = Boolean.valueOf(true); /*5*/ Boolean b5 = Boolean.valueOf("true"); /*6*/ Boolean b6 = Boolean.parseBoolean("true"); 

2つの違いは何ですか? 最初のメソッドと2番目のメソッドのみが新しいオブジェクトを返します(コンストラクターのため)。 アセンブリ中の3番目のメソッドは4番目のメソッドに削減され、最後の2つのメソッドと同様に、プレゼンスからBoolean.FALSE/Boolean.TRUEを返します。


そのため、多くの同一の(コンテンツ内の)オブジェクトが出現する理由は、 Boolean.valueOfを呼び出すのではなく、コンストラクターを直接呼び出すことで、単純なbooleanをラッパーでラップするためです。 最初の疑いは図書館の開発者に降りかかった。 さて、可能性のあるパンクを見つけてみましょう。 接続された依存関係のソースの検索(Ideaの開発者のおかげ)では疑わしいものは何も明らかにされなかったので、コンストラクターでデバッガーとして立ち、曲線が導くところに立つ必要がありました。


最初のヒットは推測を確認しました:それは反射、特に注釈を処理するための使用の匂いがしました。 コードを考慮してください:


 @Transactional(readOnly = true) public class MyService { } 

実行中、リフレクションは@Transactionalプロパティ(この場合はreadOnly )を読み取るために使用されます。 これは次のように発生します(Spring Core 5.0.4.RELEASE):



チェーンを上に移動すると、ソースを読み取ることができるsun.reflect.DelegatingMethodAccessorImplますが、その後、不可解なGeneratedMethodAccessor13が始まります。 また、デバッガーによると、このクラスはsun.reflectパッケージにもありますが、Ideaからはそのコードは使用できず、名前自体がクラスがその場で作成されたことを示唆しています。 そして、最終的にBoolean(boolean value)コンストラクターをinvoke()はまさに彼のinvoke()メソッドです。


物事は複雑になります:今、あなたは何らかの方法でこのメソッドのコードを取得する必要があります。 私はこの問題をすばやく解決できなかったので、別の方法で行かなければなりませんでした。コード自体を取得できないため、コードの作成方法を確実に明らかにすることができます。 これを行うには、 booleanを返すメソッドのリフレクション呼び出しで簡単なエクスペリエンスを設定します。


 import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { int invocationCount = 20; Object[] booleans = new Object[invocationCount]; Method method = Main.class.getMethod("f"); for (int i = 0; i < invocationCount; i++) { booleans[i] = invoke(method); } } public static Object invoke(Method method) throws Exception { return method.invoke(null); } public static boolean f() { return false; } } 

ところで、 jlBooleanコンストラクターからjlBooleanを削除しませんでしたか? ただし、この時点でサイクルの最初の16パスの間のみ、デバッガーは停止しません! 繰り返しますが、 method.invoke(null)実行するたびに新しいオブジェクト (つまり、 booleans[i-1] != booleans[i] method.invoke(null)が返され、このオブジェクト自体のコンストラクターは呼び出されません。


最初の16回のパスのいずれかでDelegatingMethodAccessorImpl.invoke()内で停止して先に進むと、コールチェーンに以前に欠落していたクラス、つまりsun.reflect.NativeMethodAccessorImplがあることがsun.reflect.NativeMethodAccessorImplます。



ここにあります:


 class NativeMethodAccessorImpl extends MethodAccessorImpl { private final Method method; private DelegatingMethodAccessorImpl parent; private int numInvocations; NativeMethodAccessorImpl(Method method) { this.method = method; } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { // We can't inflate methods belonging to vm-anonymous classes because // that kind of class can't be referred to by name, hence can't be // found from the generated bytecode. if (++numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); } return invoke0(method, obj, args); } void setParent(DelegatingMethodAccessorImpl parent) { this.parent = parent; } private static native Object invoke0(Method m, Object obj, Object[] args); 

コンストラクター呼び出しが表示されなかった理由に対する質問の答えは次のとおりです。代わりに、VMの腸のどこかにオブジェクトを作成するプラットフォーム依存のinvoke0()メソッドがinvoke0()ます。 同じコードは、17回目のパスで、コンストラクターが呼び出しチェーンに表示され、 NativeMethodAccessorImpl消える理由を説明します: f()メソッドの呼び出し回数がReflectionFactory.inflationThreshold()によって返される値を超えた後(JDK 8/9/10/11の場合は、 15)、その場でMethodAccessorGeneratorはその中間体を作成します。これは、 MethodAccessorImplオブジェクトの形式でDelegatingMethodAccessorImpl-よりも高いレベルに渡されます。


17番目のパッセージから開始して、通常の画像を確認します( MethodAccessorImpl新しく作成された実装MethodAccessorImplされていMethodAccessorImpl )。



したがって、新しいオブジェクトを返す2つの場所が発見されました:ネイティブメソッドNativeMethodAccessorImpl.invoke0()と、 new MethodAccessorGenerator().generateMethod()を使用してオンザフライで作成されたコードnew MethodAccessorGenerator().generateMethod() 。 抵抗が最も少ない道をたどり、今のところJavaの側に残ります。 var123 (アプリケーションがビルドされたJDK 8の場合)、コンパイルされたクラス(rt.jarから)にしかアクセスできず、逆コンパイルでは、変数名の代わりにvar123使用して、説明なしで判読できない偽のソースをvar123します。リポジトリ。


ソースコード MethodAccessorGeneratorと、すべてがMethodAccessorGenerator場所に配置されます。ここでバイトコードが作成されます(はい、元の形式、つまりバイト配列の形式のバイトコードです)。 私たちにとって重要なメソッドはemitInvoke()と呼ばれ、その中に必要なものを見つけることができます


 if (!isConstructor) { // Box return value if necessary if (isPrimitive(returnType)) { cb.opc_invokespecial(ctorIndexForPrimitiveType(returnType), typeSizeInStackSlots(returnType), 0); } else if (returnType == Void.TYPE) { cb.opc_aconst_null(); } } 

663行目:いわゆる、校正を見落としていました。 valueOf()を呼び出して単純な戻り値をラップする代わりに、コンストラクター呼び出しを入力しました。 明らかに、これはinvokestatic呼び出しをinvokestaticに置き換えるinvokespecialがあり、コンストラクタの代わりにファクトリメソッドをinvokestatic invokespecialがあるのはビジネスです。


悲しいかな、ソースに精通している さくらんぼ 「ナイン」は、(非常に突然)私だけがそれほど頭が良いわけではないことを示しました。そして、この問題で栄誉を得ることはありませ


 if (!isConstructor) { // Box return value if necessary if (isPrimitive(returnType)) { cb.opc_invokestatic(boxingMethodForPrimitiveType(returnType), typeSizeInStackSlots(returnType), 0); } else if (returnType == Void.TYPE) { cb.opc_aconst_null(); } } 

これはより明白です(左側のJDK 9):



この問題はかなり前に発見されており、対応する問題は2004年から存在しています(!)


トピックに関する議論があります:


開始する


継続


それが良いかどうか今すぐ確認しましょう。 「9」に切り替えて、私たちの経験を繰り返して、これを見るでしょう:



16回ヒットすると、 Boolean.valueOf()を使用してBoolean.TRUE/Boolean.FALSEを返すコードが作成されBoolean.TRUE/Boolean.FALSENativeMethodAccessorImpl.invoke0()NativeMethodAccessorImpl.invoke0()メソッドにはまだ問題がありました。このメソッドは、新しいオブジェクトを永続的に返します(10-keでも)。 何もする必要はありません。VMのソースコードにアクセスして、それについて何かできるかどうかを確認する必要があります。


invoke0への直接参照は見つかりませんinvoke0 、このトピックに関する議論では、 reflection.cppファイルが表示され、コンストラクターがinvoke()メソッドによって呼び出されているように見えます。 この方法では、最後のが最も重要です。


 return Reflection::box((jvalue*)result.get_value_addr(), rtype, THREAD); 

Reflection::box コード


 oop Reflection::box(jvalue* value, BasicType type, TRAPS) { if (type == T_VOID) { return NULL; } if (type == T_OBJECT || type == T_ARRAY) { // regular objects are not boxed return (oop) value->l; } oop result = java_lang_boxing_object::create(type, value, CHECK_NULL); if (result == NULL) { THROW_(vmSymbols::java_lang_IllegalArgumentException(), result); } return result; } 

主なものは空の行で強調表示されます。 今java_lang_boxing_object ::コードを作成


 oop java_lang_boxing_object::create(BasicType type, jvalue* value, TRAPS) { oop box = initialize_and_allocate(type, CHECK_0); if (box == NULL) return NULL; switch (type) { case T_BOOLEAN: box->bool_field_put(value_offset, value->z); break; //.... case-case-case return box; } oop java_lang_boxing_object::initialize_and_allocate(BasicType type, TRAPS) { Klass* k = SystemDictionary::box_klass(type); if (k == NULL) return NULL; instanceKlassHandle h (THREAD, k); if (!h->is_initialized()) h->initialize(CHECK_0); return h->allocate_instance(THREAD); } 

ご覧のとおり、VMは最初に新しい空のオブジェクトを作成し、次に値をフラッシュしてから返します。 これは、コンストラクターを呼び出さずに新しいオブジェクトの外観を説明します。 タイプT_BOOLEAN場合、VMレベルで2つの値をキャッシュすることは可能かもしれませんが、ゲームがろうそくに値するかどうかは明確ではありません。


乾燥残留物中


「9」に切り替えた後、どのくらい勝ちますか? 計算:


 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"}) public class ReflectiveCallBenchmark { @Benchmark public Object invoke(Data data) throws Exception { return data.method.invoke(data); } @State(Scope.Thread) public static class Data { Method method; @Setup public void setup() throws Exception { method = getClass().getMethod("f"); } public boolean f() { return true; } } } 

Jdk 8Jdk 9Jdk 10Jdk 11
ベンチマークモードCnt得点得点得点得点単位
呼び出すavgt309.97.07.67.7ns / op
呼び出し:gc.alloc.rate.normgcprof3032161616B / op

再帰呼び出しのすべてのコストはここで測定されます。 コンストラクタとvalueOfを使用してラッピングbooleanの違いを測定する必要がある場合は、より簡単な測定を使用できます。


 @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Fork(jvmArgsAppend = {"-XX:+UseParallelGC", "-Xms1g", "-Xmx1g"}) public class BooleanInstantiationBenchmark { @Benchmark public Boolean constructor(Data data) { return new Boolean(data.value); } @Benchmark public Boolean valueOf(Data data) { return Boolean.valueOf(data.value); } @State(Scope.Thread) public static class Data { @Param({"true", "false"}) boolean value; } } 

Jdk 8Jdk 9Jdk 10Jdk 11
ベンチマークモードCnt得点得点得点得点単位
valueOfavgt303,73.43.63,5ns / op
コンストラクターavgt307.45,05.55.9ns / op
valueOf:gc.alloc.rate.normgcprof300000B / op
コンストラクター:gc.alloc.rate.normgcprof3016161616B / op

合計: booleanを返す再帰メソッド呼び出しごとに-16バイトおよび-2..3 ns。 単純な変更については、悪くはありません。特に、血まみれのエンタープライズでリフレクションを使用する頻度と、改善が他のプリミティブにも適用されるという事実を考慮してください。 VM内にオブジェクトを作成するのではなく、 new MethodAccessorGenerator().generateMethod()を使用して作成されたコードのパフォーマンスを測定することに注意してください。


結論として、記載されている改善自体は非常に小さく、その効果はほとんど認識できません。 Javaの新しいエディションの生産性を生み出すのは、まさにこのような些細なことです。


PS ReflectionFactory.inflationThreshold()メソッドによって返される値は、VMの起動時に引数として渡される-Dsun.reflect.inflationThresholdプロパティを使用してオーバーライドできます。 したがって、すでに「9」に移動している場合は、このフラグを使用して、再帰呼び出しのバイトコードを作成するためのしきい値を下げることができます。 これにより、アプリケーションの起動が遅くなる場合がありますが、「ゴミ」は少なくなります。 ドキュメント 、このメカニズムが発明された理由を説明しています。


PPS「nine」で始まる検討中のクラス( MethodAccessorGeneratorNativeMethodAccessorImplDelegatingMethodAccessorImplMethodAccessorImpl )は、 jdk.internal.reflectパッケージに転送されました。


PP MethodAccessorGenerator 。記載されている改善の一環として、 MethodAccessorGeneratorだけでなく、 かなりの数のクラスが変更されていることに注意してください。


PPPPS jlBooleanデバイスは、少し単純化して 、数ナノ秒獲得できます ;)



Source: https://habr.com/ru/post/J351988/


All Articles