前陣子和朋友討論一個問題:java
字符串常量歸常量池管理,那好比 String str = "abc"; "abc"這個對象是放在內存中的哪一個位置,是字符串常量池中仍是堆?
」這句代碼的abc固然在常量池中,只有new String("abc")這個對象纔在堆中建立「,他們大概是這麼回答。面試
「abc」這個東西,是放在常量池中,這個答案是錯誤的。數組
字符串「abc"的本體、實例,應該是存在於Java堆中。app
可能還真的有部分同窗對這個知識點不熟悉,今天和你們聊聊字符串這個問題 ~測試
初學Java時,學到字符串這一部分,有一段代碼ui
String str1 = "hello"; String str2 = new String("hello");
書上的解釋是:執行第一行的時候,已經把"hello"字符串放到了常量池中,執行第二行代碼時,會將常量池中已經存在的"hello"複製一份到堆內存中,建立一個的新的String對象。雖然值同樣,但他們是不一樣的對象。
當時看完這個解釋,我產生了不少疑惑。由於在此以前已經知道字符串的底層是char數組實現的。我很疑惑:spa
當時在網上搜了一些文章和答案,各有說辭,大部分回答都是 "str" 這個對象在常量池中,但也有認爲字符串常量實例(或叫對象)是在堆中建立,只是將其引用放到字符串常量池中,交給常量池管理。線程
理清這個問題前,須要梳理一下前置知識。code
從一個經典的示意圖講起,以hotspot虛擬機爲例,此內存模型需創建在JDK1.7以前的版原本討論,JDK1.7以後有所改變,可是原理仍是同樣的。server
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
(原文)運行時常量池(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");
str一、str二、str3 三個變量所指向的都是不一樣的對象。(str1 != str2 != str3)
那麼,這三個對象裏的char數組是不是同一個數組?相信你們都有答案了。
此文所討論的Java內存模型是創建在JDK1.7以前。JDK 7開始 Hotspot 虛擬機把字符串常量池(Interned String位置)從永久代(PermGen)挪到Heap堆,JDK 8又完全取消 PermGen,把諸如klass之類的元數據都挪到GC堆以外管理。但無論怎樣,基本原理仍是不變的,字面量 」hello「 等依舊不是放在 Interned String 中。
推薦文章:
做者:RednaxelaFX,曾爲《深刻理解Java虛擬機》提推薦語
隆鵬
廣州蘆葦科技Java開發團隊
蘆葦科技-廣州專業互聯網軟件服務公司
抓住每一處細節 ,創造每個美好
關注咱們的公衆號,瞭解更多
想和咱們一塊兒奮鬥嗎?lagou搜索「 蘆葦科技 」或者投放簡歷到 server@talkmoney.cn 加入咱們吧
關注咱們,你的評論和點贊對咱們最大的支持