目錄
- 第 13 章 StringTable
- 1、String 的基本特性
- 1.1、String 概述
- 1.2、String 的基本特征
- 1.3、String 的底層結構
- 2、String 的記憶體分配
- 2.1、String 記憶體分配演程序序
- 2.2、為什么要調整 String 位置
- 3、String 的基本操作
- 4、字串拼接操作
- 4.1、符串拼接操作的結論
- 4.2、字串拼接的底層細節
- 5、intern() 的使用
- 5.1、intern() 方法的說明
- 5.2、new String() 的說明
- 5.3、有點難的面試題
- 5.4、intern() 方法的總結
- 5.5、intern() 方法的練習
- 5.6、intern() 方法效率測驗
- 6、StringTable 的垃圾回收
- 7、G1 中的 String 去重操作
- 1、String 的基本特性
微信搜一搜: 全堆疊小劉,獲取文章全套 pdf版
第 13 章 StringTable
1、String 的基本特性
1.1、String 概述
String 的概述
- String:字串,使用一對 "" 引起來表示
String s1 = "mogublog" ;
String s2 = new String("moxi");
- 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";
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
- 位元組碼指令
- 取字串 "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
</java></java></java></java></java></java></hello></abc></abc>
當對現有的字串進行連接操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值
- 代碼
@Test
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2 += "def";
System.out.println(s2);
System.out.println(s1);
}
- 位元組碼指令:拼接操作通過 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
</java></java></java></java></java></java></def></java></java></java></abc></abc>
當呼叫string的replace()方法修改指定字符或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值
@Test
public void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);
System.out.println(s2);
}
來看看 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;
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 的不可變性
- 代碼
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);
System.out.println(ex.ch);
}
}
- 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 的長度
- 代碼
public class StringTest2 {
public static void main(String[] args) {
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 長度下,程式的性能
- 代碼
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 = https://www.cnblogs.com/spiritmark/p/br.readLine()) != null) {
data.intern();
}
long end = System.currentTimeMillis();
System.out.println("花費的時間為:" + (end - start));
} 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 記憶體分配的演程序序
- 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堆中的資料更多,駐留在永久生成中的資料更少,因此可能需要調整堆大小,
代碼示例
- 代碼
public class StringTest3 {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
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類實體,
題目一
- 代碼
public class StringTest4 {
public static void main(String[] args) {
System.out.println();
System.out.println("1");
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");
System.out.println("1");
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 個字串常量

- 加載 換行符

- 加載了字串常量 "1"~"9"

- 加載字串常量 "10"

- 之后的字串"1" 到 "10"不會再次加載

- 程式啟動時已經加載了 2330 個字串常量
題目二
- 代碼
class Memory {
public static void main(String[] args) {
int i = 1;
Object obj = new Object();
Memory mem = new Memory();
mem.foo(obj);
}
private void foo(Object param) {
String str = param.toString();
System.out.println(str);
}
}
- 分析運行時記憶體(foo() 方法是實體方法,其實圖中少了一個 this 區域變數)

4、字串拼接操作
4.1、符串拼接操作的結論
字串拼接操作的結論
- 常量與常量的拼接結果在常量池,原理是編譯期優化
- 常量池中不會存在相同內容的變數
- 拼接前后,只要其中有一個是變數,結果就在堆中,變數拼接的原理是StringBuilder
- 如果拼接的結果呼叫intern()方法,則主動將常量池中還沒有的字串物件放入池中,并回傳此物件地址
- 如果存在,則回傳字串在常量池中的地址
- 如果字串常量池中不存在該字串,則在常量池中創建一份,并回傳此物件的地址
常量與常量的拼接結果在常量池,原理是編譯期優化
- 代碼
@Test
public void test1() {
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
- 從位元組碼指令看出:編譯器做了優化,將 "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
</java></java></java></java></java></abc></abc>
- IDEA 反編譯 class 檔案后,來看這個問題

拼接前后,只要其中有一個是變數,結果就在堆中
呼叫 intern() 方法,則主動將字串物件存入字串常量池中,并將其地址回傳
- 代碼
@Test
public 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);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s3 == s7);
System.out.println(s5 == s6);
System.out.println(s5 == s7);
System.out.println(s6 == s7);
String s8 = s6.intern();
System.out.println(s3 == s8);
}
- 從位元組碼角度來看:拼接前后有變數,都會使用到 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
</java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></java></javaee></java></java></java></java></hadoop></java></java></java></javaeehadoop></javaeehadoop></hadoop></javaee>
4.2、字串拼接的底層細節
字串拼接的底層細節
代碼示例 1
- 代碼
@Test
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
- 位元組碼指令
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
</java></java></java></java></java></java></java></ab></b></a>
- 分析拼接的步驟
- new StringBuilder()
9 new #9 <java lang stringbuilder>
12 dup
13 invokespecial #10 <java lang stringbuilder.<init>>
</java></java>
- 加載字串變數,進行 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>
</java></java></java>
- 呼叫 StringBuilder 類的 toString() 方法,轉換為字串,并存盤在區域變數中
24 invokevirtual #12 <java lang stringbuilder.tostring>
27 astore 4
</java>
代碼示例 2
- 代碼
@Test
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
- 從位元組碼角度來看:為變數 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
</java></java></ab></ab></b></a>
- IDEA 反編譯結果

課后練習
- 代碼
@Test
public void test5(){
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);
final String s4 = "javaEE";
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);
}
- 位元組碼指令:
ldc #8 <javaeehadoop></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
</java></java></javaeehadoop></javaee></java></java></java></java></hadoop></java></java></java></javaee></javaeehadoop>
拼接操作與 append 操作的效率對比
- 代碼
@Test
public void test6(){
long start = System.currentTimeMillis();
method2(100000);
long end = System.currentTimeMillis();
System.out.println("花費的時間為:" + (end - start));
}
public void method1(int highLevel){
String srchttps://www.cnblogs.com/spiritmark/p/= "";
for(int i = 0;i < highLevel;i++){
src = https://www.cnblogs.com/spiritmark/p/src +"a";
}
}
public void method2(int highLevel){
StringBuilder src = https://www.cnblogs.com/spiritmark/p/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,需要花費額外的時間,
- 改進的空間:
- 在實際開發中,如果基本確定要前前后后添加的字串長度不高于某個限定值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
</java></java></a></java></java></java>
- 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
</java></a></java></java>
關于 StringBuilder 構造器
- StringBuilder 構造器:可傳入一個 int 型別的變數,用于初始化內部的 char[] 陣列
public StringBuilder(int capacity) {
super(capacity);
}
- AbstractStringBuilder(StringBuilder 的父類)的構造器
AbstractStringBuilder(int capacity) {
value = https://www.cnblogs.com/spiritmark/p/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")會創建幾個物件?
- 代碼
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
</java></ab></java>
0 new #2 <java lang string></java>:在堆中創建了一個 String 物件4 ldc #3 <ab></ab>:在字串常量池中放入 "ab"(如果之前字串常量池中沒有 "ab" 的話)

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>
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
</java></java></java></b></java></java></java></a></java></java></java>
- 位元組碼指令分析:
0 new #2 <java lang stringbuilder></java>:拼接字串會創建一個 StringBuilder 物件7 new #4 <java lang string></java>:創建 String 物件,對應于 new String("a")11 ldc #5:在字串常量池中放入 "a"(如果之前字串常量池中沒有 "a" 的話)19 new #4 <java lang string></java>:創建 String 物件,對應于 new String("b")23 ldc #8:在字串常量池中放入 "b"(如果之前字串常量池中沒有 "b" 的話)31 invokevirtual #9 <java lang stringbuilder.tostring></java>:呼叫 StringBuilder 的 toString() 方法,會生成一個 String 物件

深入剖析 StringBuilder 的toString() 方法
- toString() 方法
@Override
public String toString() {
return new String(value, 0, count);
}
- value 是個 char[] 陣列
char[] value;
5.3、有點難的面試題
有點難的面試題
- 代碼
public class StringIntern {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
}
記憶體分析
- JDK6 :正常眼光判斷即可
- new String() 即在堆中
- str.intern() 則把字串放入常量池中

- JDK7/8 :這就有點不一樣了
- new String() 即在堆中
- str.intern() 則把字串放入常量池中,出于節省空間的目的,如果 str 不存在于字串常量池中,則將 str 在堆中的參考存盤在字串常量池中,沒錯,字串常量池中存的是 str 在堆中的參考,所以 s3 == s4 為 true

面試題的拓展
public class StringIntern1 {
public static void main(String[] args) {
String s3 = new String("1") + new String("1");
String s4 = "11";
String s5 = s3.intern();
System.out.println(s3 == s4);
System.out.println(s5 == s4);
}
}
5.4、intern() 方法的總結
關于 intern() 的總結
- JDK1.6中,將這個字串物件嘗試放入串池,
- 如果串池中有,則并不會放入,回傳已有的串池中的物件的地址
- 如果沒有,會 把此物件復制一份,放入串池,并回傳串池中的物件地址
- JDK1.7起,將這個字串物件嘗試放入串池,
- 如果串池中有,則并不會放入,回傳已有的串池中的物件的地址
- 如果沒有,則會 把物件的參考地址復制一份,放入串池,并回傳串池中的參考地址
5.5、intern() 方法的練習
intern() 方法的課后練習
練習 1
- 代碼
public class StringExer1 {
public static void main(String[] args) {
String s = new String("a") + new String("b");
String s2 = s.intern();
System.out.println(s2 == "ab");
System.out.println(s == "ab");
}
}
- JDK 6 中:在串池中創建一個字串"ab"

- JDK 7/8 中:串池中沒有創建字串"ab",而是創建一個參考,指向new String("ab"),將此參考回傳

練習 2
- 代碼
public class StringExer1 {
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
String s2 = s.intern();
System.out.println(s2 == "ab");
System.out.println(s == "ab");
}
}
- 記憶體分析

練習 3
- 代碼 1
public class StringExer2 {
public static void main(String[] args) {
String s1 = new String("ab");
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2);
}
}
- 代碼 2
public class StringExer2 {
public static void main(String[] args) {
String s1 = new String("a") + new String("b");
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);
}
}
5.6、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 = https://www.cnblogs.com/spiritmark/p/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])).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 的垃圾回收
- 代碼
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物件被認為是去重的候選物件
你只管學習,我來負責記筆記?? 關注公眾號! ,更多筆記,等你來拿,謝謝





轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/170563.html
標籤:Java
上一篇:CDH5部署三部曲之一:準備作業
