java內存優化:使用String.intern()節省java堆內存

一,什麼是字符串常量池?

A pool of strings, initially empty, is maintained privately by the class String.一個由String類私有維持的初始化爲空的字符串池

 

JDK1.6前,字符串常量池像運行時常量池一樣,屬於方法區(永久代,Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。對於習慣在HotSpot虛擬機上開發、部署程序的開發者來說,很多人都更願意把方法區稱爲“永久代”(Permanent Generation));

 

JDK1.7開始,字符串常量池被從永久代(方法區)中移除,HotSpot VM裏,成爲記錄interned string的一個全局表叫做StringTable,它本質上就是個HashSet<String>。是個純運行時的結構,而且是惰性(lazy)維護的 ,滿足java動態解析。注意它只存儲對java.lang.String實例的引用,而不存儲String對象的內容。


二,String的intern()方法有什麼作用?

假設,有字符串:a

   JDK1.6——a.intern(),由於字符串常量池位於“永久代”,會把首次遇到的字符串(equals())的實例clone到永久代,並返回該引用,否則直接返回已存在實例的引用。注意:返回的引用都是指向永久代中實例的引用

 

   JDK1.7——a.intern(),字符串常量池從方法區移除,被全局表StringTable(本質HashSet<String>)代替,會把首次遇到的字符串(equals())的引用添加到StringTable,並返回此引用,否則直接返回StringTable中已持有的引用。注意:返回的引用都指向heap堆

 

三,使用 “”(double quotation marks)與 new String("")初始化字符串變量的區別?

    1, 無論是 double quotation marks(" ") 還是 new String("") 的字符串字面量,在編譯期都被存儲到Class文件常量池,由CONSTANT_String常項(持有index,由index指向的CONSTANT_Utf8常項(持有實際文本內容。在類加載過程加載階段通過驗證階段文件格式驗證動作,將二進制字節流讀入方法區存儲結構—— 即,將Class文件常量池存儲爲方法區運行時常量池

 

2,對象的創建需要諸如 ldc、new 等字節碼指令的執行。所以,存儲在運行時常量池中的字面量,一般不會在類加載時heap堆創建實例並添加引用到StringTable。注意:ldc指令用於將int、float等基本類型及String型字面量從運行時常量池中推送至棧頂

 

3,ldc指令執行時遇到String類型字面量,首先會查看字符串常量池StringTable中有無此字符串對象的引用(equals()),如果有,則返回此引用;否則在heap堆創建新的字符串對象,將引用駐留在StringTable,再返回此引用。此過程類似於,String.intern()的執行過程。所以有:

String s1=new String("he")+new String("llo"); // 執行StringBuilder().append().toString(){ new String() }在堆中創建新的對象,引用賦值s1

s1=s1.intern();   // 仍然返回s1原本的引用,因爲StringTable在此之前還沒有駐留 “hello”的引用
    String s2="hello";  // "hello"在類加載過程中並未發生解析,Runtime程序執行開始時,StringTable中並沒有該實例的引用

 

String s1="hello"; 近似於String s1=new String("hello"); s1=s1.intern();

 

③ String s1=new String("hello");s1=s1.intern();首先執行ldc指令“hello”,創建實例駐留引用在字符串常量池StringTable,再執行new指令創建對象在heap堆中,所以s1=s1.intern();返回的引用不是s1的引用,而是“hello”駐留在StringTable中的引用;

 

4,類加載過程包含加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個部分統稱爲連接。加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段不一定:它在某些情況下可以在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。

 

5,特殊的:

① Static類變量,在類加載過程中的準備階段被加載到方法區,賦初值(零值),初始化階段執行<clinit>()方法,被初始化;

 

②如果Static類變量沒有被final修飾①,或者並非基本類型及字符串,則將會選擇在<clinit>方法中進行初始化;

 

③ final 只有在修飾Static類變量時, 編譯器Javac纔會爲之生成ConstantValue屬性,在準備階段加載到方法區的時候,虛擬機就會根據ConstantValue設置的值,直接初始化;

 

④ 實例變量將會在實例構造器 <init>() 方法中被初始化!……實例構造器<init>()方法和類構造器<clinit>()方法,都是在編譯期字節碼生成階段添加到語法樹中的(注意,這裏的實例構造器不是指默認構造函數,如果用戶代碼中沒有提供任何構造函數,那編譯器將會添加一個沒有參數的、訪問性(public、protected或private)與當前類一致的默認構造函數,這個工作在填充符號表階段就已經完成);

 

⑤ 在類編譯的字節碼生成階段,還做了一些代碼替換工作用於優化程序的實現邏輯!……所以有

(1)String s1=new String("he")+new String("llo"); // 執行StringBuilder().append().toString(){ new String() }在堆中創建新的對象,引用賦值s1;

 

(2)String s1="he"+"llo"; // 編譯階段,被優化爲“hello”,同時與“he”和“llo”以字面量形式存儲在常量池,執行階段等效於

String s1="hello";

 

(3)String s1=new String("he")+"llo"; // 執行StringBuilder().append().toString(){ new String() }在堆中創建新的對象,引用賦值s1;

 

(4)String s=new String("he");String s1=s+"llo"; // 執行StringBuilder().append().toString(){ new String() }在堆中創建新的對象,引用賦值s1;

 

(5)String s="he";String s1=s+"llo"; // 執行StringBuilder().append().toString(){ new String() }在堆中創建新的對象,引用賦值s1;

 

最後:我們可以通過 String.intern(),重用字符串常量池StringTable的引用,釋放String在heap堆中新創建的實例的引用,使該實例進入到GC回收期,回收引用指向的堆中內存,以此優化堆內存

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章