天天看點

第十三章 - StringTable

第十三章 - StringTable

文章目錄

  • ​​第十三章 - StringTable​​
  • ​​1.String的基本特性​​
  • ​​1.1 String在jdk9中存儲結構變更​​
  • ​​1.2 String的基本特性​​
  • ​​2.String的記憶體配置設定​​
  • ​​3.String的基本操作​​
  • ​​4.字元串拼接操作​​
  • ​​5.intern( )的使用​​
  • ​​5.1 面試題​​
  • ​​5.2 intern的使用:JDK6 vs JDK7/8​​
  • ​​5.2.1 練習(對JDK不同版本intern的進一步了解)​​
  • ​​5.3 intern的效率測試:空間角度​​
  • ​​6.StringTable的垃圾回收​​
  • ​​7.G1中的String去重操作​​

1.String的基本特性

  • String:字元串,使用一對" "引起來表示
String s1 = “baidu”; //字面量的定義方式
String s2 = new String("hello");      
  • String聲明為final的,不可被繼承
  • String實作了Serializable接口:表示字元串是支援序列化的
  • String實作了Comparable接口:表示string可以比較大小
  • String在jdk8及以前内部定義了final char[ ] value用于存儲字元串資料。JDK9時改為byte[ ]

1.1 String在jdk9中存儲結構變更

官網位址:​​JEP 254: Compact Strings (java.net)​​

Motivation

The current implementation of the ​

​String​

​​ class stores characters in a ​

​char​

​​ array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most ​

​String​

​​ objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal ​

​char​

​​ arrays of such ​

​String​

​ objects is going unused.

Description

We propose to change the internal representation of the ​

​String​

​​ class from a UTF-16 ​

​char​

​​ array to a ​

​byte​

​​ array plus an encoding-flag field. The new ​

​String​

​ class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

String-related classes such as ​

​AbstractStringBuilder​

​​, ​

​StringBuilder​

​​, and ​

​StringBuffer​

​ will be updated to use the same representation, as will the HotSpot VM’s intrinsic string operations.

This is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.

The prototyping work done to date confirms the expected reduction in memory footprint, substantial reductions of GC activity, and minor performance regressions in some corner cases.

動機

該類的目前實作​

​String​

​​将字元存儲在一個 ​

​char​

​​數組中,每個字元使用兩個位元組(十六位)。從許多不同應用程式收集的資料表明,字元串是堆使用的主要組成部分,而且大多數​

​String​

​​對象僅包含 Latin-1 字元。此類字元僅需要一個位元組的存儲空間,是以此類對象的内部​

​char​

​​數組中的 一半空間未使用。​

​String​

描述

我們建議将​

​String​

​​類的内部表示從 UTF-16​

​char​

​​數組更改為​

​byte​

​​數組加上編碼标志字段。新​

​String​

​類将根據字元串的内容存儲編碼為 ISO-8859-1/Latin-1(每個字元一個位元組)或 UTF-16(每個字元兩個位元組)的字元。編碼标志将訓示使用哪種編碼。

與字元串相關的類(例如​

​AbstractStringBuilder​

​​、​

​StringBuilder​

​​和 )​

​StringBuffer​

​将被更新為使用相同的表示形式,HotSpot VM 的内在字元串操作也是如此。

這純粹是一個實作更改,對現有的公共接口沒有任何更改。沒有計劃添加任何新的公共 API 或其他接口。

迄今為止完成的原型設計工作證明了記憶體占用的預期減少、GC 活動的大幅減少以及在某些極端情況下的輕微性能回歸。

結論:String再也不用char[ ] 來存儲了,改成了byte[ ] 加上編碼标記,節約了一些空間

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

1.2 String的基本特性

  • String:代表不可變的字元序列。簡稱:不可變性
  • 當對字元串重新指派時,需要重寫指定記憶體區域指派,不能使用原有的value進行指派
  • 當對現有的字元串進行連接配接操作時,也需要重新指定記憶體區域指派,不能使用原有的value進行指派
  • 當調用string的replace( )方法修改指定字元或字元串時,也需要重新指定記憶體區域指派,不能使用原有的value進行指派
  • 通過字面量的方式(差別于new)給一個字元串指派,此時的字元串值聲明在字元串常量池中
  • 字元串常量池是不會存儲相同内容的字元串的
  • String的String Pool是一個固定大小的Hashtable,預設值大小長度是1009。如果放進String Pool的String非常多,就會造成Hash沖突嚴重,進而導緻連結清單會很長,而連結清單長了後直接會造成的影響就是當調用String.intern時性能會大幅下降
  • 使用​

    ​-XX:StringTablesize​

    ​可設定StringTable的長度
  • 在JDK6中StringTable是固定的,就是1009的長度,是以如果常量池中的字元串過多就會導緻效率下降很快。​

    ​StringTablesize​

    ​設定沒有要求
  • 在JDK7中,StringTable的長度預設值是60013,​

    ​StringTablesize​

    ​設定沒有要求
  • 在JDK8開始,設定StringTable長度的話,1009是可以設定的最小值
代碼示例:展現 String 的不可變性
/**
 * String的基本使用:展現String的不可變性
 */
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); //hello
        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
    }

}      
在JDK8開始,設定StringTable長度的話,1009是可以設定的最小值
public static void main(String[] args) {
        //測試StringTableSize參數
//        System.out.println("我來打個醬油");
//        try {
//            Thread.sleep(1000000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
}      
  • 預設什麼都不設定,運作
  • 指令行輸入指令檢視 StringTable 的長度
jps
jinfo -flag StringTableSize      
第十三章 - StringTable
  • 可以看到預設長度就是60013
  • 設定JVM參數
-XX:StringTableSize=1000      
  • 結果報錯啦~
第十三章 - StringTable

StringTable 大小為 1000 無效;必須介于 1009 和 2305843009213693951 之間

String 筆試題:考察對String不可變性的認識

public class StringExer {

    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringExer ex = new StringExer();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str); //good
        System.out.println(ex.ch); //best
    }

}      
測試StringTable大小對性能的影響
  • 先産生10萬個字元串
/**
 * 産生10萬個長度不超過10的字元串,包含a-z,A-Z
 */
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;
    }

}      
  • 再将這 10萬 個字元串存入字元串常量池中,測試不同的StringTable對性能的影響
/**
 *  -XX:StringTableSize=1009
 */
public class StringTest2 {

    public static void main(String[] args) {
        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();

            System.out.println("花費的時間為:" + (end - start)); //1009:128ms  10000:51ms
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(br != null){
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    }

}      
  • 設定 StringTable 大小為預設最小值 1009
-XX:StringTableSize=1009      
  • 結果為:128ms
花費的時間為:128      
  • 再将 StringTable 大小設定為 10000
-XX:StringTableSize=10000      
  • 結果為:51ms
花費的時間為:51      

2.String的記憶體配置設定

  • 在Java語言中有8種基本資料類型和一種比較特殊的類型String。這些類型為了使它們在運作過程中速度更快、更節省記憶體,都提供了一種常量池的概念。
  • 常量池就類似一個Java系統級别提供的緩存。8種基本資料類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種。
  • 直接使用雙引号聲明出來的String對象會直接存儲在常量池中。
  • 比如:​

    ​String info = “baidu.com”​

    ​;
  • 如果不是用雙引号聲明的String對象,可以使用String提供的intern( )方法。這個後面重點談
  • Java 6及以前,字元串常量池存放在永久代
  • Java 7中 Oracle的工程師對字元串池的邏輯做了很大的改變,即将字元串常量池的位置調整到Java堆内
  • 所有的字元串都儲存在堆(Heap)中,和其他普通對象一樣,這樣可以讓你在進行調優應用時僅需要調整堆大小就可以了。
  • 字元串常量池概念原本使用得比較多,但是這個改動使得我們有足夠的理由讓我們重新考慮在Java 7中使用​

    ​String.intern()​

  • Java8元空間,字元串常量在堆空間中
第十三章 - StringTable
第十三章 - StringTable
第十三章 - StringTable
StringTable為什麼要調整?
  • permSize預設比較小
  • 永久代垃圾回收頻率低

官網位址:​​Java SE 7 Features and Enhancements (oracle.com)​​

Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the ​

​String.intern()​

​ method will see more significant differences.

簡介:在JDK 7中,内部字元串不再配置設定在Java堆的永久代中,而是配置設定在Java堆的主要部分(稱為年輕代和老年代),與應用程式建立的其他對象一起。這種變化将導緻更多的資料駐留在主Java堆中,而更少的資料在永久代中,是以可能需要調整堆的大小。大多數應用程式将看到由于這一變化而導緻的堆使用的相對較小的差異,但加載許多類或大量使用String.intern( )方法的大型應用程式将看到更明顯的差異。

代碼示例

/**
 * jdk6中:
 * -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
 *
 * jdk8中:
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 */
public class StringTest3 {

    public static void main(String[] args) {
        //使用Set保持着常量池引用,避免full gc回收常量池行為
        Set<String> set = new HashSet<String>();
        //在short可以取值的範圍内足以讓6MB的PermSize或heap産生OOM了。
        short i = 0;
        while(true){
            set.add(String.valueOf(i++).intern());
        }
    }

}      
  • 設定JVM參數
-XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m      
  • 可以看到OOM是發生在堆空間中,是以字元串常量池在JDK8中确實是存在堆空間中的
第十三章 - StringTable

3.String的基本操作

Java語言規範裡要求完全相同的字元串字面量,應該包含同樣的Unicode字元序列(包含同一份碼點序列的常量),并且必須是指向同一個String類執行個體。
public class StringTest4 {

    public static void main(String[] args) {
        System.out.println();//1230
        System.out.println("1");//1231
        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");//1240
        //如下的字元串"1" 到 "10"不會再次加載
        System.out.println("1");//1241
        System.out.println("2");//1241
        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");//1241
    }

}      
  • 對一些代碼進行打斷點操作
  • 初始化有 1230 個字元串
第十三章 - StringTable
  • 執行字元串”1”,可以發現字元串數量變成了1231個
第十三章 - StringTable
  • 執行字元串”10”,可以發現字元串數量變成了1240個
第十三章 - StringTable
  • 下面相同的字元串都在字元串常量池加載過一次了,是以下面相同的字元串都不會再被加載了
第十三章 - StringTable
代碼示例2
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

}      
第十三章 - StringTable
  • 上面圖檔的局部變量表還缺少幾個參數,下面列出正确的局部變量表數量
第十三章 - StringTable
第十三章 - StringTable

4.字元串拼接操作

  • 常量與常量的拼接結果在常量池,原理是編譯期優化
  • 常量池中不會存在相同内容的變量
  • 隻要其中有一個是變量,結果就在堆中。變量拼接的原理是​

    ​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
    }      
  • 反編譯位元組碼檔案
第十三章 - StringTable
  • 可以看到 String s4 = s1 + s2; 相當于new了一個 StringBuilder,然後使用append拼接 s1 和 s2 字元串,最後再使用toString( )方法約等于new 了一個String對象存放在堆中,這裡要和字元串常量池區分開,s3存放在字元串常量池中,s4存放在堆中,是以 s3 不等于 s4
知識補充:在 JDK 5 之後,使用的是 StringBuilder,在 JDK 5 之前使用的是 StringBuffer
String StringBuffer StringBuilder
String 的值是不可變的,這就導緻每次對 String 的操作都會生成新的 String 對象,不僅效率低下,而且浪費大量優先的記憶體空間 StringBuffer 是可變類,和線程安全的字元串操作類,任何對它指向的字元串的操作都不會産生新的對象。每個 StringBuffer 對象都有一定的緩沖區容量,當字元串大小沒有超過容量時,不會配置設定新的容量,當字元串大小超過容量時,會自動增加容量 可變類,速度更快
不可變 可變 可變
線程安全 線程不安全
多線程操作字元串 單線程操作字元串
代碼示例4
/*
    1. 字元串拼接操作不一定使用的是StringBuilder!
       如果拼接符号左右兩邊都是字元串常量或常量引用,則仍然使用編譯期優化,即非StringBuilder的方式。
    2. 針對于final修飾類、方法、基本資料類型、引用資料類型的量的結構時,能使用上final的時候建議使用上。
     */
    @Test
    public void test4(){
        final String s1 = "a";
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; //s4:常量
        System.out.println(s3 == s4);//true
    }      
  • 注意,我們左右兩邊如果是變量的話,就是需要 new StringBuilder 進行拼接,但是如果使用的是 final 修飾,則是從常量池中擷取。是以說拼接符号左右兩邊都是字元串常量或常量引用 則仍然使用編譯器優化。也就是說被 final 修飾的變量,将會變成常量,類和方法将不能被繼承。
  • 在開發中,能夠使用 final 的時候,建議使用上
代碼示例5
/*
    體會執行效率:通過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);//5046
        method2(100000);//6

        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
        }
//        System.out.println(src);
    }

    public void method2(int highLevel){
        //隻需要建立一個StringBuilder
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");
        }
//        System.out.println(src);
    }      

5.intern( )的使用

官方API文檔中的解釋

public String intern( )

Returns a canonical representation for the string object.

A pool of strings, initially empty, is maintained privately by the class ​

​String​

​.

When the intern method is invoked, if the pool already contains a string equal to this ​

​String​

​​ object as determined by the ​

​[equals(Object)](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#equals-java.lang.Object-)​

​​ method, then the string from the pool is returned. Otherwise, this ​

​String​

​​ object is added to the pool and a reference to this ​

​String​

​ object is returned.

It follows that for any two strings ​

​s​

​​ and ​

​t​

​​, ​

​s.intern() == t.intern()​

​​ is ​

​true​

​​ if and only if ​

​s.equals(t)​

​​ is ​

​true​

​.

All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.

  • **Returns:**a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

當調用intern方法時,如果池子裡已經包含了一個與這個String對象相等的字元串,正如equals(Object)方法所确定的,那麼池子裡的字元串會被傳回。否則,這個String對象被添加到池中,并傳回這個String對象的引用。

由此可見,對于任何兩個字元串s和t,當且僅當s.equals(t)為真時,s.intern( ) == t.intern( )為真。

所有字面字元串和以字元串為值的常量表達式都是interned。

傳回一個與此字元串内容相同的字元串,但保證是來自一個唯一的字元串池。

  • ​intern()​

    ​ 是一個 native 方法,調用的是底層 C 的方法。
public native String intern();      
  • 如果不是用雙引号聲明的String對象,可以使用String提供的intern方法,它會從字元串常量池中查詢目前字元串是否存在,若不存在就會将目前字元串放入常量池中。
String myInfo = new string("I love alibaba").intern();      
  • 也就是說,如果在任意字元串上調用String.intern方法,那麼其傳回結果所指向的那個類執行個體,必須和直接以常量形式出現的字元串執行個體完全相同。是以,下清單達式的值必定是true
("a"+"b"+"c").intern() == "abc"      
  • 通俗點講,Interned string就是確定字元串在記憶體裡隻有一份拷貝,這樣可以節約記憶體空間,加快字元串操作任務的執行速度。注意,這個值會被存放在字元串内部池(String Intern Pool)
/**
 * 如何保證變量s指向的是字元串常量池中的資料呢?
 * 有兩種方式:
 * 方式一: String s = "shkstart";//字面量定義的方式
 * 方式二: 調用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 */      
第十三章 - StringTable

5.1 面試題

new String(“ab”)會建立幾個對象
/**
 * new String("ab") 會建立幾個對象? 
 * 看位元組碼就知道是2個對象
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}      
  • 我們轉換成位元組碼來檢視
第十三章 - StringTable
  • 這裡面就是兩個對象
  • 一個對象是:new 關鍵字在堆空間中建立
  • 另一個對象:字元串常量池中的對象**“ab”**
new String(“a”) + new String(“b”) 會建立幾個對象
/**
 * new String("a") + new String("b") 會建立幾個對象? 
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}      
  • 我們轉換成位元組碼來檢視
0 new #2 <java/lang/StringBuilder> //new StringBuilder()
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
 7 new #4 <java/lang/String> //new String()
10 dup
11 ldc #5 <a> //常量池中的 “a”
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V> //new String("a")
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> //append()
19 new #4 <java/lang/String> //new String()
22 dup
23 ldc #8 <b> //常量池中的 “b”
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V> //new String("b")
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> //append()
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> //toString()裡面會new一個String對象
34 astore_1
35 return      
  • 我們建立了 6 個對象
  • 對象1:​

    ​new StringBuilder()​

  • 對象2:​

    ​new String("a")​

  • 對象3:常量池中的 “a”
  • 對象4:​

    ​new String("b")​

  • 對象5:常量池中的 “b”
  • 對象6:toString 中會建立一個​

    ​new String("ab")​

  • toString( )的調用,在字元串常量池中,沒有生成"ab"
toString( )的調用,在字元串常量池中,沒有生成"ab"
  • StringBuilder中toString( )源碼
@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }      
  • 我們轉換成位元組碼來檢視
第十三章 - StringTable
  • 可以看到toString( )裡面隻是new了一個String對象,并沒有存放到字元串常量池中

5.2 intern的使用:JDK6 vs JDK7/8

public class StringIntern {

    public static void main(String[] args) {

        /**
         * ① String s = new String("1")
         * 建立了兩個對象
         *       堆空間中一個new對象
         *       字元串常量池中一個字元串常量"1"(注意:此時字元串常量池中已有"1")
         * ② s.intern()由于字元串常量池中已存在"1"
         *
         * s  指向的是堆空間中的對象位址
         * s2 指向的是堆空間中常量池中"1"的位址
         * 是以不相等
         */
        String s = new String("1");
        s.intern();//調用此方法之前,字元串常量池中已經存在了"1"
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false   jdk7/8:false

        /**
         * ① String s3 = new String("1") + new String("1")
         * 等價于new String("11"),但是,常量池中并不生成字元串"11";
         *
         * ② s3.intern()
         * 由于此時常量池中并無"11",是以把s3中記錄的對象的位址存入常量池
         * 是以s3 和 s4 指向的都是一個位址
         */
        String s3 = new String("1") + new String("1");//s3變量記錄的位址為:new String("11")
        //執行完上一行代碼以後,字元串常量池中,是否存在"11"呢?答案:不存在!!
        s3.intern();//在字元串常量池中生成"11"。如何了解:jdk6:在常量池中真正建立了一個新的對象"11",也就有新的位址。
                                            //         jdk7:此時常量池中并沒有真正建立"11",而是建立一個指向堆空間中new String("11")的位址
        String s4 = "11";//s4變量記錄的位址:使用的是上一行代碼代碼執行時,在常量池中生成的"11"的位址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

}      
JDK 6 中
第十三章 - StringTable
JDK 7 中
第十三章 - StringTable
拓展:jdk8環境
public class StringIntern1 {

    public static void main(String[] args) {
        //StringIntern.java中練習的拓展:
        String s3 = new String("1") + new String("1");//new String("11")
        //執行完上一行代碼以後,字元串常量池中,是否存在"11"呢?答案:不存在!!
        String s4 = "11";//在字元串常量池中生成對象"11"
        String s5 = s3.intern();
        System.out.println(s3 == s4);//false
        System.out.println(s5 == s4);//true
    }

}      
總結String的intern( )的使用:
  • JDK1.6中,将這個字元串對象嘗試放入字元串常量池中。
  • 如果字元串常量池中有,則并不會放入。傳回已有的字元串常量池中的對象的位址
  • 如果沒有,會把此對象複制一份,放入字元串常量池,并傳回字元串常量池中的對象位址
  • JDK1.7起,将這個字元串對象嘗試放入字元串常量池中。
  • 如果字元串常量池中有,則并不會放入。傳回已有的字元串常量池中的對象的位址
  • 如果沒有,則會把對象的引用位址複制一份,放入字元串常量池,并傳回字元串常量池中的引用位址

5.2.1 練習(對JDK不同版本intern的進一步了解)

練習1
public class StringExer1 {
    
    public static void main(String[] args) {
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代碼執行完以後,字元串常量池中并沒有"ab"

        String s2 = s.intern();//jdk6中:在字元串常量池中建立一個字元串"ab",并把字元串常量池中的"ab"位址傳回給s2
                               //jdk8中:字元串常量池中沒有建立字元串"ab",而是建立一個引用,指向new String("ab"),将此引用傳回給s2

        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
    
}      
第十三章 - StringTable
第十三章 - StringTable
練習2
第十三章 - StringTable
練習3:jdk8環境
public class StringExer2 {

    public static void main(String[] args) {
        String s1 = new String("a") + new String("b"); //執行完以後,不會在字元串常量池中會生成"ab"
        s1.intern(); //此時字元串常量池中存放的是堆空間中對象的引用
        String s2 = "ab"; //指向字元串常量池中的引用位址
        System.out.println(s1 == s2); //true
    }

}      
public class StringExer2 {

    public static void main(String[] args) {
        String s1 = new String("ab");//執行完以後,會在字元串常量池中會生成"ab"
        s1.intern(); //此時字元串常量池中存放上一行代碼生成的字元串常量的對象位址
        String s2 = "ab"; //指向字元串常量池中的對象位址
        System.out.println(s1 == s2); //false
    }

}      

5.3 intern的效率測試:空間角度

/**
 * 使用intern()測試執行效率:空間使用上
 */
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();
    }
}      
  • 運作結果
不使用intern:7215ms
使用intern:1542ms      
  • 不使用intern的情況下,産生了1千多萬個String的執行個體對象
第十三章 - StringTable
  • 使用intern的情況下,隻産生了2百多萬個String的執行個體對象
第十三章 - StringTable
結論
  • 對于程式中大量使用存在的字元串時,尤其存在很多已經重複的字元串時,使用intern( )方法能夠節省記憶體空間。
  • 大的網站平台,需要記憶體中存儲大量的字元串。比如社交網站,很多人都存儲:北京市、海澱區等資訊。這時候如果字元串都調用intern( )方法,就會很明顯降低記憶體的大小。

6.StringTable的垃圾回收

/**
 * String的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 */
public class StringGCTest {

    public static void main(String[] args) {
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }

}      
第十三章 - StringTable

7.G1中的String去重操作

官網位址:​​JEP 192: String Deduplication in G1 (java.net)​​

Motivation

Many large-scale Java applications are currently bottlenecked on memory. Measurements have shown that roughly 25% of the Java heap live data set in these types of applications is consumed by ​

​String​

​​ objects. Further, roughly half of those ​

​String​

​​ objects are duplicates, where duplicates means ​

​string1.equals(string2)​

​​ is true. Having duplicate ​

​String​

​​ objects on the heap is, essentially, just a waste of memory. This project will implement automatic and continuous ​

​String​

​ deduplication in the G1 garbage collector to avoid wasting memory and reduce the memory footprint.

目前,許多大規模的Java應用程式在記憶體上遇到了瓶頸。測量表明,在這些類型的應用程式中,大約25%的Java堆實時資料集被​

​String'對象所消耗。此外,這些 "String "對象中大約有一半是重複的,其中重複意味着 "string1.equals(string2) "是真的。在堆上有重複的​

​String’對象,從本質上講,隻是一種記憶體的浪費。這個項目将在G1垃圾收集器中實作自動和持續的`String’重複資料删除,以避免浪費記憶體,減少記憶體占用。

注意這裡說的重複,指的是在堆中的資料,而不是常量池中的,因為常量池中的本身就不會重複

背景:對許多Java應用(有大的也有小的)做的測試得出以下結果:

  • 堆存活資料集合裡面string對象占了25%
  • 堆存活資料集合裡面重複的string對象有13.5%
  • string對象的平均長度是45
許多大規模的Java應用的瓶頸在于記憶體,測試表明,在這些類型的應用裡面,Java堆中存活的資料集合差不多25%是String對象。更進一步,這裡面差不多一半string對象是重複的,重複的意思是說: ​

​string1.equals(string2) == true​

​。堆上存在重複的String對象必然是一種記憶體的浪費。這個項目将在G1垃圾收集器中實作自動持續對重複的string對象進行去重,這樣就能避免浪費記憶體。
  • 當垃圾收集器工作的時候,會通路堆上存活的對象。對每一個通路的對象都會檢查是否是候選的要去重的String對象
  • 如果是,把這個對象的一個引用插入到隊列中等待後續的處理。一個去重的線程在背景運作,處理這個隊列。處理隊列的一個元素意味着從隊列删除這個元素,然後嘗試去重它引用的string對象。
  • 使用一個hashtable來記錄所有的被String對象使用的不重複的char數組。當去重的時候,會查這個hashtable,來看堆上是否已經存在一個一模一樣的char數組。
  • 如果存在,String對象會被調整引用那個數組,釋放對原來的數組的引用,最終會被垃圾收集器回收掉。
  • 如果查找失敗,char數組會被插入到hashtable,這樣以後的時候就可以共享這個數組了。
# 開啟String去重,預設是不開啟的,需要手動開啟。 
UseStringDeduplication(bool)  
# 列印詳細的去重統計資訊 
PrintStringDeduplicationStatistics(bool)  
# 達到這個年齡的String對象被認為是去重的候選對象
StringpeDuplicationAgeThreshold(uintx)