<JVM パート 1: メモリとガベージ コレクション>10 - StringTable

注記の出典: Shang Silicon Valley の JVM チュートリアルの完全なセット、数百万回の再生、ネットワーク全体のピーク (Song Hongkang が Java 仮想マシンについて詳しく説明しています)

10.1. 文字列の基本プロパティ

  • 文字列: "" 引用符のペアで表される文字列
  • 文字列は final と宣言されており、継承できません
  • String は Serializable インターフェースを実装しています。これは、文字列がシリアライゼーションをサポートしていることを意味します。
  • String は Comparable インターフェースを実装します。これは、文字列のサイズを比較できることを意味します。
  • String は、文字列データを格納するfinal char[] valueために。JDK9では、に変更byte[]

10.1.1. jdk9 での文字列格納構造の変更

公式サイトアドレス: JEP 254: Compact Strings (java.net)

モチベーション

このクラスの現在の実装Stringでは、各文字に 2 バイト (16 ビット) を使用して文字を配列に格納しますchar多くの異なるアプリケーションから収集されたデータは、文字列がヒープ使用の主要な構成要素であり、さらに、ほとんどのStringオブジェクトには Latin-1 文字のみが含まれていることを示しています。charこのような文字は 1 バイトのストレージしか必要としないため、そのようなオブジェクトの内部配列のスペースの半分はString使用されません。

説明

クラスの内部表現をStringUTF-16char配列からbyte配列とエンコーディング フラグ フィールドに変更することを提案します。新しいStringクラスは、文字列の内容に基づいて、ISO-8859-1/Latin-1 (1 文字あたり 1 バイト) または UTF-16 (1 文字あたり 2 バイト) としてエンコードされた文字を格納します。encoding フラグは、どのエンコーディングが使用されているかを示します。

AbstractStringBuilder、、、などの文字列関連のクラスはStringBuilderStringBufferHotSpot VM の固有の文字列操作と同様に、同じ表現を使用するように更新されます。

これは純粋に実装の変更であり、既存のパブリック インターフェイスへの変更はありません。新しいパブリック API やその他のインターフェイスを追加する予定はありません。

これまでに行われたプロトタイピング作業により、予想されるメモリ フットプリントの削減、GC アクティビティの大幅な削減、およびいくつかのまれなケースでのマイナーなパフォーマンスの低下が確認されました。

モチベーション

String クラスの現在の実装では、1 文字あたり 2 バイト (16 ビット) を使用して、文字を char 配列に格納します。多くの異なるアプリケーションから収集されたデータは、文字列がヒープ使用の主要な構成要素であることを示しています。ほとんどの文字列オブジェクトには、Latin-1 文字のみが含まれます。これらの文字は 1 バイトのストレージしか必要としないため、これらの文字列オブジェクトの内部文字配列の半分は使用されません。

例証する

String クラスの内部表現を UTF-16 文字の配列からバイト配列とエンコーディング フラグ フィールド. 新しい String クラスは、文字列の内容に応じて、文字エンコーディングを ISO-8859-1/Latin-1 (1 文字あたり 1 バイト) または UTF-16 (1 文字あたり 2 バイト) として格納します。encoding フラグは、どのエンコーディングが使用されているかを示します。


などの文字列に関連するクラスAbstractStringBuilder、StringBuilder、および StringBuffer は、HotSpot VM の固有の文字列操作と同様に、同じ表現を使用するように更新されます。

これは純粋に実装の変更であり、既存のパブリック インターフェイスへの変更はありません。現在、新しいパブリック API やその他のインターフェイスを追加する予定はありません。

これまでに行われたプロトタイピング作業により、予想されるメモリ フットプリントの削減、GC アクティビティの大幅な削減、およびいくつかのまれなケースでのわずかなパフォーマンスの低下が確認されました。

結論:文字列は char[] に格納されなくなりましたが、byte[] とエンコード マークに変更されました。これにより、スペースが節約されます。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    
    
    @Stable
    private final byte[] value;
}

10.1.2. 文字列の基本プロパティ

文字列: 不変の文字列を表します。略語: 不可变性.

  • 文字列を代入する場合、指定した代入用メモリ領域を書き換える必要があり、元の値を代入に使用することはできません。
  • 既存の文字列に対して連結演算を行う場合、代入するメモリ領域も再指定する必要があり、元の値を代入に使用することはできません。
  • string の replace() メソッドを呼び出して、指定した文字または文字列を変更する場合、代入用のメモリ領域も再指定する必要があり、元の値を代入に使用することはできません。

リテラル値 (new とは異なります) を使用して文字列に値を代入し、このときの文字列値を文字列定数プールで宣言します。

ケースプレゼンテーション:

/**
 * String的基本使用:体现String的不可变性
 *
 * @author shkstart  [email protected]
 * @create 2020  23:42
 */
public class StringTest1 {
    
    
    @Test
    public void test1() {
    
    
        String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";
        s1 = "hello";

        System.out.println(s1 == s2);//判断地址:true  --> false

        System.out.println(s1);//
        System.out.println(s2);//abc

    }

    @Test
    public void test2() {
    
    
        String s1 = "abc";
        String s2 = "abc";
        s2 += "def";
        System.out.println(s2);//abcdef
        System.out.println(s1);//abc
    }

    @Test
    public void test3() {
    
    
        String s1 = "abc";
        String s2 = s1.replace('a', 'm');
        System.out.println(s1);//abc
        System.out.println(s2);//mbc
    }
}

文字列定数プールは、同じ内容の文字列を格納しません

String の String Pool は固定サイズのHashtableであり、デフォルトのサイズと長さは 1009 です。多くの文字列が文字列プールに入れられると、深刻なハッシュ競合が発生し、リンク リストが非常に長くなり、長いリンク リストの直接的な影響は、String.intern を呼び出すときにパフォーマンスが大幅に低下することです。

-XX:StringTablesizeStringTable の長さを設定できます

  • StringTable は jdk6 で 1009 の長さで固定されているため、定数プールに文字列が多すぎると効率が急激に低下します。StringTablesize の設定は不要です
  • jdk7 では、StringTable の長さのデフォルト値は 60013 であり、StringTable のサイズ設定の要件はありません。
  • JDK8では、StringTableの長さを設定する場合、設定できる最小値は1009です

**注:** String.intern が呼び出されたときにリンク リストが長いと、パフォーマンスが大幅に低下するのはなぜですか?

String プールの最下層は HashTable です。String.intern() を呼び出すと、文字列定数プールに文字列が追加されます。結合する前に、まず文字列がプールに存在するかどうかを判断します. このプロセスでリンクされたリストが長すぎると、文字列を走査する速度が非常に遅くなり、パフォーマンスが低下します.

JDK6 --> JDK7 で、StringTable のデフォルト サイズが増加したのはなぜですか?

StringTable が大きいほど、HashTable に対応する基になる配列の長さが大きくなり、要素を追加するときのハッシュ競合の可能性が小さくなり、リンクされたリストの長さが短くなり、要素を見つけるためにトラバースする時間が短くなり、効率アップ!(長さが1009の場合、10Wのストリングを追加するのに143msかかります。長さが60013の場合、47msかかります)

テストコード:

/**
 * 产生10万个长度不超过10的字符串,包含a-z,A-Z
 * @author shkstart  [email protected]
 * @create 2020  23:58
 */
public class GenerateString {
    
    
    public static void main(String[] args) throws IOException {
    
    
        FileWriter fw =  new FileWriter("words.txt");

        for (int i = 0; i < 100000; i++) {
    
    
            //1 - 10
           int length = (int)(Math.random() * (10 - 1 + 1) + 1);
            fw.write(getString(length) + "\n");
        }

        fw.close();
    }

    public static String getString(int length){
    
    
        String str = "";
        for (int i = 0; i < length; i++) {
    
    
            //65 - 90, 97-122
            int num = (int)(Math.random() * (90 - 65 + 1) + 65) + (int)(Math.random() * 2) * 32;
            str += (char)num;
        }
        return str;
    }
}

/**
 * 测试在StringTableSize=1009 和 StringTableSize=60013下,添加10W个字符串耗时情况
 * 结论:1009:143ms  100009:47ms
 *  配置:-XX:StringTableSize=1009
 * @author shkstart  [email protected]
 * @create 2020  23:53
 */
public class StringTest2 {
    
    
    public static void main(String[] args) {
    
    
        //测试StringTableSize参数
//        System.out.println("我来打个酱油");
//        try {
    
    
//            Thread.sleep(1000000);
//        } catch (InterruptedException e) {
    
    
//            e.printStackTrace();
//        }

        BufferedReader br = null;
        try {
    
    
            br = new BufferedReader(new FileReader("words.txt"));
            long start = System.currentTimeMillis();
            String data;
            while((data = br.readLine()) != null){
    
    
                data.intern(); //如果字符串常量池中没有对应data的字符串的话,则在常量池中生成
            }

            long end = System.currentTimeMillis();
            // 数组越长,Hash碰撞越少,效率越高~
            System.out.println("花费的时间为:" + (end - start));//1009:143ms  100009:47ms
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if(br != null){
    
    
                try {
    
    
                    br.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }

            }
        }
    }
}

10.2. 文字列メモリ割り当て

Java 言語には、8 つの基本データ型と特殊な型 String があります。それらを高速化し、操作中のメモリを節約するために、これらの型は定数プールの概念を提供します。

定数プールは、Java システム レベルで提供されるキャッシュに似ています。8 つの基本データ型の定数プールは、システムによって調整されます。String 型の定数プールは特殊です。使い方は大きく分けて2つあります。

  • 二重引用符を使用して直接宣言された文字列オブジェクトは、定数プールに直接格納されます。

  • 二重引用符で宣言された String オブジェクトでない場合は、String が提供する intern() メソッドを使用できます。これについては後で説明します

Java 6 以前では、文字列定数プールは永続世代に格納されます

Java 7 では、Oracle のエンジニアが文字列プールのロジックに大きな変更を加えました。文字列定数プールの場所は、Java ヒープに合わせて調整されます

  • すべての文字列は、他の通常のオブジェクトと同様にヒープ (ヒープ) に保存されるため、アプリケーションのチューニング時にヒープ サイズを調整するだけで済みます。

  • 文字列定数プールの概念はもともとより多く使用されていましたが、この変更により、Java 7 での使用を再検討する十分な理由が得られますString.intern()

Java8 メタスペース、文字列定数はヒープ上にあります

画像-20200711093546398

画像-20200711093558709

詳細な説明については、https://blog.csdn.net/qq_43842093/article/details/122991756 を参照してください。

StringTable を調整する必要があるのはなぜですか?

公式 Web サイトのアドレス: Java SE 7 の機能と拡張機能 (oracle.com)

概要: JDK 7 では、インターンされた文字列は Java ヒープの永続的な世代に割り当てられなくなりましたが、代わりに Java ヒープの主要部分 (若い世代と古い世代として知られています) に割り当てられます。アプリケーション。この変更により、メインの Java ヒープに存在するデータが増え、永続世代のデータが少なくなるため、ヒープ サイズの調整が必要になる場合があります。ほとんどのアプリケーションでは、この変更によるヒープ使用量の差は比較的小さくなりますが、多くのクラスをロードしたり、メソッドを頻繁に使用する大規模なアプリケーションでは、String.intern()より大きな違いが見られます。

概要: JDK 7 では、内部文字列は Java ヒープの永続的な世代ではなく、Java ヒープの主要な部分 (若い世代と古い世代と呼ばれる) に、アプリケーションによって作成された他のオブジェクトと共に割り当てられます。この変更により、メインの Java ヒープに存在するデータが増え、永続世代のデータが少なくなるため、ヒープのサイズ変更が必要になる場合があります。ほとんどのアプリケーションでは、この変更によるヒープ使用量の差は比較的小さくなりますが、多くのクラスをロードしたり、String.intern() メソッドを頻繁に使用する大規模なアプリケーションでは、より顕著な違いが見られます。

調整理由: ①permSizeがデフォルトで比較的小さい ②Permanent Generationガベージコレクションの頻度が低いため、OOMが発生しやすい

10.3. 文字列の基本操作

/**
 * Debug查看字符串常量池中有多少字符串
 * @author shkstart  [email protected]
 * @create 2020  0:49
 */
@Test
public void test1() {
    
    
    System.out.print1n("1"); //2321
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("10"); //2330
    System.out.println("1"); //2330 因为池中已经存在该字符串了,所以不会再创建
    System.out.println("2"); //2330
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.print1n("6");
    System.out.print1n("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("10");//2330
}

Java 言語仕様では、同一の文字列リテラルには同じ Unicode 文字シーケンス (同じコード ポイント シーケンスを含む定数) が含まれている必要があり、String クラスの同じインスタンスを指している必要があります。

class Memory {
    
    
    public static void main(String[] args) {
    
    //line 1
        int i= 1;//line 2
        Object obj = new Object();//line 3
        Memory mem = new Memory();//Line 4
        mem.foo(obj);//Line 5
    }//Line 9
    private void foo(Object param) {
    
    //line 6
        String str = param.toString();//line 7
        System.out.println(str);
    }//Line 8
}

画像-20210511111607132

注:文字列と intern() のリテラル値のみが文字列に追加されます。Object#toString は、前者ではなく、複数の文字をつなぎ合わせた結果を返します

なおnew String()、フォームについては後述

10.4. 文字列連結操作

  • 定数と定数のスプライシング結果は定数プールにあり、原則はコンパイル時の最適化です
  • 同じ内容の変数は定数プールに存在しません
  • それらの1つが変数である限り、結果はヒープにあります[ここでのヒープは、文字列定数プール以外のヒープ領域を指します]。変数スプライシングの原則は StringBuilder です
  • スプライシング結果が intern() メソッドを呼び出した場合、定数プールにない文字列オブジェクトを積極的にプールに入れ、このオブジェクトのアドレスを返します

例 1

@Test
public void test1(){
    
    
    String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
    String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
    /*
         * 最终.java编译成.class,再执行.class
         * String s1 = "abc";
         * String s2 = "abc"
         */
    System.out.println(s1 == s2); //true
    System.out.println(s1.equals(s2)); //true
}

例 2

@Test
public void test2(){
    
    
    String s1 = "javaEE";
    String s2 = "hadoop";

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";//编译期优化
    //如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4);//true
    System.out.println(s3 == s5);//false
    System.out.println(s3 == s6);//false
    System.out.println(s3 == s7);//false
    System.out.println(s5 == s6);//false
    System.out.println(s5 == s7);//false
    System.out.println(s6 == s7);//false
    //intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
    //如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回次对象的地址。
    String s8 = s6.intern();
    System.out.println(s3 == s8);//true
}

例 3

@Test
public void test3(){
    
    
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    /*
        如下的s1 + s2 的执行细节:(变量s是我临时定义的)
        ① StringBuilder s = new StringBuilder();
        ② s.append("a")
        ③ s.append("b")
        ④ s.toString()  --> 约等于 new String("ab")

        补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
         */
    String s4 = s1 + s2;//
    System.out.println(s3 == s4);//false
}

例 4

  • 最終的な変更がなければ、変数です。たとえば、s3 行の s1 と s2 は new StringBuilder によってスプライシングされます
  • 定数である最終変更を使用します。コードの最適化はコンパイラによって実行されます。実際の開発では、finalが使えるなら使ってみてください
    /*
    1. 字符串拼接操作不一定使用的是StringBuilder!
       如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式。
    2. 针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上。
       final修饰的,在编译时就初始化好了
     */
@Test
public void test4(){
    
    
    final String s1 = "a";
    final String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);//true
}

バイトコードのパースペクティブ分析

例 3 のバイトコードを見ると、s1 + s2StringBuilder オブジェクトが実際に新しいことがわかります。append メソッドを使用して s1 と s2 を追加し、最後に toString メソッドを呼び出して s4 に割り当てます。

 0 ldc #14 <a> #加载常量a
 2 astore_1       #将a放到局部变量表的下标为1的位置
 3 ldc #15 <b>
 5 astore_2       #将b放到局部变量表的下标为2的位置
 6 ldc #16 <ab>
 8 astore_3
 9 new #9 <java/lang/StringBuilder> #创建StringBuilder对象
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init> : ()V>
16 aload_1
17 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> #append
20 aload_2
21 invokevirtual #11 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> #append
24 invokevirtual #12 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> #toString
27 astore 4
29 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #4 <java/io/PrintStream.println : (Z)V>
46 return

文字列連結演算の性能比較

    /*
    体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
    
    详情:① StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象
          	        使用String的字符串拼接方式:创建过多个StringBuilder和String的对象
         	② 使用String的字符串拼接方式:内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;如果进行GC,需要花费额外的时间。

     改进的空间:在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
               StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
     */
@Test
public void test6(){
    
    

    long start = System.currentTimeMillis();

    //        method1(100000);//4014
    method2(100000);//7

    long end = System.currentTimeMillis();

    System.out.println("花费的时间为:" + (end - start));
}

public void method1(int highLevel){
    
    
    String src = "";
    for(int i = 0;i < highLevel;i++){
    
    
        src = src + "a";//每次循环都会创建一个StringBuilder、String
    }
}

public void method2(int highLevel){
    
    
    //只需要创建一个StringBuilder
    StringBuilder src = new StringBuilder();
    src.toString();
    for (int i = 0; i < highLevel; i++) {
    
    
        src.append("a");
    }
}      

10.5. intern() の使用

10.5.1 intern() の基本紹介

公式 API ドキュメントでの説明

public String intern()

文字列オブジェクトの正規表現を返します。

最初は空である文字列のプールは、クラスによってプライベートに維持されますString

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

したがって、任意の 2 つの文字列sおよび はtである場合にのみ、 でs.intern() == t.intern()あるということになりますtrues.equals(t)true

すべてのリテラル文字列と文字列値の定数式はインターンされます。文字列リテラルは、Java™ 言語仕様のセクション 3.10.5 で定義されています。

  • 戻り値:

    この文字列と同じ内容を持つ文字列ですが、一意の文字列のプールからのものであることが保証されています。

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

したがって、任意の 2 つの文字列 s および t について、s.intern() == t.intern() は、s.equals(t) が true である場合にのみ true になります。

文字列値を取るすべてのリテラル文字列と定数式はインターンされます。

この文字列と同じ内容の文字列を返しますが、文字列の一意のプールからのものであることが保証されています。


intern は、基になる C メソッドを呼び出すネイティブ メソッドです。

public native String intern();

二重引用符で宣言された String オブジェクトでない場合は、String によって提供される intern メソッドを使用できます。これは、現在の文字列が文字列定数プールに存在するかどうかを照会し、存在しない場合は、現在の文字列を定数プールに入れます。

String myInfo = new string("I love atguigu").intern();

つまり、任意の文字列で String.intern メソッドが呼び出された場合、返された結果が指すクラス インスタンスは、定数の形式で直接表示される文字列インスタンスとまったく同じでなければなりません。したがって、次の式は true と評価される必要があります。

("a"+"b"+"c").intern() == "abc"

簡単に言えば、インターンされた文字列は、メモリ内に文字列のコピーが 1 つだけ存在するようにすることです。これにより、メモリ スペースを節約し、文字列操作タスクの実行を高速化できます。この値は String Intern Pool (String Intern Pool) に格納されることに注意してください。

画像-20210511145542579

変数 s が文字列定数プール内のデータを指していることを確認するにはどうすればよいですか?

次の 2 つの方法があります。

  • 方法 1: String s = "shkstart";//リテラル​​定義の方法

  • 方法 2: intern() を呼び出す

    String s = new String(“shkstart”).intern();

    String s = new StringBuilder(“shkstart”).toString().intern();

10.5.2 文字列に関するインタビューの 2 つの質問

インタビューの質問: new String("ab") はいくつのオブジェクトを作成しますか? new String("a") + new String("b") はどうですか?

String str = new String("ab");

バイトコードを見ると、2 つあることがわかります。

  • 一个对象是:new关键字在堆空间创建的
    
  • 另一个对象是:字符串常量池中的对象"ab"。 字节码指令ldc :在常量池中常见对象
    

画像-20230215152940742

なぜ 2 つに設計する必要があるのでしょうか。

その後、リテラル パラメーターを使用する場合、常にこのパラメーターの場所が必要です。これはリテラル値であるため、文字列定数プールに自然に配置され、新しい String 操作はこのパラメーターを使用して新しいオブジェクトに値を割り当てます
。ヒープ空間で、新しいオブジェクト アドレスを str に渡します。

String str = new String("a") + new String("b");

10.5.3 インターンの使用: JDK6 対 JDK7/8

/**
 * 看一道关于intern的面试题
 */
public class StringIntern {
    
    
    public static void main(String[] args) {
    
    

        String s = new String("1");// s记录的地址:堆空间中new String("1")的地址
        s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";// s2记录的地址:常量池中对象的地址
        System.out.println(s == s2);//jdk6:false   jdk7/8:false


        String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
                                            //         jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
        		            // 本来该生成两个地址,但是都是在堆空间,而且存11,那就没必要再开辟串池空间了。这是JDK的优化
       
        String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

}

ダイアグラム

jdk6図

jdk7 図 1

String の intern() の使用を要約すると、次のようになります。

JDK1.6 では、この文字列オブジェクトを文字列プールに入れてみます。

  • 文字列プールに 1 つある場合は、入れられません。既存の文字列プール内のオブジェクトのアドレスを返します
  • なければこれを入れてオブジェクトのコピー、文字列プールに入れ、文字列プール内のオブジェクトのアドレスを返します

JDK1.7 から、この文字列オブジェクトを文字列プールに入れてみます。

  • 文字列プールに 1 つある場合は、入れられません。既存の文字列プール内のオブジェクトのアドレスを返します
  • そうでない場合は、入れますオブジェクト参照アドレスコピーを作成して文字列プールに入れ、参照アドレスを文字列プールに返します

参考記事:Meituan技術チームによるString#internの詳細分析

10.5.4 intern() の演習

演習 1

/**
 * @author shkstart  [email protected]
 * @create 2020  20:17
 */
public class StringExer1 {
    
    
    public static void main(String[] args) {
    
    
        // String x = "ab";
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代码执行完以后,字符串常量池中并没有"ab"

        String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
                               //jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回

        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
}

図:

画像-20200711150859709

画像-20200711151326909

画像-20200711151433277

演習 2

/**
 *
 * 10行:false
 * 11行:jdk6:false jdk7/8:true 
 * @author shkstart  [email protected]
 * @create 2020  20:26
 */
public class StringExer2 {
    
    
    public static void main(String[] args) {
    
    
        String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
//        String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
        s1.intern();
        String s2 = "ab";
        System.out.println(s1 == s2);
    }
}

10.5.5 インターンの効率テスト: 空間角度

実際にテストしてみましょう.インターンを使用する場合と使用しない場合では、実際にはかなりの違いがあります。

/**
 * 使用intern()测试执行效率:空间使用上
  *
 *
 * @author shkstart  [email protected]
 * @create 2020  21:17
 */
public class   StringIntern2 {
    
    
    static final int MAX_COUNT = 1000 * 10000;
    static final String[] arr = new String[MAX_COUNT];

    public static void main(String[] args) {
    
    
        Integer[] data = new Integer[]{
    
    1,2,3,4,5,6,7,8,9,10};

        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX_COUNT; i++) {
    
    
//            arr[i] = new String(String.valueOf(data[i % data.length]));
            arr[i] = new String(String.valueOf(data[i % data.length])).intern();

        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:" + (end - start));

        try {
    
    
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.gc();
    }
}

Jprofiler または JVisualVM を使用して、String オブジェクトの作成を観察します。

結論: プログラム内で多数の既存の文字列が使用されている場合、特に文字列の繰り返しが多い場合は、intern() メソッドを使用するとメモリ スペースを節約できます。

分析:メモリを減らすことができるのはなぜですか?

intern() を呼び出すと、定数プール内のオブジェクトが毎回使用されます。ヒープ空間にも作成されますが、定数プールにあるものを使用しており、ヒープ空間の部分は誰も使わないのでGCすることでメモリを削減できます。

大規模な Web サイト プラットフォームでは、多数の文字列をメモリに格納する必要があります。たとえば、ソーシャル ネットワーキング サイト、多くの人々 ストア: 北京、海淀区、その他の情報。このとき、文字列が intern() メソッドを呼び出すと、メモリ サイズが大幅に削減されます。

10.6. StringTable のガベージコレクション

/**
 * String的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 *
 * @author shkstart  [email protected]
 * @create 2020  21:27
 */
public class StringGCTest {
    
    
    public static void main(String[] args) {
    
    
        for (int j = 0; j < 100000; j++) {
    
    
            String.valueOf(j).intern(); //通过源码可知 等价于 new String(..).intern()
        }
    }
}

操作結果:

画像-20230215210718685

10.7. G1 での文字列の重複排除

公式 Web サイトのアドレス: JEP 192: G1 での文字列重複排除 (java.net)

モチベーション

現在、大規模な Java アプリケーションの多くがメモリのボトルネックになっています。測定によると、これらのタイプのアプリケーションでは Java ヒープのライブ データ セットの約 25% がStringオブジェクトによって消費されています。さらに、これらのStringオブジェクトの約半分は重複であり、重複がstring1.equals(string2)true であることを意味します。Stringヒープ上にオブジェクトを複製することは、本質的には単なるメモリの浪費です。このプロジェクトでは、StringG1 ガベージ コレクターに自動かつ継続的な重複排除を実装して、メモリの浪費を回避し、メモリ フットプリントを削減します。

現在、大規模な Java アプリケーションの多くはメモリがボトルネックになっています。測定によると、これらのタイプのアプリケーションでは、Java ヒープ リアルタイム データ セットの約 25% がString'对象所消耗。此外,这些 "String "对象中大约有一半是重复的,其中重复意味着 "string1.equals(string2) "是真的。在堆上有重复的String' オブジェクトによって占められており、これは基本的にメモリの浪費にすぎません。このプロジェクトは、G1 ガベージ コレクターで自動的かつ継続的な「文字列」重複排除を実装して、メモリの浪費を回避し、メモリ フットプリントを削減します。


ここで言及されている複製は、定数プール自体ではなく、ヒープ内のデータを参照していることに注意してください。定数プール自体は繰り返されないためです。

背景: 多くの Java アプリケーション (大小を問わず) でのテストでは、次の結果が得られました。

  • 文字列オブジェクトは、ヒープ生存データ コレクションの 25% を占めます
  • ヒープ サバイバル データ コレクションには、13.5% の繰り返し文字列オブジェクトがあります。
  • 文字列オブジェクトの平均の長さは 45 です

多くの大規模な Java アプリケーションのボトルネックはメモリであり、テストによると、これらのタイプのアプリケーションでは、Java ヒープに残っているデータ コレクションのほぼ 25% が String オブジェクトです。. さらに、文字列オブジェクトのほぼ半分が繰り返されており、繰り返しの意味は次のとおりですstringl.equals(string2)= trueヒープ上の重複する文字列オブジェクトはメモリの無駄に違いない. このプロジェクトは、G1 ガベージ コレクターで重複する文字列オブジェクトの自動かつ継続的な重複排除を実装し、メモリの浪費を回避します。

達成

  1. ガベージ コレクターが動作している場合、ヒープ上のライブ オブジェクトにアクセスします。アクセスされたオブジェクトごとに、それが重複排除の対象となる String オブジェクトの候補であるかどうかを確認します
  2. その場合は、さらに処理するために、このオブジェクトへの参照をキューに挿入します。バックグラウンドで重複排除スレッドが実行され、このキューが処理されます。キューの要素を処理するということは、要素をキューから削除し、それが参照する文字列オブジェクトを逆参照しようとすることを意味します。
  3. ハッシュテーブルを使用して、String オブジェクトで使用されるすべての一意の char 配列を記録します。重複を削除すると、ハッシュテーブルがチェックされ、同一の char 配列がヒープに既に存在するかどうかが確認されます。
  4. 存在する場合、String オブジェクトはその配列を参照するように調整され、元の配列への参照が解放されます。元の配列は最終的にガベージ コレクターによって回収されます。
  5. ルックアップが失敗した場合、配列を後で共有できるように、char 配列がハッシュテーブルに挿入されます。

コマンド ライン オプション

  • UseStringDeduplication(bool): 文字列の重複排除を有効にします。これはデフォルトでは有効になっておらず、手動で有効にする必要があります。
  • PrintStringDeduplicationStatistics(bool) : 詳細な重複排除統計を出力します
  • StringpeDuplicationAgeThreshold(uintx): この経過時間に達する文字列オブジェクトは重複排除の候補と見なされます

おすすめ

転載: blog.csdn.net/LXYDSF/article/details/129052698