更新時(shí)間:2022-08-25 來源:黑馬程序員 瀏覽量:
1 String類的底層演變
?。?) JDK8以及之前版本
(2)JDK9以及之后版本
```java
JDK8的字符串存儲在char類型的數(shù)組里面,在java中,一個(gè)char類型占兩個(gè)字節(jié)。但是很多時(shí)候,一個(gè)字符只需要一個(gè)字節(jié)就可存儲,比如各種字母什么的,兩個(gè)字節(jié)存儲勢必會浪費(fèi)空間,JDK9的一個(gè)優(yōu)化就在這,內(nèi)存的優(yōu)化,所以JDK9之后字符串改成byte類型數(shù)組進(jìn)行存儲。
private final byte coder;
在JDK9的String類中,新增了一個(gè)屬性coder,它是一個(gè)編碼格式的標(biāo)識,使用LATIN1還是UTF16,這個(gè)是在String生成的時(shí)候自動確定的,如果字符串中都是能用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的長度固定為1009。 如果放進(jìn) String Pool的String非常多,就會造成Hash沖突嚴(yán)重,從而導(dǎo)致鏈表會很長,而鏈表長了后直接會造成的影響就是當(dāng)調(diào)用 intern() 時(shí)性能會大幅下降。 從jdk7起,StringTable的長度默認(rèn)值是60013。 使用-XX:StringTableSize可設(shè)置StringTable的長度。 在jdk8之前,對StringTableSize的設(shè)置沒有最小限制。 jdk8開始,StringTable可設(shè)置的最小值是1009。 驗(yàn)證: 通過 jps 命令查看進(jìn)程號 使用 jinfo -flag StringTableSize 進(jìn)程號 查看StringTable大小 ```
2.2 內(nèi)存位置變化
Java6及以前,字符串常量池存放在永久代。 Java7開始,字符串常量池的位置調(diào)整到Java堆內(nèi)。 所有的字符串都保存在堆(Heap)中,和其他普通對象一樣,這樣在進(jìn)行調(diào)優(yōu)應(yīng)用時(shí)僅需要調(diào)整堆大小就可以了。 ```
官網(wǎng)說明
https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes
JDK6環(huán)境下測試:
/* 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)境下測試:
/* 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對象,利用append方法進(jìn)行拼接。
(注:jdk1.4之前使用StringBuffer)
JDK9以后的編譯器已經(jī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方法來生成MethodHandle; generate根據(jù)不同的STRATEGY來生成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ù)組來操作,并且字節(jié)數(shù)組長度預(yù)先計(jì)算好,可以減少字符串復(fù)制操作。 可以通過添加JVM參數(shù)來改變默認(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方法來生成MethodHandle==
==generate根據(jù)不同的STRATEGY來生成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è)字符串對象:字符串常量池中一個(gè),堆內(nèi)存中一個(gè)。 */ String s = new String("abc"); /* 產(chǎn)生1個(gè)字符串對象:常量池中的"abc"。 代碼在編譯階段會優(yōu)化為 String s = "abc"; */ String s = "a"+"b"+"c"; /* 5個(gè)字符串對象 常量池:"a", "b" 堆內(nèi)存:new方式的"a",new方式的"b",new方式的"ab" 注意:常量池中不會產(chǎn)生"ab" */ String s = new String("a") + new String("b"); /* jdk8及之前創(chuàng)建3個(gè)字符串對象: 常量池: "c" , "ab" 堆中: new "abc" jdk9之后創(chuàng)建2個(gè)字符串對象: 常量池: "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"對象 String s1 = new String("ab").intern(); String s2 = "ab"; System.out.println(s1==s2); //true /* 從常量池中取出和s1內(nèi)容相同的"ab"對象,此時(shí)常量池中沒有"ab"對象。 如果常量池中沒有該字符串對象: jdk6及之前,intern()方法會創(chuàng)建新的字符串對象,放入常量池并返回新的地址。 jdk7及之后,intern()方法會將調(diào)用者對象的地址放入常量池,并返回調(diào)用者對象地址。 */ 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è)字符串對象嘗試放入常量池中,并返回地址。
jdk1.6中:
如果池中有,則不會放入,返回已有的池中的對象的地址。
如果池中沒有,則把此對象重新創(chuàng)建一份,放入池中,并返回池中新的對象地址。
jdk1.7起:
如果池中有,則不會放入,返回已有的池中的對象的地址。
如果池中沒有,則把此對象的引用地址復(fù)制一份,放入池中,并返回池中的引用地址。
```