更新時(shí)間:2022-08-25 來(lái)源:黑馬程序員 瀏覽量:
1 String類(lèi)的底層演變
?。?) JDK8以及之前版本
(2)JDK9以及之后版本
```java
JDK8的字符串存儲(chǔ)在char類(lèi)型的數(shù)組里面,在java中,一個(gè)char類(lèi)型占兩個(gè)字節(jié)。但是很多時(shí)候,一個(gè)字符只需要一個(gè)字節(jié)就可存儲(chǔ),比如各種字母什么的,兩個(gè)字節(jié)存儲(chǔ)勢(shì)必會(huì)浪費(fèi)空間,JDK9的一個(gè)優(yōu)化就在這,內(nèi)存的優(yōu)化,所以JDK9之后字符串改成byte類(lèi)型數(shù)組進(jìn)行存儲(chǔ)。
private final byte coder;
在JDK9的String類(lèi)中,新增了一個(gè)屬性coder,它是一個(gè)編碼格式的標(biāo)識(shí),使用LATIN1還是UTF16,這個(gè)是在String生成的時(shí)候自動(dòng)確定的,如果字符串中都是能用LATIN1編碼表示,那coder的值就是0,否則就是UTF16編碼,coder的值就是1。
可以看到JDK9在這方面的優(yōu)化,在較多情況下不包含那些奇奇怪怪的字符的時(shí)候,足以應(yīng)付,而這個(gè)空間卻小了1byte,實(shí)現(xiàn)了String空間的壓縮。
2 String常量池的演變
2.1 StringTable變化
String 的 String Pool是一個(gè)固定大小的 Hashtable。
在jdk6中,StringTable的長(zhǎng)度固定為1009。
如果放進(jìn) String Pool的String非常多,就會(huì)造成Hash沖突嚴(yán)重,從而導(dǎo)致鏈表會(huì)很長(zhǎng),而鏈表長(zhǎng)了后直接會(huì)造成的影響就是當(dāng)調(diào)用 intern() 時(shí)性能會(huì)大幅下降。
從jdk7起,StringTable的長(zhǎng)度默認(rèn)值是60013。
使用-XX:StringTableSize可設(shè)置StringTable的長(zhǎng)度。
在jdk8之前,對(duì)StringTableSize的設(shè)置沒(méi)有最小限制。
jdk8開(kāi)始,StringTable可設(shè)置的最小值是1009。
驗(yàn)證:
通過(guò) jps 命令查看進(jìn)程號(hào)
使用 jinfo -flag StringTableSize 進(jìn)程號(hào) 查看StringTable大小
```
2.2 內(nèi)存位置變化
Java6及以前,字符串常量池存放在永久代。
Java7開(kāi)始,字符串常量池的位置調(diào)整到Java堆內(nèi)。
所有的字符串都保存在堆(Heap)中,和其他普通對(duì)象一樣,這樣在進(jìn)行調(diào)優(yōu)應(yīng)用時(shí)僅需要調(diào)整堆大小就可以了。
```
官網(wǎng)說(shuō)明
https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes
JDK6環(huán)境下測(cè)試:
/*
jdk6中,修改JVM內(nèi)存大?。?
-XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
*/
public class StringTableTest {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
int i=0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
}
執(zhí)行結(jié)果異常信息:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
```
JDK7環(huán)境下測(cè)試:
/*
jdk7中,修改JVM內(nèi)存大?。?
-XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m -XX:-UseGCOverheadLimit
*/
public class StringTableTest {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
int i=0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
}
執(zhí)行結(jié)果異常信息:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:331)
at java.lang.String.valueOf(String.java:2954)
at StringTableTest.main(StringTableTest.java:14)
```
3 String的拼接原理
3.1 拼接原理
源代碼:
public static void main(String[] args) {
String s1 ="hello";
String s2 ="world";
String s3 = s1+s2;
System.out.println(s3);
}
```
使用 JDK8 編譯后字節(jié)碼:
0 ldc #2 <hello>
2 astore_1
3 ldc #3 <world>
5 astore_2
6 new #4 <java/lang/StringBuilder>
9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init>>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append>
17 aload_2
18 invokevirtual #6 <java/lang/StringBuilder.append>
21 invokevirtual #7 <java/lang/StringBuilder.toString>
24 astore_3
25 getstatic #8 <java/lang/System.out>
28 aload_3
29 invokevirtual #9 <java/io/PrintStream.println>
32 return
```
使用 JDK9 編譯后字節(jié)碼:
0 ldc #2 <hello>
2 astore_1
3 ldc #3 <world>
5 astore_2
6 aload_1
7 aload_2
8 invokedynamic #4 <makeConcatWithConstants, BootstrapMethods #0>
13 astore_3
14 getstatic #5 <java/lang/System.out>
17 aload_3
18 invokevirtual #6 <java/io/PrintStream.println>
21 return
```
結(jié)論:
```java
JDK8及之前,字符串變量的拼接,底層使用的是StringBuilder對(duì)象,利用append方法進(jìn)行拼接。
(注:jdk1.4之前使用StringBuffer)
JDK9以后的編譯器已經(jīng)改成使用動(dòng)態(tài)指令invokedynamic,
調(diào)用StringConcatFactory.makeConcatWithConstants方法進(jìn)行字符串拼接優(yōu)化。
```
3.2 核心方法
makeConcatWithConstants方法在StringConcatFactory類(lèi)中定義。
makeConcatWithConstants內(nèi)部調(diào)用了doStringConcat,
而doStringConcat方法則調(diào)用了generate方法來(lái)生成MethodHandle;
generate根據(jù)不同的STRATEGY來(lái)生成MethodHandle,這些STRATEGY(策略)有
BC_SB(等價(jià)于JDK8的優(yōu)化方式)
BC_SB_SIZED
BC_SB_SIZED_EXACT
MH_SB_SIZED
MH_SB_SIZED_EXACT
MH_INLINE_SIZED_EXACT(默認(rèn))
前五種策略本質(zhì)還是用StringBuilder的實(shí)現(xiàn),而默認(rèn)的策略MH_INLINE_SIZED_EXACT是直接使用字節(jié)數(shù)組來(lái)操作,并且字節(jié)數(shù)組長(zhǎng)度預(yù)先計(jì)算好,可以減少字符串復(fù)制操作。
可以通過(guò)添加JVM參數(shù)來(lái)改變默認(rèn)的策略,例如將策略改為BC_SB
-Djava.lang.invoke.stringConcat=BC_SB
-Djava.lang.invoke.stringConcat.debug=true
```
源碼:
==makeConcatWithConstants內(nèi)部調(diào)用了doStringConcat方法==
==doStringConcat方法則調(diào)用了generate方法來(lái)生成MethodHandle==
==generate根據(jù)不同的STRATEGY來(lái)生成MethodHandle==
==這些STRATEGY(策略)分別是==
private enum Strategy {
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder}.
*/
BC_SB,
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder};
* but trying to estimate the required storage.
*/
BC_SB_SIZED,
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder};
* but computing the required storage exactly.
*/
BC_SB_SIZED_EXACT,
/**
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
* This strategy also tries to estimate the required storage.
*/
MH_SB_SIZED,
/**
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
* This strategy also estimate the required storage exactly.
*/
MH_SB_SIZED_EXACT,
/**
* MethodHandle-based generator, that constructs its own byte[] array from
* the arguments. It computes the required storage exactly.
*/
MH_INLINE_SIZED_EXACT
}
```
==默認(rèn)的策略MH_INLINE_SIZED_EXACT==
3.3 常見(jiàn)筆試題
/*
產(chǎn)生2個(gè)字符串對(duì)象:字符串常量池中一個(gè),堆內(nèi)存中一個(gè)。
*/
String s = new String("abc");
/*
產(chǎn)生1個(gè)字符串對(duì)象:常量池中的"abc"。
代碼在編譯階段會(huì)優(yōu)化為 String s = "abc";
*/
String s = "a"+"b"+"c";
/*
5個(gè)字符串對(duì)象
常量池:"a", "b"
堆內(nèi)存:new方式的"a",new方式的"b",new方式的"ab"
注意:常量池中不會(huì)產(chǎn)生"ab"
*/
String s = new String("a") + new String("b");
/*
jdk8及之前創(chuàng)建3個(gè)字符串對(duì)象:
常量池: "c" , "ab"
堆中: new "abc"
jdk9之后創(chuàng)建2個(gè)字符串對(duì)象:
常量池: "c"
堆中: new "abc"
*/
String s1 = "c";
String s2 = "a"+"b"+s1;
```
4 intern()方法的演變
4.1 intern()方法調(diào)用區(qū)別
public class StringDemo5 {
public static void main(String[] args) {
String s1 = new String("ab");
String s2 = "ab";
System.out.println(s1==s2); //fasle
//intern()方法從常量池中取出"ab"對(duì)象
String s1 = new String("ab").intern();
String s2 = "ab";
System.out.println(s1==s2); //true
/*
從常量池中取出和s1內(nèi)容相同的"ab"對(duì)象,此時(shí)常量池中沒(méi)有"ab"對(duì)象。
如果常量池中沒(méi)有該字符串對(duì)象:
jdk6及之前,intern()方法會(huì)創(chuàng)建新的字符串對(duì)象,放入常量池并返回新的地址。
jdk7及之后,intern()方法會(huì)將調(diào)用者對(duì)象的地址放入常量池,并返回調(diào)用者對(duì)象地址。
*/
String s1 = new String("a") + new String("b");
s1.intern();
String s2 = "ab";
System.out.println(s1==s2); //jdk6 false; jdk7之后true
}
}
```
4.2 intern()方法總結(jié)
```java
intern()方法將這個(gè)字符串對(duì)象嘗試放入常量池中,并返回地址。
jdk1.6中:
如果池中有,則不會(huì)放入,返回已有的池中的對(duì)象的地址。
如果池中沒(méi)有,則把此對(duì)象重新創(chuàng)建一份,放入池中,并返回池中新的對(duì)象地址。
jdk1.7起:
如果池中有,則不會(huì)放入,返回已有的池中的對(duì)象的地址。
如果池中沒(méi)有,則把此對(duì)象的引用地址復(fù)制一份,放入池中,并返回池中的引用地址。
```