文字列メモリの割り当てとスプライシング操作

文字列の基本機能

  • ""文字列:文字列、引用符のペアで表されます
  • 文字列をインスタンス化する2つの方法:String s1 = "hello";String s2 = new String("hello");
  • 文字列はfinalとして宣言され、継承できません
  • 文字列はSerializableインターフェースを実装します:文字列がシリアル化をサポートすることを示します。
  • 文字列はComparableインターフェイスを実装します:文字列のサイズを比較できることを示します

jdk9での文字列ストレージ構造の変更

  • 文字列は、文字列データを格納するためにjdk8以前の内部で最終的なchar []値(文字ごとに2バイト(16ビット)を使用)を定義しますが、多くの異なるアプリケーションから収集されたデータは、文字列がヒープで使用される主要コンポーネントであり、ほとんどの文字列であることを示していますオブジェクトにはLatin-1文字のみが含まれ、1バイトのストレージのみが必要です。
  • jdk9以降、Stringクラスの内部表現メソッドは、UTF-16文字配列からバイト配列(byte [])とエンコードフラグフィールドに変更されます。新しいStringクラスは、文字列の内容に応じて、ISO-8859-1 / Latin-1(1文字あたり1バイト)またはUTF-16(1文字あたり2バイト)で文字エンコードを格納します。エンコーディングフラグは、使用されているエンコーディングを示します。
  • 結論:文字列はchar []に格納されなくなりましたが、byte []とエンコーディングマークに変更されました。これにより、作業中のメモリ使用量が削減され、GCアクティビティが大幅に削減されます。

文字列:文字の不変のシーケンス

文字列にリテラル値()が割り当てられるString str="Hello";と、文字列値は文字列定数プールで宣言されます。文字列定数プールには、同じ内容の文字列は格納されません。

文字列定数プール:
文字列の文字列プールは固定サイズのハッシュテーブルです。文字列プール内の文字列が多すぎると、深刻なハッシュ競合が発生し、リンクリストが長くなります。また、リンクリストが長い場合の直接的な影響は、String.internが呼び出されたときにパフォーマンスが大幅に低下することです。-XX:StringTablesizeを使用して、StringTableの長さを設定します

  • jdk6では、StringTableは固定されています。つまり1009の長さであるため、定数プールに文字列が多すぎると、効率が急速に低下し、StringTablesizeを設定する必要はありません。
  • jdk7では、StringTableのデフォルトの長さは60013であり、StringTablesize設定の要件はありません。
  • jdk8では、StringTableの長さを設定すると、1009が設定可能な最小値になります。

文字列メモリの割り当て

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

定数プールは、Javaシステムレベルで提供されるキャッシュのようなものです。8つの基本データ型の定数プールはシステムによって調整されます。文字列型の定数プールは特別です。保存には主に2つの方法があります。

  • 二重引用符で直接宣言された文字列オブジェクトは、定数プールに直接格納されます。
  • Stringオブジェクトが二重引用符で宣言されていない場合は、Stringが提供するintern()メソッドを使用できます。

JDK 7以降、内部文字列はJavaヒープの永続的な生成では割り当てられなくなりました(永続的な生成のガベージコレクションの頻度は低くなります)が、Javaヒープの主要部分(老いも若きも)に割り当てられ、すべての文字列はOnに格納されます。ヒープと、アプリケーションによって作成された他のオブジェクト。この変更により、メインのJavaヒープに存在するデータが増え、永続生成のデータが少なくなるため、ヒープのサイズを変更する必要がある場合があります。

ここに画像の説明を挿入

この変更の明らかな効果は、多くのクラスをロードするか、String.intern()メソッドを多用する大規模なアプリケーションで見られます。

例:

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
}

ここに画像の説明を挿入

文字列連結操作

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

例1:

public static void test1() {
    
    
    // 都是常量,前端编译期会进行代码优化
    String s1 = "a" + "b" + "c";  
    String s2 = "abc"; 

    // true,有上述可知,s1和s2实际上指向字符串常量池中的同一个值
    System.out.println(s1 == s2); 
}

クラスファイルに逆コンパイルすると、String s1 = "abc"が見つかります。これは、コードがコンパイル時に最適化されていることを示しています。
ここに画像の説明を挿入

例2:

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

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";    
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4); // true 编译期优化
    System.out.println(s3 == s5); // false s1是变量,不能编译期优化
    System.out.println(s3 == s6); // false s2是变量,不能编译期优化
    System.out.println(s3 == s7); // false s1、s2都是变量
    System.out.println(s5 == s6); // false s5、s6 不同的对象实例
    System.out.println(s5 == s7); // false s5、s7 不同的对象实例
    System.out.println(s6 == s7); // false s6、s7 不同的对象实例

    String s8 = s6.intern();
    System.out.println(s3 == s8); // true intern之后,s8和s3一样,指向字符串常量池中的"javaEEhadoop"
}

変数スプライシングの原理:
2つの変数の場合:String s1 =“ a”; String s2 =“ b”;追加:実行の詳細は次のとおりです
。①StringBuilders= new StringBuilder();
②s.append( "a");
③s.append( "b");
④s.toString();

例3:

public void test3(){
    
    
    String s0 = "ab";
    String s1 = "a";
    String s2 = "b";
    String s3 = s1 + s2;
    System.out.println(s0 == s3); // false s3指向对象实例,s0指向字符串常量池中的"ab"
    String s7 = "cd";
    final String s4 = "c";
    final String s5 = "d";
    String s6 = s4 + s5;
    System.out.println(s6 == s7); // true s4和s5是final修饰的,编译期就能确定s6的值了
}

文字列ビルダーは、必ずしも文字列のスプライシング操作に使用されるとは限りません。最終的な変更が使用される場合、それは一定であり、コードの最適化はコンパイラーで実行されます。最終変更が使用されていない場合、それは変数であり、新しいStringBuilderを介してスプライスされます。実際の開発では、finalを使ってみてください。

例4:

文字列連結操作のパフォーマンスの比較:

public class Test{
    
        
	public static void main(String[] args) {
    
            
		int times = 40000;        
		
		long start = System.currentTimeMillis();        
		
		testString(times);    // String  6963ms    
		//testStringBuilder(times); // StringBuilder    2ms             
		
		long end = System.currentTimeMillis();        
		System.out.println("String: " + (end-start) + "ms");        
		
	
	}    
	
	public static void testString(int times) {
    
            
		String str = "";        
		for (int i = 0; i < times; i++) {
    
                
			str += "test";        
		}    
	}    
	
	public static void testStringBuilder(int times) {
    
            
		StringBuilder sb = new StringBuilder();        
		for (int i = 0; i < times; i++) {
    
                
			sb.append("test");        
		}    
	}    

}

結果: StringBuilderのappend()メソッドを使用して文字列を追加する効率は、Stingの文字列スプライシングメソッドを使用する効率よりもはるかに高くなります。

詳細: StringBuilderのappend()メソッド:最初から最後まで1つのStringBuilderオブジェクトのみが作成されています。Stringの文字列スプライシングメソッドを使用すると、実行プロセス中に複数のStringBuilderオブジェクトとStringオブジェクトが作成され、大量のメモリを占有します。実行すると、より多くの時間がかかります。

intern()の使用

intern():文字列オブジェクトを文字列プールに入れようとします。最初に、対応する文字列値が文字列定数プールにあるかどうかを確認します。存在する場合は、定数プール内の文字列のアドレスを返します。ない場合は、存在する場合は、定数プールに含まれます。文字列を追加して、対応するアドレスを返します。

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

インターンされた文字列は、メモリ内に文字列のコピーが1つしかないことを保証します。これにより、メモリスペースを節約し、文字列操作タスクの実行を高速化できます。この値は文字列インターンプールに保存されることに注意してください。

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


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

String s = new String("ab");2つのオブジェクトが作成されます。ヒープスペース内の新しいオブジェクトと、文字列定数プール内の文字列定数 "ab"(この時点で定数が文字列定数プールにすでに存在する場合は作成されません)
ここに画像の説明を挿入
インタビューの質問:新規String( "a")+ new String( "b")はいくつのオブジェクトを作成しますか?
ここに画像の説明を挿入


インターンの利用:
ここに画像の説明を挿入

jdk6でのinternの使用:false
ここに画像の説明を挿入
false①2String s = new String("1")つのオブジェクトを作成しました(新しいオブジェクト、文字列定数)
②s.intern()「1」は文字列定数プールにすでに存在するため、sはヒープスペースs2のオブジェクトアドレスを指しますヒープスペースの定数プール内の「1」のアドレスを指します
③:s3変数レコードのアドレスは次のとおりですnew String("11");が、文字列定数プール内に文字列は生成されません"11"
④:s3.intern()は文字列定数内にありますpool("11"新しいオブジェクト)
⑤:s4は文字列定数プール内"11"のアドレスを指しているため、s3とs4が指すアドレスは異なります

jdk7 / 8でのインターンの使用:false true

ここに画像の説明を挿入
jdk7 / 8では、s3.intern()はすでに存在するためnew String("11");、定数プールに「11」の参照アドレスを生成し、new String("11");s4は、前の行のときに定数プールに生成された「11」の参照を指します。コードの実行アドレスであるため、s3とs4の両方が同じアドレスを指します。

概要:
①:jdk6では、文字列プールに1つあると、入れられません。既存の文字列プール内のオブジェクトのアドレスを返します。そうでない場合は、オブジェクトのコピーを作成して文字列プールに入れ、文字列プール内のオブジェクトアドレスを返します。
②:jdk7 / 8では、プールに文字列があると入れられません。既存の文字列定数プール内のオブジェクトのアドレスを返しますそうでない場合は、オブジェクトの参照アドレスをコピーして文字列プールに入れ、文字列プールに参照アドレスを返します。


演習:
ここに画像の説明を挿入
jdk6の場合:s.intern()は文字列定数プールに文字列 "ab"を作成し、s2は "ab"を指し、実行結果:true false
ここに画像の説明を挿入
jdk7 / 8の場合:s.intern()は文字列を作成しません " ab "ですが、新しいString(" ab ");を指す参照が作成され、sとs2の両方がこのアドレスを指します。実行結果:true true
ここに画像の説明を挿入


おすすめ

転載: blog.csdn.net/Lzy410992/article/details/118707321