在Java虛擬機器中,字串常量到底存放在哪

廣州蘆葦科技Java開發團隊發表於2019-01-15

前言

前陣子和朋友討論一個問題: 字串常量歸常量池管理,那比如 String str = "abc"; "abc"這個物件是放在記憶體中的哪個位置,是字串常量池中還是堆?

”這句程式碼的abc當然在常量池中,只有new String("abc")這個物件才在堆中建立“,他們大概是這麼回答。

“abc”這個東西,是放在常量池中,這個答案是錯誤的。

字串“abc"的本體、例項,應該是存在於Java堆中。

可能還真的有部分同學對這個知識點不熟悉,今天和大家聊聊字串這個問題 ~

初學Java時,學到字串這一部分,有一段程式碼

String str1 = "hello";
String str2 = new String("hello"); 
複製程式碼

書上的解釋是:執行第一行的時候,已經把"hello"字串放到了常量池中,執行第二行程式碼時,會將常量池中已經存在的"hello"複製一份到堆記憶體中,建立一個的新的String物件。雖然值一樣,但他們是不同的物件。

當時看完這個解釋,我產生了很多疑惑。因為在此之前已經知道字串的底層是char陣列實現的。我很疑惑:

  • 他copy一份過去,是copy了char陣列呢?
  • 還是copy整個String物件?
  • "hello" 這個物件例項真的存放在常量池中嗎?

當時在網上搜了一些文章和答案,各有說辭,大部分回答都是 "str" 這個物件在常量池中,但也有認為字串常量例項(或叫物件)是在堆中建立,只是將其引用放到字串常量池中,交給常量池管理。

JAVA記憶體區域 — 執行時資料區

理清這個問題前,需要梳理一下前置知識。

從一個經典的示意圖講起,以hotspot虛擬機器為例,此記憶體模型需建立在JDK1.7之前的版本來討論,JDK1.7之後有所改變,但是原理還是一樣的。

在Java虛擬機器中,字串常量到底存放在哪

Java虛擬機器管理的記憶體是執行時資料區那一部分,簡單概括一下其中各個區域的區別:

  • 虛擬機器棧:執行緒私有,生命週期與執行緒相同,即每條執行緒都一個獨立的棧(VM Stack)。每個方法執行時都會建立一個棧幀,也就是說,當有一條執行緒執行了多個方法時,就會有一個棧,棧中有多個棧幀。

  • 本地方法棧:執行緒私有

  • 程式計數器:執行緒私有

  • 堆Heap:執行緒共享,是Java虛擬機器所管理的記憶體中最大的一塊,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。在Java虛擬機器規範中的描述是:所有的物件例項以及陣列都要在堆上分配。 ** (原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated) 但有特殊情況,隨著JIT編譯器的發展,逃逸分析和標量替換技術的逐漸成熟,物件也可以在棧上**分配。另外,雖說堆是執行緒共享,但其中也可以劃分出多個執行緒私有的分配緩衝區*(Thread Local Allocation Buffer,TLAB)*。

  • 方法區:執行緒共享,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

JAVA的三種常量池

此外,Java有三種常量池,即字串常量池(又叫全域性字串池)、class檔案常量池、執行時常量池

(圖一)

1. 字串常量池(也叫全域性字串池、string pool、string literal pool)

字串常量池在每個VM中只有一份,他在記憶體中的位置如圖,紅色箭頭所指向的區域 Interned Strings

2. 執行時常量池(runtime constant pool)

當程式執行到某個類時,class檔案中的資訊就會被解析到記憶體的方法區裡的執行時常量池中。看圖可清晰感知到每一個類被載入進來都會產生一個執行時常量池,由此可知,每個類都有一個執行時常量池。它在記憶體中的位置如圖,藍色箭頭所指向的區域,方法區中的Class Date中的執行時常量池(Run-Time Constant Pool)

(圖二)

3. class檔案常量池(class constant pool)

class常量池是在編譯後每個class檔案都有的,class檔案中除了包含類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是 常量池*(constant pool table)*,用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。*字面量就是我們所說的常量概念,如文字字串、被宣告為final的常量值等。*他在class檔案中的位置如上圖所示,Constant Pool 中。

個人理解

public static void main(String[] args) {
	String str = "hello";
}
複製程式碼

回到一開始說到的這句程式碼,可以來總結一下它的執行過程了。

  1. 首先,字面量 "hello" 在編譯期,就會被記錄在 class檔案的 class常量池中。
  2. 而當 class檔案被載入到記憶體中後,JVM就會將 class常量池中的大部分內容存放到執行時常量池中,但是字串 "hello" 的本體(物件)和其他所有物件一樣,是會在堆中建立再將引用放到字串常量池,也就是圖一的 Interned Strings的位置。(RednaxelaFX 的文章裡,測試結果是在新生代的Eden區。但因為一直有一個引用駐留在字串常量池,所以不會被GC清理掉)
  3. 而到了String str = "hello" 這步,JVM會去字串常量池中找,如果找到了,JVM會在棧中的區域性變數表裡建立str變數,然後把字串常量池中的(hello 物件的)引用複製給 str 變數。

在《深入理解Java虛擬機器》這本書中也有字串相關的解釋,舉其中幾個例子:

例子1

(原文)執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

最後一句描述不太準確,編譯期生成的各種字面量並不是全部進入方法區的執行時常量池中。字串字面量就不進入執行時常量池,而是在堆中建立了物件再將引用駐留到字串常量池中。

例子2

//程式碼清單2-7 String.intern()返回引用的測試

public class RuntimeConstantPoolOOM{

	public static void main(String[]args){
		String str1=new StringBuilder("計算機").append("軟體").toString();
		System.out.println(str1.intern()==str1);
		String str2=new StringBuilder("ja").append("va").toString();
		System.out.println(str2.intern()==str2);
	}
}
複製程式碼

(原文)這段程式碼在JDK 1.6中執行,會得到兩個false,而在JDK 1.7中執行,會得到一個true和一個false。產生差異的原因是:在JDK 1.6中,intern()方法會把首次遇到的字串例項複製到永久代中,返回的也是永久代中這個字串例項的引用,而由StringBuilder建立的字串例項在Java堆上,所以必然不是同一個引用,將返回false。而JDK 1.7(以及部分其他虛擬機器,例如JRockit)的intern()實現不會再複製例項,只是在常量池中記錄首次出現的例項引用,因此intern()返回的引用和由StringBuilder建立的那個字串例項是同一個。對str2比較返回false是因為 “java” 這個字串在執行StringBuilder.toString()之前已經出現過,字串常量池中已經有它的引用了,不符合“首次出現”的原則,而“計算機軟體”這個字串則是首次出現的,因此返回true。

原文解釋也不太準確,我覺得在 JDK 1.6中,intern()並不會把首次遇到的字串例項複製到永久代中,而是會將例項再複製一份到堆(heap)中,然後將其引用放入字串常量池中進行管理,所以此程式碼返回false。而JDK1.7中的intern()不會再複製例項,直接將首次遇到的此字串例項的引用,放入字串常量池,於是返回true。關於此觀點,還沒看到大神文章實錘,歡迎討論。

最後再延伸一點,大家都知道,字串的value是final修飾的char陣列,那麼以下這段程式碼:

// private final char value[];
String str1 = "hello world";
String str2 = new String("hello world");
String str3 = new String("hello world");
複製程式碼

str1、str2、str3 三個變數所指向的都是不同的物件。(str1 != str2 != str3)

那麼,這三個物件裡的char陣列是否是同一個陣列?相信大家都有答案了。


此文所討論的Java記憶體模型是建立在JDK1.7之前。JDK 7開始 Hotspot 虛擬機器把字串常量池(Interned String位置)從永久代(PermGen)挪到Heap堆,JDK 8又徹底取消 PermGen,把諸如class之類的後設資料都挪到GC堆之外管理。但不管怎樣,基本原理還是不變的,字面量 ”hello“ 等依舊不是放在 Interned String 中。


推薦文章:


隆鵬

廣州蘆葦科技Java開發團隊

蘆葦科技-廣州專業網際網路軟體服務公司

抓住每一處細節 ,創造每一個美好

關注我們的公眾號,瞭解更多

想和我們一起奮鬥嗎?lagou搜尋“ 蘆葦科技 ”或者投放簡歷到 server@talkmoney.cn 加入我們吧

在Java虛擬機器中,字串常量到底存放在哪

關注我們,你的評論和點贊對我們最大的支援

相關文章