第二章 Java記憶體區域與記憶體溢位異常(1)

weixin_33782386發表於2017-06-01

JAVA中常遇到的幾種常量池的區別

1. Class檔案常量池

Class檔案中除了有類的版本資訊,欄位,方法,介面等描述資訊外,還有一部分叫Class檔案常量池,這個常量池可以理解為Class檔案中的資源倉庫,它當中主要存放兩大類常量:字面量和符號引用

字面量:如文字字串,"aaa"; 宣告為final型別的常量值等等
符號引用:又分為三類常量
1)類和介面的全限定名
2)欄位的名稱和描述符
3)方法的名稱和描述符

2. 執行時常量池

執行時常量池是方法區的一部分,Class檔案常量池中的內容在編譯時就產生了,而在類載入後,這部分內容會存在執行時常量池中,另外,由符號引用轉變成的直接引用也會存在執行時常量池中。
執行時常量池相對於Class檔案常量池的一個重要特徵是具備動態性,也就是常量並不一定在編譯時產生,執行時也可能將新的常量放入常量池中。

3. 字串池

這是一個比較難懂的概念,在工作中,String類是我們使用頻率非常高的一種物件型別。JVM為了提升效能和減少記憶體開銷,避免字串的重複建立,其維護了一塊特殊的記憶體空間,即字串池(String Pool)。這部分記憶體之前是在方法區,jdk1.8之後已經移除了方法區,轉而替代為Metaspace區,那麼這個字串池應該是被劃到這個Metaspace中了吧(有疑問,還沒弄明白)。

我們知道,在Java中有兩種建立字串物件的方式:
1)採用字面值的方式賦值
2)採用new關鍵字新建一個字串物件。
這兩種方式在效能和記憶體佔用方面存在著差別

方式一:採用字面值的方式賦值,例如:

String a = "aaa";
String b = "aaa";
System.out.println(a == b)

我們來分析一下過程,JVM首先會去字串池中查詢是否存在"aaa"這個物件,如果不存在,則在字串池中建立"aaa"這個物件,然後將池中"aaa"這個物件的引用地址返回給字串常量a,這樣a會指向池中"aaa"這個字串物件;如果存在,則不建立任何物件,直接將池中"aaa"這個物件的地址返回,賦給字串常量b。所以a==b返回值是true,因為二均指向了字串池中的"aaa".

方式二:採用new關鍵字新建一個字串物件,例如:

String a = new String("aaa");
String b = new String("aaa");
System.out.println(a == b);

採用new關鍵字,JVM會先從常量池中檢視有無"aaa"字串,有的話就拷貝一份到新new出來的堆記憶體中,返回的是堆記憶體的地址;如果沒有的話,直接在堆中new出來一塊空間存放"aaa"的值,同樣返回的是堆記憶體的地址,那麼問題來了

這個時候這個堆記憶體的"aaa"是否會也在字串池中建立一份呢?

這個問題在網上爭議很大,有的認為這個時候也會在字串池中建立一份,這個說法我不太認同,因為這樣的話豈不是造成了堆和字串池的完全重複?也就是不管字串池中有沒有"aaa",只要我是new,那都會在堆和字串池中同時存在"aaa".這樣不就造成了記憶體的浪費嗎?還有一種說法是,如果字串池中沒有"aaa",那先在堆中創造出"aaa",如果需要往字串池中加入"aaa"的話,就呼叫String的intern方法。我個人比較認同這種說法。

關於intern方法

intern方法使用:一個初始為空的字串池,它由類String獨自維護。當呼叫 intern方法時,如果池已經包含一個等於此String物件的字串(用equals(oject)方法確定),則返回池中的字串。否則,將此String物件新增到池中,並返回此String物件的引用。 對於任意兩個字串s和t,當且僅當s.equals(t)為true時,s.instan() == t.instan才為true。所有字面值字串和字串賦值常量表示式都使用 intern方法進行操作。

下面看一些經常出現的例子
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
           
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

首先說明一點,在java 中,直接使用==操作符,比較的是兩個字串的引用地址,並不是比較內容,比較內容請用String.equals()。

s1 == s2這個非常好理解,s1、s2在賦值時,均使用的字串字面量,說白話點,就是直接把字串寫死,在編譯期間,這種字面量會直接放入class檔案的常量池中,從而實現複用,載入執行時常量池後,s1、s2指向的是同一個記憶體地址,所以相等。

s1 == s3這個地方有個坑,s3雖然是動態拼接出來的字串,但是所有參與拼接的部分都是已知的字面量,在編譯期間,這種拼接會被優化,編譯器直接幫你拼好,因此String s3 = "Hel" + "lo";在class檔案中被優化成String s3 = "Hello";,所以s1 == s3成立。

s1 == s4當然不相等,s4雖然也是拼接出來的,但new String("lo")這部分不是已知字面量,是一個不可預料的部分,編譯器不會優化,必須等到執行時才可以確定結果,結合字串不變定理,鬼知道s4被分配到哪去了,所以地址肯定不同。

s1 == s9也不相等,道理差不多,雖然s7、s8在賦值的時候使用的字串字面量,但是拼接成s9的時候,s7、s8作為兩個變數,都是不可預料的,編譯器畢竟是編譯器,不可能當直譯器用,所以不做優化,等到執行時,s7、s8拼接成的新字串,在堆中地址不確定,不可能與方法區常量池中的s1地址相同。

s4 == s5已經不用解釋了,絕對不相等,二者都在堆中,但地址不同。

s1 == s6這兩個相等完全歸功於intern方法,s5在堆中,內容為Hello ,intern方法會嘗試將Hello字串新增到常量池中,並返回其在常量池中的地址,因為常量池中已經有了Hello字串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等。

這只是讀書筆記,大多內容都是來自於其他前輩的帖子和《深入理解Java虛擬機器》這本書,所有來源均列出,供大家閱讀

Java字串池和字串堆的記憶體分配
String放入執行時常量池的時機與String.intern()方法解惑
Java 6,7,8 中的 String.intern – 字串池
Java中的字串常量池與Java中的堆和棧的區別
Java字串池(String Pool)深度解析
觸控java常量池
Java中幾種常量池的區分

相關文章