更新時間:2022-08-25 來源:黑馬程序員 瀏覽量:
1 String類的底層演變
(1) JDK8以及之前版本
(2)JDK9以及之后版本
```java
JDK8的字符串存儲在char類型的數組里面,在java中,一個char類型占兩個字節。但是很多時候,一個字符只需要一個字節就可存儲,比如各種字母什么的,兩個字節存儲勢必會浪費空間,JDK9的一個優化就在這,內存的優化,所以JDK9之后字符串改成byte類型數組進行存儲。
private final byte coder;
在JDK9的String類中,新增了一個屬性coder,它是一個編碼格式的標識,使用LATIN1還是UTF16,這個是在String生成的時候自動確定的,如果字符串中都是能用LATIN1編碼表示,那coder的值就是0,否則就是UTF16編碼,coder的值就是1。
可以看到JDK9在這方面的優化,在較多情況下不包含那些奇奇怪怪的字符的時候,足以應付,而這個空間卻小了1byte,實現了String空間的壓縮。
2 String常量池的演變
2.1 StringTable變化
String 的 String Pool是一個固定大小的 Hashtable。 在jdk6中,StringTable的長度固定為1009。 如果放進 String Pool的String非常多,就會造成Hash沖突嚴重,從而導致鏈表會很長,而鏈表長了后直接會造成的影響就是當調用 intern() 時性能會大幅下降。 從jdk7起,StringTable的長度默認值是60013。 使用-XX:StringTableSize可設置StringTable的長度。 在jdk8之前,對StringTableSize的設置沒有最小限制。 jdk8開始,StringTable可設置的最小值是1009。 驗證: 通過 jps 命令查看進程號 使用 jinfo -flag StringTableSize 進程號 查看StringTable大小 ```
2.2 內存位置變化
Java6及以前,字符串常量池存放在永久代。 Java7開始,字符串常量池的位置調整到Java堆內。 所有的字符串都保存在堆(Heap)中,和其他普通對象一樣,這樣在進行調優應用時僅需要調整堆大小就可以了。 ```
官網說明
https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes
JDK6環境下測試:
/* jdk6中,修改JVM內存大小: -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()); } } } 執行結果異常信息: Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) ```
JDK7環境下測試:
/* jdk7中,修改JVM內存大小: -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()); } } } 執行結果異常信息: 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 編譯后字節碼:
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 編譯后字節碼:
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 ```
結論:
```java
JDK8及之前,字符串變量的拼接,底層使用的是StringBuilder對象,利用append方法進行拼接。
(注:jdk1.4之前使用StringBuffer)
JDK9以后的編譯器已經改成使用動態指令invokedynamic,
調用StringConcatFactory.makeConcatWithConstants方法進行字符串拼接優化。
```
3.2 核心方法
makeConcatWithConstants方法在StringConcatFactory類中定義。 makeConcatWithConstants內部調用了doStringConcat, 而doStringConcat方法則調用了generate方法來生成MethodHandle; generate根據不同的STRATEGY來生成MethodHandle,這些STRATEGY(策略)有 BC_SB(等價于JDK8的優化方式) BC_SB_SIZED BC_SB_SIZED_EXACT MH_SB_SIZED MH_SB_SIZED_EXACT MH_INLINE_SIZED_EXACT(默認) 前五種策略本質還是用StringBuilder的實現,而默認的策略MH_INLINE_SIZED_EXACT是直接使用字節數組來操作,并且字節數組長度預先計算好,可以減少字符串復制操作。 可以通過添加JVM參數來改變默認的策略,例如將策略改為BC_SB -Djava.lang.invoke.stringConcat=BC_SB -Djava.lang.invoke.stringConcat.debug=true ```
源碼:
==makeConcatWithConstants內部調用了doStringConcat方法==
==doStringConcat方法則調用了generate方法來生成MethodHandle==
==generate根據不同的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 } ```
==默認的策略MH_INLINE_SIZED_EXACT==
3.3 常見筆試題
/* 產生2個字符串對象:字符串常量池中一個,堆內存中一個。 */ String s = new String("abc"); /* 產生1個字符串對象:常量池中的"abc"。 代碼在編譯階段會優化為 String s = "abc"; */ String s = "a"+"b"+"c"; /* 5個字符串對象 常量池:"a", "b" 堆內存:new方式的"a",new方式的"b",new方式的"ab" 注意:常量池中不會產生"ab" */ String s = new String("a") + new String("b"); /* jdk8及之前創建3個字符串對象: 常量池: "c" , "ab" 堆中: new "abc" jdk9之后創建2個字符串對象: 常量池: "c" 堆中: new "abc" */ String s1 = "c"; String s2 = "a"+"b"+s1; ```
4 intern()方法的演變
4.1 intern()方法調用區別
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內容相同的"ab"對象,此時常量池中沒有"ab"對象。 如果常量池中沒有該字符串對象: jdk6及之前,intern()方法會創建新的字符串對象,放入常量池并返回新的地址。 jdk7及之后,intern()方法會將調用者對象的地址放入常量池,并返回調用者對象地址。 */ 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()方法總結
```java
intern()方法將這個字符串對象嘗試放入常量池中,并返回地址。
jdk1.6中:
如果池中有,則不會放入,返回已有的池中的對象的地址。
如果池中沒有,則把此對象重新創建一份,放入池中,并返回池中新的對象地址。
jdk1.7起:
如果池中有,則不會放入,返回已有的池中的對象的地址。
如果池中沒有,則把此對象的引用地址復制一份,放入池中,并返回池中的引用地址。
```