更新時(shí)間:2022-08-25 來(lái)源:黑馬程序員 瀏覽量:
1 String類的底層演變
(1) JDK8以及之前版本

(2)JDK9以及之后版本

```java
JDK8的字符串存儲(chǔ)在char類型的數(shù)組里面,在java中,一個(gè)char類型占兩個(gè)字節(jié)。但是很多時(shí)候,一個(gè)字符只需要一個(gè)字節(jié)就可存儲(chǔ),比如各種字母什么的,兩個(gè)字節(jié)存儲(chǔ)勢(shì)必會(huì)浪費(fèi)空間,JDK9的一個(gè)優(yōu)化就在這,內(nèi)存的優(yōu)化,所以JDK9之后字符串改成byte類型數(shù)組進(jìn)行存儲(chǔ)。
private final byte coder;
在JDK9的String類中,新增了一個(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è)置沒有最小限制。 jdk8開始,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開始,字符串常量池的位置調(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類中定義。 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 常見筆試題
/*
產(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í)常量池中沒有"ab"對(duì)象。
如果常量池中沒有該字符串對(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ì)象的地址。
如果池中沒有,則把此對(duì)象重新創(chuàng)建一份,放入池中,并返回池中新的對(duì)象地址。
jdk1.7起:
如果池中有,則不會(huì)放入,返回已有的池中的對(duì)象的地址。
如果池中沒有,則把此對(duì)象的引用地址復(fù)制一份,放入池中,并返回池中的引用地址。
```