かつて、寒い冬の季節(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は次の機能を提供します。
Boolean b1 = new Boolean(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 {
コンストラクター呼び出しが表示されなかった理由に対する質問の答えは次のとおりです。代わりに、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) {
663行目:いわゆる、校正を見落としていました。 valueOf()
を呼び出して単純な戻り値をラップする代わりに、コンストラクター呼び出しを入力しました。 明らかに、これはinvokestatic
呼び出しをinvokestatic
に置き換えるinvokespecial
があり、コンストラクタの代わりにファクトリメソッドをinvokestatic
invokespecial
があるのはビジネスです。
悲しいかな、ソースに精通している さくらんぼ 「ナイン」は、(非常に突然)私だけがそれほど頭が良いわけではないことを示しました。そして、この問題で栄誉を得ることはありません 。
if (!isConstructor) {
これはより明白です(左側のJDK 9):

この問題はかなり前に発見されており、対応する問題は2004年から存在しています(!) 。
トピックに関する議論があります:
開始する
継続
それが良いかどうか今すぐ確認しましょう。 「9」に切り替えて、私たちの経験を繰り返して、これを見るでしょう:

16回ヒットすると、 Boolean.valueOf()
を使用してBoolean.TRUE/Boolean.FALSE
を返すコードが作成されBoolean.TRUE/Boolean.FALSE
。 NativeMethodAccessorImpl.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) {
主なものは空の行で強調表示されます。 今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;
ご覧のとおり、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 8 | Jdk 9 | Jdk 10 | Jdk 11 | |
---|
ベンチマーク | モード | Cnt | 得点 | 得点 | 得点 | 得点 | 単位 |
呼び出す | avgt | 30 | 9.9 | 7.0 | 7.6 | 7.7 | ns / op |
呼び出し:gc.alloc.rate.norm | gcprof | 30 | 32 | 16 | 16 | 16 | B / 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 8 | Jdk 9 | Jdk 10 | Jdk 11 | |
---|
ベンチマーク | モード | Cnt | 得点 | 得点 | 得点 | 得点 | 単位 |
valueOf | avgt | 30 | 3,7 | 3.4 | 3.6 | 3,5 | ns / op |
コンストラクター | avgt | 30 | 7.4 | 5,0 | 5.5 | 5.9 | ns / op |
valueOf:gc.alloc.rate.norm | gcprof | 30 | 0 | 0 | 0 | 0 | B / op |
コンストラクター:gc.alloc.rate.norm | gcprof | 30 | 16 | 16 | 16 | 16 | B / op |
合計: boolean
を返す再帰メソッド呼び出しごとに-16バイトおよび-2..3 ns。 単純な変更については、悪くはありません。特に、血まみれのエンタープライズでリフレクションを使用する頻度と、改善が他のプリミティブにも適用されるという事実を考慮してください。 VM内にオブジェクトを作成するのではなく、 new MethodAccessorGenerator().generateMethod()
を使用して作成されたコードのパフォーマンスを測定することに注意してください。
結論として、記載されている改善自体は非常に小さく、その効果はほとんど認識できません。 Javaの新しいエディションの生産性を生み出すのは、まさにこのような些細なことです。
PS ReflectionFactory.inflationThreshold()
メソッドによって返される値は、VMの起動時に引数として渡される-Dsun.reflect.inflationThreshold
プロパティを使用してオーバーライドできます。 したがって、すでに「9」に移動している場合は、このフラグを使用して、再帰呼び出しのバイトコードを作成するためのしきい値を下げることができます。 これにより、アプリケーションの起動が遅くなる場合がありますが、「ゴミ」は少なくなります。 ドキュメントは 、このメカニズムが発明された理由を説明しています。
PPS「nine」で始まる検討中のクラス( MethodAccessorGenerator
、 NativeMethodAccessorImpl
、 DelegatingMethodAccessorImpl
、 MethodAccessorImpl
)は、 jdk.internal.reflect
パッケージに転送されました。
PP MethodAccessorGenerator
。記載されている改善の一環として、 MethodAccessorGenerator
だけでなく、 かなりの数のクラスが変更されていることに注意してください。
PPPPS jlBoolean
デバイスは、少し単純化して 、数ナノ秒獲得できます ;)