測試用的Java版本: 11html
內存模型
當咱們學習字符串內存模型的時候常常看到相似這樣的圖java
這類圖簡明扼要得解釋了字符串變量、堆、常量池的引用關係,然而我以爲表述的並不許確,深刻思考時甚至經常被它誤導。git
衆所周知字符串的數據是以字節數組的形式存儲的,Java中除了八大基本數據類型外,都是以對象的形式存在,字節數組也不例外。只要是對象,必然擁有本身的獨立存儲空間,不會屈居在String對象的內存空間中:算法
經過查看String的構造方法便可證明s2的value和字節數組的關係。數組
這裏有更多相關測試,方便感興趣的同窗查看:學習
https://gitee.com/ellipse/java_practices/tree/master/src/main/java/org/misty/practices/string測試
這張圖只描述了常量池中已存在的狀況。常量池中不存在的狀況,須要藉助動態生成字符串(拼接)來模擬。爲了更好地理解,將先簡單介紹一下字符串拼接。優化
字符串拼接 +
字面量鏈接
var helloworld = "Hello" + "World"; /* 0: ldc #10 // String HelloWorld 2: astore_0 */
Javac會將"字面量鏈接"優化成完整字符串常量的調用。ui
字符串變量鏈接
// Java11 var hello = "Hello"; var helloworld = hello + "World"; /* 0: ldc #2 // String Hello 2: astore_0 3: aload_0 4: invokedynamic #3, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; 9: astore_1 */
古時候,Javac會把字符串鏈接語句轉譯成StringBuilder調用,而在新版中變成了動態方法調用。那麼這個#0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
是什麼呢?編碼
通過一番摸索,我在字節碼的底部看到了它的聲明,原來它是經過StringConcatFactory.makeConcatWithConstants
方法建立的一個lambda表達式。它的原理是:
lambda表達式中包含一個pattern,這個pattern是經過解析字符串鏈接語句得來的:
hello + "World" => "\u0001World" "Hello" + world => "Hello\u0001" hello + world => "\u0001\u0001"
其中的\u0001
是佔位符。lambda表達式執行時,用傳入的字符串變量(們)按順序替換\u0001
佔位符,而後將結果返回,併入棧。
咱們也能夠經過編碼實現這一過程:
public static void main(String[] args) throws Throwable { var lookup = MethodHandles.lookup(); var methodType = MethodType.methodType(String.class, String.class); var callSite = StringConcatFactory.makeConcatWithConstants(lookup, "makeConcatWithConstants", methodType, /*這裏是pattern*/ "\u0001World"); var methodHandler = callSite.dynamicInvoker(); var res = methodHandler.invoke("Hello"); System.out.println(res); // HelloWorld }
intern
瞭解了字符串拼接後,咱們再回過頭看看當常量池中不存在時,內存模型是什麼樣的。
爲了使圖片內容更清爽,圖中省略了"a"
的內存引用。因爲這裏的字節數組是由lambda表達式動態生成的,所以它儲存在堆中。
接下來咱們調用intern方法進行池化。新版本池化的邏輯是:若是常量池中不存在該字符串,則直接在常量池中建立一個對當前字符串(堆中)的引用。
建立了幾個對象?
這是一道經典的問題:執行下面這條語句一共建立了幾個對象?
String str = new String("HelloWorld");
標準答案是:1個或者2個。
- 若是常量池存在
"HelloWorld"
則在堆中建立一個String,並指向常量池中的值(value)。 - 若是常量池不存在
"HelloWorld"
則在常量池中建立一個值爲HelloWorld的String,而後在堆中建立第二個String,並指向常量池中的。
然而你就歷來沒有懷疑過這個答案嗎?
學習過類加載機制的同窗們都知道,一句代碼的執行要經歷不少步驟:
編碼 > 編譯 > 裝載(加載字節碼 > 連接 > 初始化)> 運行
其中連接又可細分爲校驗>準備>解析
。解析
過程的描述是將常量池內的符號引用轉換爲直接引用
。就是將字節碼中Constant pool
區域的數據儲存到內存中相應的位置。字符串常量的建立也是在這個階段進行。但因爲存在延遲解析
,即符號引用第一次被使用時進行解析,所以字符串常量的建立時機會被推遲到第一次使用的時候。測試代碼:
咱們回到主題,看看這行語句到底作了什麼:
public static void main(String[] args) { var str = new String("HelloWorld"); }
要想知道Java程序執行的結果,不能看源碼怎麼寫,而是看虛擬機如何執行(字節碼):
stack=3, locals=2, args_size=1 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String HelloWorld 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: return
先簡單解釋一下這段字節碼:
操做數棧大小爲3,本地變量表大小爲2(其中索引位置0被方法入參佔用)
操做數棧 [ | | ] 本地變量表 [ args | ]
0 建立一個String對象,將引用入棧。這個對象未初始化
操做數棧 [ objRef | | ] 本地變量表 [ args | ]
3 dup複製棧頂元素併入棧
操做數棧 [ objRef | objRef | ] 本地變量表 [ args | ]
4 ldc 加載常量#3
併入棧。#3
即符號連接,會轉換成"HelloWorld"
的直接引用
操做數棧 [ consRef | objRef | objRef ] 本地變量表 [ args | ]
6 彈出棧頂兩個元素,即"HelloWorld"
字符串和String對象,執行構造器方法
操做數棧 [ objRef | | ] 本地變量表 [ args | ]
9 彈出棧頂元素,保存到本地變量表索引1位置
操做數棧 [ | | ] 本地變量表 [ args | objRef ]
經過分析字節碼,咱們會發現答案2的表述並不許確。
首先,在堆中建立一個未初始化的String對象,接着在常量池中查找"HelloWorld"
,若是不存在,因爲這個符號連接是第一次使用,所以在常量池中建立"HelloWorld"
對象(延遲解析)。最後用"HelloWorld"
對象做參數初始化第一個String對象。
尾聲
這篇文章,從早上9點,一直寫到下午4點。其間花費了大量時間查找資料,設計編寫測試代碼,繪製模型圖。文中觀點大多通過代碼測試,但依然存在沒法測試,或須要閱讀底層代碼才能解釋的部分,由於精力有限,暫時再也不深刻。若有錯誤歡迎批評指正。
測試代碼:https://gitee.com/ellipse/java_practices/tree/master/src/main/java/org/misty/practices/string
參考資料
http://www.javashuo.com/article/p-nxufqfky-nv.html
https://www.jianshu.com/p/039d6df30fea
https://www.cnblogs.com/tiancai/p/9399530.html
以及其餘介紹java字節碼的文章,沒法一一列舉
附:用"abc"
撐爆堆內存
前面咱們瞭解了字符串鏈接會在堆裏建立String對象和字節數組對象,不管拼接後的字符串是否存在常量池中。因而咱們能夠利用很小的,重複的字符串撐爆堆內存。無需編寫複雜的算法來生成隨機字符串。
public class StringOOM { static String ABC = "abc"; /** * VM Options -Xms100M -Xmx100M */ public static void main(String[] args) throws InterruptedException { var list = new ArrayList<String>(10000); var a = "A"; var b = "B"; var c = "C"; try { while (true) { list.add((a + b + c)); // 1946160 // list.add((a + b + c).intern()); // 14778652 } } catch (Throwable e) { System.out.println(list.size()); } } }