JVM_09 字符串常量池StringTable

1.String的基本特性

  • String:字符串,使用一對""引發來表示。
    • String sl = "atguigu";//字面量的定義方式
    • String s2 = new String("hello") ;
  • String聲明爲final的, 不可被繼承
  • String實現了Serializable接口:表示字符串是支持序列化的。 實現了Comparable接口:表示String能夠比較大小
  • ==String在jdk8及之前內部定義了final char[],value用於存儲字符串數據。jdk9時改成byte[]==
    • 結論: String不再用char[] 來存儲啦,改爲了byte[] 加上編碼標記,節約了一些空間。StringBuffer和StringBuilder也作了一些修改
public final class String implements java.io.Serializable, Comparable<String>,CharSequence {
@Stable
private final byte[] value;
}
複製代碼
  • String:表明不可變的字符序列。簡稱:不可變性。
    • 當對字符串從新賦值時,須要重寫指定內存區域賦值,不能使用原有的value進行賦值。
    • 當對現有的字符串進行鏈接操做時,也須要從新指定內存區域賦值,不能使用原有的value進行賦值。
    • 當調用String的replace()方法修改指定字符或字符串時,也須要從新指定內存區域賦值,不能使用原有的value進行賦值。
  • 經過字面量的方式(區別於new)給一個字符串賦值,此時的字符串值聲明在字符串常量池中。
/**
 * 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);//
        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。若是放進StringPool的String很是多, 就會形成Hash衝突嚴重,從而致使鏈表會很長,而鏈表長了後直接會形成的影響就是當調用String. intern時性能會大幅降低。
    • 使用一XX: StringTableSize可設置StringTable的長度
    • 在jdk6中StringTable是固定的,就是1009的長度,因此若是常量池中的字符串過多就會致使效率降低很快。StringTableSize設 置沒有要求
    • 在jdk7中,StringTable的長度默認值是60013
    • jdk8開始,1009是StringTable長度可設置的最小值

2.String的內存分配

  • 在Java語言中有8種基本數據類型和一種比較特殊的類型String。這些 類型爲了使它們在運行過程當中速度更快、更節省內存,都提供了一種常量池的概念。
  • 常量池就相似一.個Java系統級別提供的緩存。8種基本數據類型的常量 池都是系統協調的,String類 型的常量池比較特殊。它的主要使用方法有兩種。
    • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
      • 好比: String info = "abc" ;
    • 若是不是用雙引號聲明的String對象,可使用String提供的intern()方法。這個後面重點談
  • Java 6及之前,字符串常量池存放在永久代。
  • Java 7中Oracle的工程師對字符串池的邏輯作了很大的改變,即將字符串常量池的位置調整到Java堆內。
    • 全部的字符串都保存在堆(Heap)中,和其餘普通對象同樣,這樣可讓你在進行調優應用時僅須要調整堆大小就能夠了。
    • 字符串常量池概念本來使用得比較多,可是這個改動使得咱們有足夠的理由讓咱們從新考慮在Java 7中使用String. intern()。
  • Java8元空間,字符串常量在堆

StringTable爲何要調整
①永久代permSize默認比較小;
②永久代的垃圾回收頻率低;java

3.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
}
複製代碼

4.字符串拼接操做

  • 1.常量與常量的拼接結果在常量池,原理是編譯期優化
  • 2.常量池中不會存在相同內容的常量。
  • 3.==只要其中有一個是變量,結果就在堆中==。變量拼接的原理是StringBuilder
  • 4.若是拼接的結果調用intern()方法,則主動將常量池中尚未的字符串對象放入池中,並返回此對象地址。
@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
    }

    @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
    }
複製代碼

字符串拼接面試

@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
    }

    /*
    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;
        System.out.println(s3 == s4);//true
    }
    
    //練習:
    @Test
    public void test5(){
        String s1 = "javaEEhadoop";
        String s2 = "javaEE";
        String s3 = s2 + "hadoop";
        System.out.println(s1 == s3);//false

        final String s4 = "javaEE";//s4:常量
        String s5 = s4 + "hadoop";
        System.out.println(s1 == s5);//true

    }
    
複製代碼

拼接操做與append的效率對比

append效率要比字符串拼接高不少數組

/*
    體會執行效率:經過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
        }
//        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()的使用

若是不是用雙引號聲明的String對象,可使用String提供的intern方法: intern方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。緩存

  • 好比: String myInfo = new String("I love u").intern();
    也就是說,若是在任意字符串上調用String. intern方法,那麼其返回結果所指向的那個類實例,必須和直接以常量形式出現的字符串實例徹底相同。所以,下 列表達式的值一定是true: ("a" + "b" + "c").intern()== "abc";
    通俗點講,Interned String就是確保字符串在內存裏只有一份拷貝,這樣能夠節約內存空間,加快字符串操做任務的執行速度。注意,這個值會被存放在字符串內部池(String Intern Pool)。

new String("ab")會建立幾個對象,new String("a")+new String("b")呢

public class StringNewTest {
    public static void main(String[] args) {
//        String str = new String("ab");

        String str = new String("a") + new String("b");
    }
}
複製代碼
  • new String("ab")會建立幾個對象?看字節碼,就知道是兩個。
    • 一個對象是:new關鍵字在堆空間建立的
    • 另外一個對象是:字符串常量池中的對象"ab"。 字節碼指令:ldc
  • new String("a") + new String("b")呢?
    • 對象1:new StringBuilder()
    • 對象2: new String("a")
    • 對象3: 常量池中的"a"
    • 對象4: new String("b")
    • 對象5: 常量池中的"b"
  • 深刻剖析: StringBuilder的toString():
    • 對象6 :new String("ab")
    • 強調一下,toString()的調用,在字符串常量池中,沒有生成"ab"

關於String.intern()的面試題

/**
 * 如何保證變量s指向的是字符串常量池中的數據呢?
 * 有兩種方式:
 * 方式一: String s = "shkstart";//字面量定義的方式
 * 方式二: 調用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 *
 */
public class StringIntern {
    public static void main(String[] args) {
        String s = new String("1");
        String s1 = s.intern();//調用此方法以前,字符串常量池中已經存在了"1"
        String s2 = "1";
        //s  指向堆空間"1"的內存地址
        //s1 指向字符串常量池中"1"的內存地址
        //s2 指向字符串常量池已存在的"1"的內存地址  因此 s1==s2
        System.out.println(s == s2);//jdk6:false   jdk7/8:false
        System.out.println(s1 == s2);//jdk6: true   jdk7/8:true
        System.out.println(System.identityHashCode(s));//491044090
        System.out.println(System.identityHashCode(s1));//644117698
        System.out.println(System.identityHashCode(s2));//644117698

        //s3變量記錄的地址爲:new String("11")
        String s3 = new String("1") + new String("1");
        //執行完上一行代碼之後,字符串常量池中,是否存在"11"呢?答案:不存在!!

        //在字符串常量池中生成"11"。如何理解:jdk6:建立了一個新的對象"11",也就有新的地址。
        //         jdk7:此時常量中並無建立"11",而是建立一個指向堆空間中new String("11")的地址
        s3.intern();
        //s4變量記錄的地址:使用的是上一行代碼代碼執行時,在常量池中生成的"11"的地址
        String s4 = "11";
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

}
複製代碼
6 7

拓展bash

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起,將這個字符串對象嘗試放入串池。
    • ➢若是字符串常量池中有,則並不會放入。返回已有的串池中的對象的地址
    • ➢若是沒有,則會把對象的引用地址複製一份,放入串池,並返回串池中的引用地址

練習

練習1

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
    }
}
複製代碼

jdk6app

8

jdk7/8
9 10ide

練習2

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); //false
    }
}
複製代碼

intern()效率測試

大的網站平臺,須要內存中存儲大量的字符串。好比社交網站,不少人都存儲:北京市、海淀區等信息。這時候若是字符串都調用 intern()方法,就會明顯下降內存的大小。oop

/**
 * 使用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();
    }
}
複製代碼

6.StrtingTable的垃圾回收

/**
 * String的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 *
 */
public class StringGCTest {
    public static void main(String[] args) {
//        for (int j = 0; j < 100; j++) {
//            String.valueOf(j).intern();
//        }
        //發生垃圾回收行爲
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}
複製代碼
11

7.G1中的String去重操做

  • 背景:對許多Java應用(有大的也有小的)作的測試得出如下結果:
    • ➢堆存活數據集合裏面String對象佔了25%
    • ➢堆存活數據集合裏面重複的String對象有13.5%
    • ➢String對象的平均長度是45
  • 許多大規模的Java應用的瓶頸在於內存,測試代表,在這些類型的應用 裏面,Java堆中存活的數據集合差很少258是String對象。更進一一步,這裏面差很少一半String對象是重複的,重複的意思是說: string1. equals (string2)=true。堆上存在重複的string對象必然是一種內存的浪費。這個項目將在G1垃圾收集器中實現自動持續對重複的String對象進行去重,這樣就能避免浪費內存。

實現

  • ➢當垃圾收集器工做的時候,會訪問堆上存活的對象。對每個訪問的對象都會檢查是不是候選的要去重的String對象。
  • ➢若是是,把這個對象的一個引用插入到隊列中等待後續的處理。一個去重的線程在後臺運行,處理這個隊列。處理隊列的一個元素意味着從隊列刪除這個元素,而後嘗試去重它引用的String對象。
  • ➢使用一個hashtable來記錄全部的被String對象使用的不重複的char數組。 當去重的時候,會查這個hashtable,來看堆上是否已經存在一個如出一轍的char數組。
  • ➢若是存在,String對象會被調整引用那個數組,釋放對原來的數組的引用,最終會被垃圾收集器回收掉。
  • ➢若是查找失敗,char數組會被插入到hashtable,這樣之後的時候就能夠共享這個數組了。

命令行選項

  • ➢UseStringDeduplication (bool) :開啓String去重,默認是不開啓的,須要手動開啓。
  • ➢PrintStringDedupl icationStatistics (bool) :打印詳細的去重統計信息,
  • ➢StringDedupl icationAgeThreshold (uintx) :達到這個年齡的string對象被認.爲是去重的候選對象
相關文章
相關標籤/搜索