第 13 章 StringTable
第 13 章 StringTable
1、String 的基本特性
1.1、String 概述
String 的概述
-
String:字串,使用一對 “” 引起來表示
String s1 = "mogublog" ; // 字面量的定義方式 String s2 = new String("moxi"); // new 物件的方式
-
String宣告為final的,不可被繼承
-
String實現了Serializable介面:表示字串是支援序列化的。實現了Comparable介面:表示String可以比較大小
-
string在jdk8及以前內部定義了final char[] value用於儲存字串資料。JDK9時改為byte[]
為什麼 JDK9 改變了 String 的結構
官方文件
http://openjdk.java.net/jeps/254
為什麼改為 byte[] 儲存?
- String類的當前實現將字元儲存在char陣列中,每個字元使用兩個位元組(16位)。
- 從許多不同的應用程式收集的資料表明,字串是堆使用的主要組成部分,而且大多數字符串物件只包含拉丁字元。這些字元只需要一個位元組的儲存空間,因此這些字串物件的內部char陣列中有一半的空間將不會使用。
- 之前 String 類使用 UTF-16 的 char[] 陣列儲存,現在改為 byte[] 陣列 外加一個編碼標誌位儲存,該編碼標誌將指定 String 類中 byte[] 陣列的編碼方式
- 結論:String再也不用char[] 來儲存了,改成了byte [] 加上編碼標記,節約了一些空間
- 同時基於String的資料結構,例如StringBuffer和StringBuilder也同樣做了修改
// 之前
private final char value[];
// 之後
private final byte[] value
1.2、String 的基本特徵
String 的基本特徵
String:代表不可變的字元序列。簡稱:不可變性。
- 當對字串重新賦值時,需要重寫指定記憶體區域賦值,不能使用原有的value進行賦值。
- 當對現有的字串進行連線操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
- 當呼叫String的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
通過字面量的方式(區別於new)給一個字串賦值,此時的字串值宣告在字串常量池中。
當對字串重新賦值時,需要重寫指定記憶體區域賦值,不能使用原有的value進行賦值
- 程式碼
@Test
public void test1() {
String s1 = "abc";//字面量定義的方式,"abc"儲存在字串常量池中
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);//判斷地址:false
System.out.println(s1);//hello
System.out.println(s2);//abc
}
- 位元組碼指令
- 取字串 “abc” 時,使用的是同一個符號引用:#2
- 取字串 “hello” 時,使用的是另一個符號引用:#3
0 ldc #2 <abc>
2 astore_1
3 ldc #2 <abc>
5 astore_2
6 ldc #3 <hello>
8 astore_1
9 getstatic #4 <java/lang/System.out>
12 aload_1
13 aload_2
14 if_acmpne 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 invokevirtual #5 <java/io/PrintStream.println>
25 getstatic #4 <java/lang/System.out>
28 aload_1
29 invokevirtual #6 <java/io/PrintStream.println>
32 getstatic #4 <java/lang/System.out>
35 aload_2
36 invokevirtual #6 <java/io/PrintStream.println>
39 return
當對現有的字串進行連線操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值
- 程式碼
@Test
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s2);//abcdef
System.out.println(s1);//abc
}
- 位元組碼指令:拼接操作通過 StringBuilder 的 append() 方法完成
0 ldc #2 <abc>
2 astore_1
3 ldc #2 <abc>
5 astore_2
6 new #7 <java/lang/StringBuilder>
9 dup
10 invokespecial #8 <java/lang/StringBuilder.<init>>
13 aload_2
14 invokevirtual #9 <java/lang/StringBuilder.append>
17 ldc #10 <def>
19 invokevirtual #9 <java/lang/StringBuilder.append>
22 invokevirtual #11 <java/lang/StringBuilder.toString>
25 astore_2
26 getstatic #4 <java/lang/System.out>
29 aload_2
30 invokevirtual #6 <java/io/PrintStream.println>
33 getstatic #4 <java/lang/System.out>
36 aload_1
37 invokevirtual #6 <java/io/PrintStream.println>
40 return
當呼叫string的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值
@Test
public void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);//abc
System.out.println(s2);//mbc
}
來看看 replace() 方法的原始碼
- new String(buf, true); 後,返回新的 String 物件
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
課後練習:String 的不可變性
- 程式碼
/**
* @author shkstart shkstart@126.com
* @create 2020 23:44
*/
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
}
}
- str 的內容並沒有變:“test ok” 位於字串常量池中的另一個區域(地址),進行賦值操作並沒有修改原來 str 指向的引用的內容
good
best
1.3、String 的底層結構
String 底層 Hashtable 結構的說明
字串常量池是不會儲存相同內容的字串的
- String的String Pool是一個固定大小的Hashtable,預設值大小長度是1009。如果放進String Pool的String非常多,就會造成Hash衝突嚴重,從而導致連結串列會很長,而連結串列長了後直接會造成的影響就是當呼叫String.intern()方法時效能會大幅下降。
- 使用-XX:StringTablesize可設定StringTable的長度
- 在JDK6中StringTable是固定的,就是1009的長度,所以如果常量池中的字串過多就會導致效率下降很快,StringTablesize設定沒有要求
- 在JDK7中,StringTable的長度預設值是60013,StringTablesize設定沒有要求
- 在JDK8中,StringTable的長度預設值是60013,StringTable可以設定的最小值為1009
程式碼示例:設定 StringTable 的長度
- 程式碼
/**
* -XX:StringTableSize=1009
*
* @author shkstart shkstart@126.com
* @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();
}
}
}
通過 -XX:StringTableSize 設定 StringTable 長度
- JVM 引數
-XX:StringTableSize=6666
- jinfo 檢視變數值
jps
jinfo -flag StringTableSize 程式id
測試不同 StringTable 長度下,程式的效能
- 程式碼
/**
* -XX:StringTableSize=1009
*
* @author shkstart shkstart@126.com
* @create 2020 23:53
*/
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的字串的話,則在常量池中生成
data.intern();
}
long end = System.currentTimeMillis();
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();
}
}
}
}
}
- -XX:StringTableSize=1009 :程式耗時 143ms
- -XX:StringTableSize=100009 :程式耗時 47ms
2、String 的記憶體分配
2.1、String 記憶體分配演進過程
String 型別
- 在Java語言中有8種基本資料型別和一種比較特殊的型別String。這些型別為了使它們在執行過程中速度更快、更節省記憶體,都提供了一種常量池的概念。
- 常量池就類似一個Java系統級別提供的快取。8種基本資料型別的常量池都是系統協調的,String型別的常量池比較特殊。它的主要使用方法有兩種。
- 直接使用雙引號宣告出來的String物件會直接儲存在常量池中。比如:
String info="atguigu.com";
- 如果不是用雙引號宣告的String物件,可以使用String提供的intern()方法。
- 直接使用雙引號宣告出來的String物件會直接儲存在常量池中。比如:
String 記憶體分配的演進過程
- Java 6及以前,字串常量池存放在永久代
- Java 7中 Oracle的工程師對字串池的邏輯做了很大的改變,即將字串常量池的位置調整到Java堆內
- 所有的字串都儲存在堆(Heap)中,和其他普通物件一樣,這樣可以讓你在進行調優應用時僅需要調整堆大小就可以了。
- 字串常量池概念原本使用得比較多,但是這個改動使得我們有足夠的理由讓我們重新考慮在Java 7中使用String.intern()。
- Java8元空間,字串常量在堆
2.2、為什麼要調整 String 位置
StringTable 為什麼要調整?
官方文件
https://www.oracle.com/java/technologies/javase/jdk7-relnotes.html#jdk7changes
- 為什麼要調整位置?
- 永久代的預設比較小
- 永久代垃圾回收頻率低
- 堆中空間足夠大,字串可被及時回收
- 在JDK 7中,interned字串不再在Java堆的永久代中分配,而是在Java堆的主要部分(稱為年輕代和年老代)中分配,與應用程式建立的其他物件一起分配。
- 此更改將導致駐留在主Java堆中的資料更多,駐留在永久生成中的資料更少,因此可能需要調整堆大小。
程式碼示例
- 程式碼
/**
* jdk6中:
* -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
*
* jdk8中:
* -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
* @author shkstart shkstart@126.com
* @create 2020 0:36
*/
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());
}
}
}
- 異常日誌說:我真沒騙你,字串真的在堆中(JDK8)
"C:\Program Files\Java\jdk1.8.0_144\bin\java" -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\lib\idea_rt.jar=1799:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;C:\Users\Heygo\Desktop\JVMDemo\out\production\chapter13;D:\JavaTools\apache-maven-3.3.9\repository\junit\junit\4.12\junit-4.12.jar;D:\JavaTools\apache-maven-3.3.9\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar" com.atguigu.java.StringTest3
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:703)
at java.util.HashMap.putVal(HashMap.java:662)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.add(HashSet.java:219)
at com.atguigu.java.StringTest3.main(StringTest3.java:22)
Process finished with exit code 1
3、String 的基本操作
核心思想
Java語言規範裡要求完全相同的字串字面量,應該包含同樣的Unicode字元序列(包含同一份碼點序列的常量),並且必須是指向同一個String類例項。
題目一
- 程式碼
/**
* @author shkstart shkstart@126.com
* @create 2020 0:49
*/
public class StringTest4 {
public static void main(String[] args) {
System.out.println();//2330
System.out.println("1");//2331
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");//2340
//如下的字串"1" 到 "10"不會再次載入
System.out.println("1");//2341
System.out.println("2");//2341
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");//2341
}
}
-
分析字串常量池的變化
- 程式啟動時已經載入了 2330 個字串常量
- 載入 換行符
- 載入了字串常量 “1”~“9”
- 載入字串常量 “10”
- 之後的字串"1" 到 "10"不會再次載入
題目二
- 程式碼
/**
* @author shkstart shkstart@126.com
* @create 2020 0:51
*/
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
}
- 分析執行時記憶體(foo() 方法是例項方法,其實圖中少了一個 this 區域性變數)
4、字串拼接操作
4.1、符串拼接操作的結論
字串拼接操作的結論
- 常量與常量的拼接結果在常量池,原理是編譯期優化
- 常量池中不會存在相同內容的變數
- 拼接前後,只要其中有一個是變數,結果就在堆中。變數拼接的原理是StringBuilder
- 如果拼接的結果呼叫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
}
- 從位元組碼指令看出:編譯器做了優化,將 “a” + “b” + “c” 優化成了 “abc”
0 ldc #2 <abc>
2 astore_1
3 ldc #2 <abc>
5 astore_2
6 getstatic #3 <java/lang/System.out>
9 aload_1
10 aload_2
11 if_acmpne 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #4 <java/io/PrintStream.println>
22 getstatic #3 <java/lang/System.out>
25 aload_1
26 aload_2
27 invokevirtual #5 <java/lang/String.equals>
30 invokevirtual #4 <java/io/PrintStream.println>
33 return
- IDEA 反編譯 class 檔案後,來看這個問題
拼接前後,只要其中有一個是變數,結果就在堆中
呼叫 intern() 方法,則主動將字串物件存入字串常量池中,並將其地址返回
- 程式碼
@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
}
- 從位元組碼角度來看:拼接前後有變數,都會使用到 StringBuilder 類
0 ldc #6 <javaEE>
2 astore_1
3 ldc #7 <hadoop>
5 astore_2
6 ldc #8 <javaEEhadoop>
8 astore_3
9 ldc #8 <javaEEhadoop>
11 astore 4
13 new #9 <java/lang/StringBuilder>
16 dup
17 invokespecial #10 <java/lang/StringBuilder.<init>>
20 aload_1
21 invokevirtual #11 <java/lang/StringBuilder.append>
24 ldc #7 <hadoop>
26 invokevirtual #11 <java/lang/StringBuilder.append>
29 invokevirtual #12 <java/lang/StringBuilder.toString>
32 astore 5
34 new #9 <java/lang/StringBuilder>
37 dup
38 invokespecial #10 <java/lang/StringBuilder.<init>>
41 ldc #6 <javaEE>
43 invokevirtual #11 <java/lang/StringBuilder.append>
46 aload_2
47 invokevirtual #11 <java/lang/StringBuilder.append>
50 invokevirtual #12 <java/lang/StringBuilder.toString>
53 astore 6
55 new #9 <java/lang/StringBuilder>
58 dup
59 invokespecial #10 <java/lang/StringBuilder.<init>>
62 aload_1
63 invokevirtual #11 <java/lang/StringBuilder.append>
66 aload_2
67 invokevirtual #11 <java/lang/StringBuilder.append>
70 invokevirtual #12 <java/lang/StringBuilder.toString>
73 astore 7
75 getstatic #3 <java/lang/System.out>
78 aload_3
79 aload 4
81 if_acmpne 88 (+7)
84 iconst_1
85 goto 89 (+4)
88 iconst_0
89 invokevirtual #4 <java/io/PrintStream.println>
92 getstatic #3 <java/lang/System.out>
95 aload_3
96 aload 5
98 if_acmpne 105 (+7)
101 iconst_1
102 goto 106 (+4)
105 iconst_0
106 invokevirtual #4 <java/io/PrintStream.println>
109 getstatic #3 <java/lang/System.out>
112 aload_3
113 aload 6
115 if_acmpne 122 (+7)
118 iconst_1
119 goto 123 (+4)
122 iconst_0
123 invokevirtual #4 <java/io/PrintStream.println>
126 getstatic #3 <java/lang/System.out>
129 aload_3
130 aload 7
132 if_acmpne 139 (+7)
135 iconst_1
136 goto 140 (+4)
139 iconst_0
140 invokevirtual #4 <java/io/PrintStream.println>
143 getstatic #3 <java/lang/System.out>
146 aload 5
148 aload 6
150 if_acmpne 157 (+7)
153 iconst_1
154 goto 158 (+4)
157 iconst_0
158 invokevirtual #4 <java/io/PrintStream.println>
161 getstatic #3 <java/lang/System.out>
164 aload 5
166 aload 7
168 if_acmpne 175 (+7)
171 iconst_1
172 goto 176 (+4)
175 iconst_0
176 invokevirtual #4 <java/io/PrintStream.println>
179 getstatic #3 <java/lang/System.out>
182 aload 6
184 aload 7
186 if_acmpne 193 (+7)
189 iconst_1
190 goto 194 (+4)
193 iconst_0
194 invokevirtual #4 <java/io/PrintStream.println>
197 aload 6
199 invokevirtual #13 <java/lang/String.intern>
202 astore 8
204 getstatic #3 <java/lang/System.out>
207 aload_3
208 aload 8
210 if_acmpne 217 (+7)
213 iconst_1
214 goto 218 (+4)
217 iconst_0
218 invokevirtual #4 <java/io/PrintStream.println>
221 return
4.2、字串拼接的底層細節
字串拼接的底層細節
程式碼示例 1
- 程式碼
@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;//"ab"
System.out.println(s3 == s4);//false
}
- 位元組碼指令
0 ldc #14 <a>
2 astore_1
3 ldc #15 <b>
5 astore_2
6 ldc #16 <ab>
8 astore_3
9 new #9 <java/lang/StringBuilder>
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init>>
16 aload_1
17 invokevirtual #11 <java/lang/StringBuilder.append>
20 aload_2
21 invokevirtual #11 <java/lang/StringBuilder.append>
24 invokevirtual #12 <java/lang/StringBuilder.toString>
27 astore 4
29 getstatic #3 <java/lang/System.out>
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>
46 return
-
分析拼接的步驟
- new StringBuilder()
9 new #9 <java/lang/StringBuilder> 12 dup 13 invokespecial #10 <java/lang/StringBuilder.<init>>
- 載入字串變數,進行 append 操作
16 aload_1 17 invokevirtual #11 <java/lang/StringBuilder.append> 20 aload_2 21 invokevirtual #11 <java/lang/StringBuilder.append> 24 invokevirtual #12 <java/lang/StringBuilder.toString>
- 呼叫 StringBuilder 類的 toString() 方法,轉換為字串,並儲存在區域性變數中
24 invokevirtual #12 <java/lang/StringBuilder.toString> 27 astore 4
程式碼示例 2
- 程式碼
/*
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
}
- 從位元組碼角度來看:為變數 s3 賦值時,直接使用 #16 符號引用,即字串常量 “ab”
0 ldc #14 <a>
2 astore_1
3 ldc #15 <b>
5 astore_2
6 ldc #16 <ab>
8 astore_3
9 ldc #16 <ab>
11 astore 4
13 getstatic #3 <java/lang/System.out>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #4 <java/io/PrintStream.println>
30 return
- IDEA 反編譯結果
課後練習
- 程式碼
//練習:
@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
}
- 位元組碼指令:
ldc #8 <javaEEhadoop>
(帶 final 的變數在編譯時就已經確定了該變數的值,當做常量來處理)
0 ldc #8 <javaEEhadoop>
2 astore_1
3 ldc #6 <javaEE>
5 astore_2
6 new #9 <java/lang/StringBuilder>
9 dup
10 invokespecial #10 <java/lang/StringBuilder.<init>>
13 aload_2
14 invokevirtual #11 <java/lang/StringBuilder.append>
17 ldc #7 <hadoop>
19 invokevirtual #11 <java/lang/StringBuilder.append>
22 invokevirtual #12 <java/lang/StringBuilder.toString>
25 astore_3
26 getstatic #3 <java/lang/System.out>
29 aload_1
30 aload_3
31 if_acmpne 38 (+7)
34 iconst_1
35 goto 39 (+4)
38 iconst_0
39 invokevirtual #4 <java/io/PrintStream.println>
42 ldc #6 <javaEE>
44 astore 4
46 ldc #8 <javaEEhadoop>
48 astore 5
50 getstatic #3 <java/lang/System.out>
53 aload_1
54 aload 5
56 if_acmpne 63 (+7)
59 iconst_1
60 goto 64 (+4)
63 iconst_0
64 invokevirtual #4 <java/io/PrintStream.println>
67 return
拼接操作與 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
}
}
public void method2(int highLevel){
//只需要建立一個StringBuilder
StringBuilder src = new StringBuilder();
for (int i = 0; i < highLevel; i++) {
src.append("a");
}
}
- 體會執行效率:通過StringBuilder的append()的方式新增字串的效率要遠高於使用String的字串拼接方式!
- 分析原因:
- StringBuilder的append()的方式:
- 自始至終中只建立過一個StringBuilder的物件
- 使用String的字串拼接方式:建立過多個StringBuilder和String的物件
- 使用String的字串拼接方式:
- 記憶體中由於建立了較多的StringBuilder和String的物件,記憶體佔用更大;
- 如果進行GC,需要花費額外的時間。
- StringBuilder的append()的方式:
- 改進的空間:
- 在實際開發中,如果基本確定要前前後後新增的字串長度不高於某個限定值highLevel的情況下,建議使用構造器例項化:
StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]
通過位元組碼分析
- method1() 方法的位元組碼指令:
- 每次 for 迴圈都會建立一個 StringBuilder 物件
- 呼叫 StringBuilder 的 toString() 方法又會建立新的 String 物件
0 ldc #23
2 astore_2
3 iconst_0
4 istore_3
5 iload_3
6 iload_1
7 if_icmpge 36 (+29)
10 new #9 <java/lang/StringBuilder>
13 dup
14 invokespecial #10 <java/lang/StringBuilder.<init>>
17 aload_2
18 invokevirtual #11 <java/lang/StringBuilder.append>
21 ldc #14 <a>
23 invokevirtual #11 <java/lang/StringBuilder.append>
26 invokevirtual #12 <java/lang/StringBuilder.toString>
29 astore_2
30 iinc 3 by 1
33 goto 5 (-28)
36 return
- method2() 方法的位元組碼指令:
0 new #9 <java/lang/StringBuilder>
3 dup
4 invokespecial #10 <java/lang/StringBuilder.<init>>
7 astore_2
8 iconst_0
9 istore_3
10 iload_3
11 iload_1
12 if_icmpge 28 (+16)
15 aload_2
16 ldc #14 <a>
18 invokevirtual #11 <java/lang/StringBuilder.append>
21 pop
22 iinc 3 by 1
25 goto 10 (-15)
28 return
關於 StringBuilder 構造器
- StringBuilder 構造器:可傳入一個 int 型別的變數,用於初始化內部的 char[] 陣列
public StringBuilder(int capacity) {
super(capacity);
}
- AbstractStringBuilder(StringBuilder 的父類)的構造器
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
5、intern() 的使用
5.1、intern() 方法的說明
intern() 方法的說明
先來點逼格,看看官方文件
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(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.
public native String intern();
關於 intern() 方法的說明
-
intern是一個native方法,呼叫的是底層C的方法
-
字串池最初是空的,由String類私有地維護。在呼叫intern方法時,如果池中已經包含了由equals(object)方法確定的與該字串物件相等的字串,則返回池中的字串。否則,該字串物件將被新增到池中,並返回對該字串物件的引用。
-
如果不是用雙引號宣告的String物件,可以使用String提供的intern方法:intern方法會從字串常量池中查詢當前字串是否存在,若不存在就會將當前字串放入常量池中。比如:
String myInfo = new string("I love atguigu").intern();
-
也就是說,如果在任意字串上呼叫String.intern方法,那麼其返回結果所指向的那個類例項,必須和直接以常量形式出現的字串例項完全相同。因此,下列表示式的值必定是true
("a"+"b"+"c").intern()=="abc"
-
通俗點講,Interned String就是確保字串在記憶體裡只有一份拷貝,這樣可以節約記憶體空間,加快字串操作任務的執行速度。注意,這個值會被存放在字串內部池(String Intern Pool)
5.2、new String() 的說明
new String(“ab”)會建立幾個物件?
- 程式碼
/**
* 題目:
* new String("ab")會建立幾個物件?看位元組碼,就知道是兩個。
* 一個物件是:new關鍵字在堆空間建立的
* 另一個物件是:字串常量池中的物件"ab"。 位元組碼指令:ldc
*
* @author shkstart shkstart@126.com
* @create 2020 20:38
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("ab");
}
}
- 位元組碼指令
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <ab>
6 invokespecial #4 <java/lang/String.<init>>
9 astore_1
10 return
0 new #2 <java/lang/String>
:在堆中建立了一個 String 物件4 ldc #3 <ab>
:在字串常量池中放入 “ab”(如果之前字串常量池中沒有 “ab” 的話)
new String(“a”) + new String(“b”) 會建立幾個物件?
- 程式碼
/**
* 思考:
* 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"
*
* @author shkstart shkstart@126.com
* @create 2020 20:38
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b");
}
}
- 位元組碼指令
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return
- 位元組碼指令分析:
0 new #2 <java/lang/StringBuilder>
:拼接字串會建立一個 StringBuilder 物件7 new #4 <java/lang/String>
:建立 String 物件,對應於 new String(“a”)11 ldc #5 <a>
:在字串常量池中放入 “a”(如果之前字串常量池中沒有 “a” 的話)19 new #4 <java/lang/String>
:建立 String 物件,對應於 new String(“b”)23 ldc #8 <b>
:在字串常量池中放入 “b”(如果之前字串常量池中沒有 “b” 的話)31 invokevirtual #9 <java/lang/StringBuilder.toString>
:呼叫 StringBuilder 的 toString() 方法,會生成一個 String 物件
深入剖析 StringBuilder 的toString() 方法
- toString() 方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
- value 是個 char[] 陣列
char[] value;
5.3、有點難的面試題
有點難的面試題
- 程式碼
/**
* 如何保證變數s指向的是字串常量池中的資料呢?有兩種方式:
* 方式一: String s = "shkstart";//字面量定義的方式
* 方式二: 呼叫intern()
* String s = new String("shkstart").intern();
* String s = new StringBuilder("shkstart").toString().intern();
*
* @author shkstart shkstart@126.com
* @create 2020 18:49
*/
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();//這方法其實沒啥屌用,呼叫此方法之前,字串常量池中已經存在"1"
String s2 = "1";
/*
jdk6:false jdk7/8:false
因為 s 指向堆空間中的 "1" ,s2 指向字元創常量池中的 "1"
*/
System.out.println(s == s2);
// 執行完下一行程式碼以後,字串常量池中,是否存在"11"呢?答案:不存在!!
String s3 = new String("1") + new String("1");//s3變數記錄的地址為:new String("11")
/*
如何理解:jdk6:建立了一個新的物件"11",也就有新的地址。
jdk7:此時常量中並沒有建立"11",而是在常量池中記錄了指向堆空間中new String("11")的地址(節省空間)
*/
s3.intern(); // 在字串常量池中生成"11"。
String s4 = "11";//s4變數記錄的地址:使用的是上一行程式碼程式碼執行時,在常量池中生成的"11"的地址
// jdk6:false jdk7/8:true
System.out.println(s3 == s4);
}
}
記憶體分析
- JDK6 :正常眼光判斷即可
- new String() 即在堆中
- str.intern() 則把字串放入常量池中
- JDK7/8 :這就有點不一樣了
- new String() 即在堆中
- str.intern() 則把字串放入常量池中,出於節省空間的目的,如果 str 不存在於字串常量池中,則將 str 在堆中的引用儲存在字串常量池中,沒錯,字串常量池中存的是 str 在堆中的引用,所以 s3 == s4 為 true
面試題的擴充
/**
* StringIntern.java中練習的擴充:
*
* @author shkstart shkstart@126.com
* @create 2020 22:10
*/
public class StringIntern1 {
public static void main(String[] args) {
//執行完下一行程式碼以後,字串常量池中,是否存在"11"呢?答案:不存在!!
String s3 = new String("1") + new String("1");//new String("11")
//在字串常量池中生成物件"11"
String s4 = "11";
String s5 = s3.intern();
// s3 是堆中的 "ab" ,s4 是字串常量池中的 "ab"
System.out.println(s3 == s4);//false
// s5 是從字串常量池中取回來的引用,當然和 s4 相等
System.out.println(s5 == s4);//true
}
}
5.4、intern() 方法的總結
關於 intern() 的總結
- JDK1.6中,將這個字串物件嘗試放入串池。
- 如果串池中有,則並不會放入。返回已有的串池中的物件的地址
- 如果沒有,會把此物件複製一份,放入串池,並返回串池中的物件地址
- JDK1.7起,將這個字串物件嘗試放入串池。
- 如果串池中有,則並不會放入。返回已有的串池中的物件的地址
- 如果沒有,則會把物件的引用地址複製一份,放入串池,並返回串池中的引用地址
5.5、intern() 方法的練習
intern() 方法的課後練習
練習 1
- 程式碼
/**
* @author shkstart shkstart@126.com
* @create 2020 20:17
*/
public class StringExer1 {
public static void main(String[] args) {
//在下一行程式碼執行完以後,字串常量池中並沒有"ab"
String s = new String("a") + new String("b");//new String("ab")
/*
jdk6中:在串池中建立一個字串"ab"
jdk8中:串池中沒有建立字串"ab",而是建立一個引用,指向new String("ab"),將此引用返回
*/
String s2 = s.intern();
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}
- JDK 6 中:在串池中建立一個字串"ab"
- JDK 7/8 中:串池中沒有建立字串"ab",而是建立一個引用,指向new String(“ab”),將此引用返回
練習 2
- 程式碼
/**
* @author shkstart shkstart@126.com
* @create 2020 20:17
*/
public class StringExer1 {
public static void main(String[] args) {
// 在這兒加一句
String x = "ab";
//在下一行程式碼執行完以後,字串常量池中並沒有"ab"
String s = new String("a") + new String("b");//new String("ab")
/*
jdk6中:在串池中建立一個字串"ab"
jdk8中:串池中沒有建立字串"ab",而是建立一個引用,指向new String("ab"),將此引用返回
*/
String s2 = s.intern();
System.out.println(s2 == "ab");//jdk6:true jdk8:true
System.out.println(s == "ab");//jdk6:false jdk8:false
}
}
- 記憶體分析
練習 3
- 程式碼 1
/**
*
* @author shkstart shkstart@126.com
* @create 2020 20:26
*/
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
}
}
- 程式碼 2
/**
*
* @author shkstart shkstart@126.com
* @create 2020 20:26
*/
public class StringExer2 {
// 物件記憶體地址可以使用System.identityHashCode(object)方法獲取
public static void main(String[] args) {
String s1 = new String("a") + new String("b");//執行完以後,不會在字串常量池中會生成"ab"
System.out.println(System.identityHashCode(s1));
s1.intern();
System.out.println(System.identityHashCode(s1));
String s2 = "ab";
System.out.println(System.identityHashCode(s2));
System.out.println(s1 == s2); // true
}
}
/* 程式執行結果
21685669
21685669
21685669
true
*/
5.6、intern() 方法效率測試
intern() 的效率測試
- 程式碼
/**
* 使用intern()測試執行效率:空間使用上
* 結論:對於程式中大量存在存在的字串,尤其其中存在很多重複字串時,使用intern()可以節省記憶體空間。
*
* @author shkstart shkstart@126.com
* @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();
}
}
- 直接 new String :由於每個 String 物件都是 new 出來的,所以程式需要維護大量存放在堆空間中的 String 例項,程式記憶體佔用也會變高
- 使用 intern() 方法:由於陣列中字串的引用都指向字串常量池中的字串,所以程式需要維護的 String 物件更少,記憶體佔用也更低
結論:
- 對於程式中大量使用存在的字串時,尤其存在很多已經重複的字串時,使用intern()方法能夠節省記憶體空間。
- 大的網站平臺,需要記憶體中儲存大量的字串。比如社交網站,很多人都儲存:北京市、海淀區等資訊。這時候如果字串都呼叫intern() 方法,就會很明顯降低記憶體的大小。
6、StringTable 的垃圾回收
- 程式碼
/**
* String的垃圾回收:
* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
*
* @author shkstart shkstart@126.com
* @create 2020 21:27
*/
public class StringGCTest {
public static void main(String[] args) {
for (int j = 0; j < 100000; j++) {
String.valueOf(j).intern();
}
}
}
- JVM 引數
-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
- 程式日誌:
- 在 PSYoungGen 區發生了垃圾回收
- Number of entries 和 Number of literals 明顯沒有 100000
- 以上兩點均說明 StringTable 區發生了垃圾回收
"C:\Program Files\Java\jdk1.8.0_144\bin\java" -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\lib\idea_rt.jar=11487:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;C:\Users\Heygo\Desktop\JVMDemo\out\production\chapter13;D:\JavaTools\apache-maven-3.3.9\repository\junit\junit\4.12\junit-4.12.jar;D:\JavaTools\apache-maven-3.3.9\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar" com.atguigu.java3.StringGCTest
[GC (Allocation Failure) [PSYoungGen: 4096K->488K(4608K)] 4096K->716K(15872K), 0.0024275 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 4608K, used 3883K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
eden space 4096K, 82% used [0x00000000ffb00000,0x00000000ffe50fb0,0x00000000fff00000)
from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 11264K, used 228K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
object space 11264K, 2% used [0x00000000ff000000,0x00000000ff039010,0x00000000ffb00000)
Metaspace used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 381K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 14158 = 339792 bytes, avg 24.000
Number of literals : 14158 = 603200 bytes, avg 42.605
Total footprint : = 1103080 bytes
Average bucket size : 0.708
Variance of bucket size : 0.711
Std. dev. of bucket size: 0.843
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 62943 = 1510632 bytes, avg 24.000
Number of literals : 62943 = 3584040 bytes, avg 56.941
Total footprint : = 5574776 bytes
Average bucket size : 1.049
Variance of bucket size : 0.824
Std. dev. of bucket size: 0.908
Maximum bucket size : 5
Process finished with exit code 0
String.valueOf() 方法原始碼
- String 類的 valueOf() 方法
public static String valueOf(int i) {
return Integer.toString(i);
}
- Integer.toString() 方法中執行了 new String() ,即在堆中建立了一個 String 物件
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
7、G1 中的 String 去重操作
官方文件
http://openjdk.java.net/jeps/192
String 去重操作的背景
- 背景:對許多Java應用(有大的也有小的)做的測試得出以下結果:
- 堆存活資料集合裡面String物件佔了25%
- 堆存活資料集合裡面重複的String物件有13.5%
- String物件的平均長度是45
- 許多大規模的Java應用的瓶頸在於記憶體,測試表明,在這些型別的應用裡面,Java堆中存活的資料集合差不多25%是String物件。更進一步,這裡面差不多一半String物件是重複的,重複的意思是說:
- str1.equals(str2)= true。堆上存在重複的String物件必然是一種記憶體的浪費。這個專案將在G1垃圾收集器中實現自動持續對重複的String物件進行去重,這樣就能避免浪費記憶體。
String 去重的的具體實現
- 當垃圾收集器工作的時候,會訪問堆上存活的物件。對每一個訪問的物件都會檢查是否是候選的要去重的String物件。
- 如果是,把這個物件的一個引用插入到佇列中等待後續的處理。一個去重的執行緒在後臺執行,處理這個佇列。處理佇列的一個元素意味著從佇列刪除這個元素,然後嘗試去重它引用的String物件。
- 使用一個Hashtable來記錄所有的被String物件使用的不重複的char陣列。當去重的時候,會查這個Hashtable,來看堆上是否已經存在一個一模一樣的char陣列。
- 如果存在,String物件會被調整引用那個陣列,釋放對原來的陣列的引用,最終會被垃圾收集器回收掉。
- 如果查詢失敗,char陣列會被插入到Hashtable,這樣以後的時候就可以共享這個陣列了。
命令列選項
- UseStringDeduplication(bool) :開啟String去重,預設是不開啟的,需要手動開啟。
- PrintStringDeduplicationStatistics(bool) :列印詳細的去重統計資訊
- stringDeduplicationAgeThreshold(uintx) :達到這個年齡的String物件被認為是去重的候選物件
相關文章
- mORMot 1.18 第13章 動態陣列ORM陣列
- JAVA 程式設計思想 第13章 字串Java程式設計字串
- 《父與子的程式設計之旅(第3版)》第13章習題答案程式設計
- JVM 中的StringTableJVM
- JS-13章-事件JS事件
- 第13章節-Python3.5-Django實現使用者登入212PythonDjango
- 【學習筆記】第13章:計算機網路名詞縮寫彙總筆記計算機網路
- 第1章 引言
- 聊聊jvm的StringTable及SymbolTableJVMSymbol
- 章13——包裝類——Math類
- 章13——包裝類——StringBuilder類UI
- 第2章 表示式
- 第2章 Python序列Python
- 第1章 概論
- 第 1 章:開始
- 第4章函式函式
- 第3章筆記筆記
- 第6章 Java APIJavaAPI
- 第1章 Why RustRust
- 每週分享第 13 期
- StringTable結構以及基本調優
- 拷貝控制c++primer13章C++
- 第17章_反射機制反射
- mORMot 1.18 第06章 概念ORM
- mORMot 1.18 第09章 AutoFreeORM
- 第8 章過程管理
- 7.10 第7章小結
- 第1章 安裝RailsAI
- 第2章 牛刀小試
- 第1章 DevOps的理想dev
- 第1章 pytest入門
- 第3章 收集輸入
- 第2章 功能實現
- 第1章 認識excelExcel
- Java學習第1章Java
- 每週分享第 13 期(20190215)
- 第15.16.17章學習筆記筆記
- 第 4 章 程式計數器