blog

今回はJavaの定数プールを徹底的に理解する

JVM定数プールは、主にクラス・ファイル定数プール、ランタイム定数プール、グローバル文字列定数プール、基本型ラッパー・クラス・オブジェクト定数プールに分けられます。 クラスファイルは、バイト単位のバイ...

Nov 30, 2020 · 12 min. read
シェア



JVM定数プールは主に次のように分けられます。

クラスファイルは、バイト単位のバイナリデータのストリームのセットは、Javaコードのコンパイル時に、書かれたJavaファイルは、クラスファイルの定数プールが含まれてディスク上に格納されている.classファイル形式のバイナリデータにコンパイルされます。 クラスファイルは、定数プールがあり、そのコンパイルフェーズが決定されている、クラスファイルの構造のjvmの仕様は、厳格な仕様を持って、この仕様を満たす必要がありますクラスファイルは、jvmの任意のロードされます。説明を容易にするために、簡単なクラスを記述します。

class JavaBean{
 private int value = 1;
 public String s = "abc";
 public final static int f = 0x101;
 public void setValue(int v){
 final int temp = 3;
 this.value = temp + v;
 }
 public int getValue(){
 return value;
 }
}

javacコマンドでコンパイルした後、javap -vコマンドでコンパイルしたファイルを表示します。

class JavaBasicKnowledge.JavaBean
 minor version: 0
 major version: 52
 flags: ACC_SUPER
Constant pool:
 #1 = Methodref #6.#29 // java/lang/Object."<init>":()V
 #2 = Fieldref #5.#30 // JavaBasicKnowledge/JavaBean.value:I
 #3 = String #31 // abc
 #4 = Fieldref #5.#32 // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String;
 #5 = Class #33 // JavaBasicKnowledge/JavaBean
 #6 = Class #34 // java/lang/Object
 #7 = Utf8 value
 #8 = Utf8 I
 #9 = Utf8 s
 #10 = Utf8 Ljava/lang/String;
 #11 = Utf8 f
 #12 = Utf8 ConstantValue
 #13 = Integer 257
 #14 = Utf8 <init>
 #15 = Utf8 ()V
 #16 = Utf8 Code
 #17 = Utf8 LineNumberTable
 #18 = Utf8 LocalVariableTable
 #19 = Utf8 this
 #20 = Utf8 LJavaBasicKnowledge/JavaBean;
 #21 = Utf8 setValue
 #22 = Utf8 (I)V
 #23 = Utf8 v
 #24 = Utf8 temp
 #25 = Utf8 getValue
 #26 = Utf8 ()I
 #27 = Utf8 SourceFile
 #28 = Utf8 StringConstantPool.java
 #29 = NameAndType #14:#15 // "<init>":()V
 #30 = NameAndType #7:#8 // value:I
 #31 = Utf8 abc
 #32 = NameAndType #9:#10 // s:Ljava/lang/String;
 #33 = Utf8 JavaBasicKnowledge/JavaBean
 #34 = Utf8 java/lang/Object

このコマンドを実行すると、クラス・ファイルのバージョン番号、定数のプール、コンパイルされたバイトコードが得られることがわかります。定数プールですから、そこに格納されているのは定数に違いありません。 クラスファイルの定数プールには、主に次の2つの定数が格納されています。



1) リテラル量: リテラル量はJava言語レベルでの定数の概念に近く、主に以下のようなものがあります:

つまり、しばしば次のように主張されます: public String s = "abc"; in "abc"

 #9 = Utf8 s
 #3 = String #31 // abc
 #31 = Utf8 abc

静的変数、インスタンス変数、ローカル変数を含む

#11 = Utf8 f
 #12 = Utf8 ConstantValue
 #13 = Integer 257

ここで明確化のポイントは、上記のリテラル量の定数プールに存在すると言われ、データの値を指します、つまり、abcと0x101(257)、2つのリテラル量の定数プールの上記の観察を通じて、定数プールに存在しています。



シンボリック参照は主にコンパイルの原則に関連する概念で、以下の3種類の定数があります。

これは、java/lang/Stringとしても知られています。このように、クラス名の元の". "を"/"に置き換えることで、主に上記のように、実行時にクラスへの直接参照を解決するために使用されます。クラス名の元の". "を"/"に置き換えて取得します。主に、上記のようにクラスへの直接参照を取得するために、実行時の解決で使用されます。

#5 = Class #33 // JavaBasicKnowledge/JavaBean
 #33 = Utf8 JavaBasicKnowledge/JavaBean

フィールドは、クラスレベル変数とインスタンスレベル変数を含む、クラスまたはインターフェイスで宣言された変数です。

#4 = Fieldref #5.#32 // JavaBasicKnowledge/JavaBean.value:I
 #5 = Class #33 // JavaBasicKnowledge/JavaBean
 #32 = NameAndType #7:#8 // value:I
 #7 = Utf8 value
 #8 = Utf8 I
 //この2つはローカル変数で、値はフィールド名を保持する。
 #23 = Utf8 v
 #24 = Utf8 temp

ご覧のように、メソッド内のローカル変数名については、クラスファイルの定数プールにはフィールド名しか保持されません。

すなわち、パラメータ型+戻り値

 #21 = Utf8 setValue
 #22 = Utf8 (I)V
 #25 = Utf8 getValue
 #26 = Utf8 ()I

ランタイム定数プールはメソッド領域の一部であり、jvmがクラスを実行するときに必ず通過しなければならず、ロードの最初のステップで実行される必要があることを知っているため、これもグローバルに貢献します:

  • 完全修飾名でクラスのバイナリ・バイト・ストリームを取得します。

  • このバイトストリームで表される静的ストレージ構造を、メソッド領域の実行時データ構造に変換します。

  • このロードされたクラスを表すクラス・オブジェクトがメモリ上に生成されます。クラス・オブジェクトはjava.lang.Classで、メソッド領域でこのクラスの様々なデータ・アクセスのエントリー・ポイントとして機能します。



クラスオブジェクトと通常のオブジェクトは異なっており、クラスオブジェクトは、クラスがロードされたときに完了され、jvmによって作成され、単一のインスタンスであり、クラスと外部の世界との対話の入り口として、通常のオブジェクトは、一般的にコールnew後に作成されます。



クラス・バイト・ストリームに代表される静的な記憶構造をメソッド領域の実行時データ構造に変換する上記の第2条には、クラス・ファイルの定数プールを実行時定数プールに入力する処理が含まれていますが、ここで強調する必要があるのは、同時に、実行時定数プールに入力する処理では、複数のクラス・ファイルの定数プールで同じになる文字列、複数のクラス・ファイルの定数プールで同じになる文字列は、実行時定数プールには1つのコピーしか存在しない、これも最適化です。文字列はランタイム定数プールに1つのコピーしか存在しませんが、これも最適化です。



ランタイム定数プールの役割は、Javaクラスファイルの定数プールにシンボリック情報を保存することです。ランタイム定数プールは、クラスファイルに記述されたいくつかのシンボリック参照を保存する一方、クラス解析フェーズでは、これらのシンボリック参照を直接参照に変換し、変換された直接参照もランタイム定数プールに保存されます。



クラス定数プールに相対的な実行時定数プールは、主要な機能は、動的であり、Javaの仕様は、定数が唯一の実行時に生成することができます必要としない、つまり、実行時定数プールの内容は、クラス定数プールからすべてではありませんが、実行時にコードを介して定数を生成することができますし、実行時定数プールに配置され、この機能は、最も使用されている文字列です。intern()です。





一般的に2つのタイプがあります:

String s0 = "hellow";
String s1 = new String("hellow");

最初の方法で宣言されたリテラルhellowはコンパイル時に決定され、クラスファイルの定数プールに直接入ります。実際、後で見るように、"hellow "オブジェクトは最終的にヒープ上に作成されます。新しい文字列()を使用する2つ目の方法は、つまり、Stringクラスのコンストラクタを呼び出す、新しい命令は、クラスのインスタンスを作成し、オブジェクトの初期化のロードを完了することであることを知っているので、文字列オブジェクトの作成を決定するために実行時にあるヒープメモリ上にあります。したがって、この時点で、呼び出しSystem.out.println(s0 == s1);リターンはフレーズでなければならないので、==記号は、要素のアドレスの両側を比較し、s1とs0はヒープ上に存在するが、アドレスは確かに同じではありません。

よくあるトピックをいくつか見てみましょう:

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false

文字列s1 = "Hello"、オブジェクトはヒープに作成されるかどうか?

この図は、理解されているjvmランタイム・データ領域の構造を示していますが、不完全な部分があります。

この図では、メソッド領域が実際には「非ヒープ」と呼ばれる領域に含まれていることがわかります。これは大雑把に解釈すると、非ヒープにはパーマネント世代が含まれ、そのパーマネント世代にはメソッド領域と文字列定数プールが含まれるということです。

Interned Stringは、グローバルに共有される「文字列定数プール」であり、ランタイム定数プールとは異なる概念です。しかし、String s1 = "Hello "と宣言した後のコードでは、クラス・ロードの過程で、クラス・ファイルの情報がメモリのメソッド領域に解析されます。



クラス・ファイルの定数プールにあるデータのほとんどは、リテラルStringを含め、実行時定数プールにロードされます。しかし同時に、"Hello "文字列への参照は、"非ヒープ "領域の文字列定数プールにも格納されます。"Hello "の本体は、他のオブジェクトと同様に、Javaヒープに作成されたままです。文字列定数プール "の "非ヒープ "領域に格納される一方で、"Hello "の本体は、すべてのオブジェクトと同様に、Javaヒープに作成されたままです。



メインスレッドが s1 の作成を開始すると、仮想マシンはまず文字列プールに移動し、equals("Hello") を持つ String があるかどうかを調べ、あれば "Hello" の参照を s1 にコピーします。等しい文字列が見つからなければ、ヒープに新しいオブジェクトを作成し、同時にstrへの参照を代入します。



リテラル代入メソッドを使用して文字列を作成すると、文字列が同じ値を持つ限り、何度作成してもヒープ内の同じオブジェクトを指します。



文字列定数プールは、JVMが保持する文字列インスタンスへの参照のテーブルで、HotSpot VMではStringTableと呼ばれるグローバルテーブルです。文字列定数プールは文字列インスタンスへの参照を維持し、基礎となるC++実装はHashtableです。これらの維持された参照が参照する文字列インスタンスは、「常駐文字列」または「インターン文字列」と呼ばれます。これらの維持される参照が参照する文字列インスタンスは、「常駐文字列」または「インターン文字列」、あるいは一般に「文字列定数プールに入った文字列」と呼ばれるものです。



中略

文字列 "リテラル "が文字列定数プールに入ったのはいつですか?

結論1:ldc命令が実行されると、int型、float型、String型の定数が定数プールからスタックの一番上にプッシュされます。



クラス・ファイル内の定数プール・エントリーのタイプについて、JVMの仕様には2つあります:

  • CONSTANT_Utf8_info

  • CONSTANT_String_info



HotSpot VMのランタイム定数プールでは、CONSTANT_Utf8_infoはクラスファイルのメソッドやフィールドなどを以下の構造で表すことができます:

まず1バイトのタグがあり、これはCONSTANT_Utf8_info構造体の定数であることを意味し、次に2バイトの長さがあり、これはバイトの長さが格納されることを意味し、その後に1バイトのバイト配列があり、これは文字列の長さが本当に格納されることを意味します。バイトというのは、ここにバイト配列があるということを意味しているだけで、この配列の長さはもちろん1バイトよりもずっと大きくなる可能性があることに注意する必要があります。もちろん、CONSTANT_Utf8_info構造体はu2、つまり2バイトの長さしか表現できないので、長さの最大値は2バイト、つまり65535です。



CONSTANT_String_infoはString定数の型ですが、String定数の内容を直接保持するのではなく、インデックスを保持します。

CONSTANT_Utf8は、クラスのロード中に作成されるのに対し、CONSTANT_Stringは、項目を最初に参照するldc命令が初めて実行されたときに遅延解決され、まだ解決されていないときは、HotSpot VMはそれをJVM_CONSTANT_UnresolvedStringと呼びます。中身はクラスファイルと同じようにインデックスだけで、解決されると、項目の定数型は最終的なJVM_CONSTANT_Stringになります。CONSTANT_UnresolvedString。これは、クラス・ファイルと同じですが、インデックスがあります。解決されると、項目の定数型は、最終的なJVM_CONSTANT_Stringになります。



つまり、HotSpot VMの実装に関する限り、クラスをロードする際、これらの文字列リテラルはグローバル文字列定数プールではなく、現在のクラスのランタイム定数プールに入ります。



ここでのldcバイトコードのセマンティクスは、現在のクラスのランタイム定数プールに行き、インデックスに対応する項目を探し、項目がまだ解決されていなければ解決し、解決された項目の内容を返すというものです。



String型の定数に遭遇した場合、resolve処理は、StringTableに一致する内容のjava.lang.Stringへの参照が既にあることがわかれば、この参照を直接返します。逆に、StringTableに一致する内容のStringインスタンスへの参照がまだない場合は、一致する内容のJavaヒープStringオブジェクトを作成し、この参照をStringTableに記録して、この参照を返します。



おわかりのように、ldc命令が新しいStringインスタンスを生成する必要があるかどうかは、このldc命令が最初に実行されたときに、StringTableが対応する内容のStringへの参照をすでに記録しているかどうかによって決まります。



String.intern()は公式に定義されています:

intern メソッドが呼び出されると、プールに this String オブジェクトと等しい文字列が既に含まれている場合はequals(Object) メソッドによって決定された this String オブジェクトと等しい文字列が既にプールに存在する場合は、プールからの文字列が返されます。 そうでない場合は、this String オブジェクトがプールに追加されへの参照が返されます。



実際には、Stringtableテーブルに文字列の内容を取ることです、ある場合は、参照に戻り、存在しない場合は、Stringtableテーブル内の "参照 "のオブジェクトです。

public class RuntimeConstantPoolOOM{
 public static void main(String[] args) {
 String str1 = new StringBuilder("コンピュータ").append("ソフトウェア").toString();
 System.out.println(str1.intern() == str1);
 String str2 = new StringBuilder("ja").append("va").toString();
 System.out.println(str2.intern() == str2);
 }
}

上記のコードはJDK6ではfalseとfalse、JDK7以上ではtrueとfalseになります。



まず第一に、呼び出しStringBuilderは、"コンピュータソフトウェア "文字列オブジェクトを作成するため、newキーワードの呼び出しは、実行時に作成されるので、JVMは文字列ではありません。



JDK6 では、intern() は最初に遭遇した String インスタンスをパーマネント世代にコピーし、このパーマネント世代の String インスタンスへの参照を返します。JDK1.7 以降では、intern() メソッドは String インスタンスをコピーしなくなり、String の intern メソッドは最初に定数プール内のオブジェクトへの参照を見つけようとし、見つかった場合は定数プール内のオブジェクトへの参照を直接返します。オブジェクトへの参照を直接返します。



したがって、1.7では、javaヒープに存在する文字列 "コンピュータソフトウェア "のインスタンスは1つだけです!3の分析を通じて、ときに文字列str1 =新しいStringBuilder("コンピュータ").append("ソフトウェア").toString();このコードの実行後、知っているヒープ内の文字列オブジェクトを作成しており、グローバル文字列定数プールでは、この文字列への参照を保持するために、str1.インターン()直接もちろんstr1.intern()== str1を満たしているこの参照を返す - 彼自身のウェルです。JVMはすでに "Java "この文字列を持っているので、参照str2のために、新しいStringBuilder。したがって、新しいStringBuilder("ja").append("va").toString()は、新しい "java "文字列オブジェクトを再作成し、intern()は、定数参照の最初に遭遇したインスタンスを返しますので、彼は "java "文字列オブジェクトのシステムに戻りました。java "文字列オブジェクトへの参照が返されるので、falseを返します。



JDK6では、str1, str2は新しく作成されたオブジェクトを指し、オブジェクトはJavaヒープに作成されるので、str1, str2はJavaヒープ内のメモリアドレスを指します。internメソッドを呼び出した後、定数プールでオブジェクトを探そうとし、定数プールに入れ、見つからなかった後にそれを返しますので、この時、str1/str2.intern()は定数プール内のアドレスを指し、JDK6の定数プールはヒープから分離された永久世代にあるので、s1.intern()とs1のアドレスはもちろん異なります。intern()は定数プール内のアドレスを指し、JDK6の定数プールは永久世代で、ヒープから分離されているので、s1.intern()とs1のアドレスはもちろん異なります。

public class Test2 {
 public static void main(String[] args) {
 /**
 * 最初に、永続生成の最大および最小メモリ・フットプリントを設定する。
 * VM args: -XX:PermSize=10M -XX:MaxPremSize=10M
 */
 List<String> list = new ArrayList<String>();
 // GCインターン・メソッドによって定数のプールに追加されないことを保証するために、その参照にリストを使用して無限ループ
 int i = 0;
 while (true) {
 // これはここでは永久に実装され、せいぜいintの範囲全体が文字列に変換され、定数プールに入れられるだけだ。
 list.add(String.valueOf(i++).intern());
 }
 }
}

上記のコードはJDK6ではPermのメモリオーバーフローになりますが、JDK7以上では問題ありません。



JDK6の定数プールには永続生成が存在し、永続生成のサイズを設定した後、定数whileループは確実にPermを満杯にし、メモリオーバーフローを引き起こします。JDK7の定数プールはネイティブヒープ(Javaヒープ、HotSpot VMはネイティブヒープとJavaヒープを区別しません)に移動されるため、永続生成のサイズを設定しても定数プールには何の影響もありません。現在のコードでは、すべてのint文字列の合計がヒープを満たすのに十分ではないので、例外はありません。



定数プール技術の実装のラッパークラスのほとんどの基本的なタイプのJavaは、これらのクラスは、バイト、ショート、整数、ロング、文字、ブール、ラッパークラスの他の2つの浮動小数点型の実装されていません。また、整数のラッパークラスのこれらの5つのタイプは、対応する値でのみ、オブジェクトプールを使用することができます127未満または等しいです、つまり、オブジェクトのこれらのクラスの127以上の作成と管理を担当していません。

public class StringConstantPool{
 public static void main(String[] args){
 //5Byte,Short,Integer,Long,Characterオブジェクトを包んでいる。,
 //値が127未満の場合、定数プールを使うことができる。
 Integer i1=127;
 Integer i2=127;
 System.out.println(i1==i2);//真を出力する
 //値が127より大きい場合、オブジェクトは定数プールから取り出されない。
 Integer i3=128;
 Integer i4=128;
 System.out.println(i3==i4);//偽を出力する
 //Booleanクラスは定数プーリング技術も実装している
 Boolean bool1=true;
 Boolean bool2=true;
 System.out.println(bool1==bool2);//真を出力する
 //浮動小数点型のラッパークラスは定数プーリング技術を実装していない
 Double d1=1.0;
 Double d2=1.0;
 System.out.println(d1==d2); //偽を出力する
 }
}

JDK5.0では、基本的なデータ型のデータを直接その対応するラッパークラスに代入することは許可されていません、例えば:整数i = 5;しかし、JDK5.0では、この書き方をサポートするために、コンパイラが自動的に次のコードに変換されますので、上記のコード:整数i = Integer.valueOf (5);これはJavaのローディングボックスです。JDK5.0は自動アンボックスも提供します: Integer i =5; int j = i;



BLOG



Read next

Spring Boot 2.X実践編 - WebFulxリアクティブプログラミング

Spring 5の最も重要なアップデートは、Reactiveプログラミングのサポートです。 Reactiveプログラミングはノンブロッキングなので、ビジネス処理の完了を待つ必要がなく、サーバーリソースの使用量を削減し、同時処理の速度と並行性を向上させます。低レイテンシー、高スループットのプロジェクトに最適です。ノンブロッキング、非同期、弾力性、イベント駆動のエンタープライズクラスのサービスを構築するために使用できます。 Spr...

Nov 30, 2020 · 7 min read