摘要:的三種常量池此外,有三種常量池,即字符串常量池又叫全局字符串池文件常量池運行時常量池。開始虛擬機把字符串常量池位置從永久代挪到堆,又徹底取消,把諸如之類的元數據都挪到堆之外管理。
前言
前陣子和朋友討論一個問題:
字符串常量歸常量池管理,那比如 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虛擬機管理的內存是運行時數據區那一部分,簡單概括一下其中各個區域的區別:
虛擬機棧:線程私有,生命周期與線程相同,即每條線程都一個獨立的棧(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"; }
回到一開始說到的這句代碼,可以來總結一下它的執行過程了。
首先,字面量 "hello" 在編譯期,就會被記錄在 class文件的 class常量池中。
而當 class文件被加載到內存中后,JVM就會將 class常量池中的大部分內容存放到運行時常量池中,但是字符串 "hello" 的本體(對象)和其他所有對象一樣,是會在堆中創建,再將引用放到字符串常量池,也就是圖一的 Interned Strings的位置。(RednaxelaFX 的文章里,測試結果是在新生代的Eden區。但因為一直有一個引用駐留在字符串常量池,所以不會被GC清理掉)
而到了String str = "hello" 這步,JVM會去字符串常量池中找,如果找到了,JVM會在棧中的局部變量表里創建str變量,然后把字符串常量池中的(hello 對象的)引用復制給 str 變量。
例子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,把諸如klass之類的元數據都挪到GC堆之外管理。但不管怎樣,基本原理還是不變的,字面量 ”hello“ 等依舊不是放在 Interned String 中。
推薦文章:
請別再拿 “String s = new String("xyz"); 創建了多少個String實例” 來面試了吧
借HSDB來探索HotSpot VM的運行時數據
作者:RednaxelaFX,曾為《深入理解Java虛擬機》提推薦語
java用這樣的方式生成字符串:String str = "Hello",到底有沒有在堆中創建對象? - 胖君的回答 - 知乎
隆鵬
廣州蘆葦科技Java開發團隊
蘆葦科技-廣州專業互聯網軟件服務公司
抓住每一處細節 ,創造每一個美好
關注我們的公眾號,了解更多
想和我們一起奮斗嗎?lagou搜索“ 蘆葦科技 ”或者投放簡歷到 server@talkmoney.cn 加入我們吧
關注我們,你的評論和點贊對我們最大的支持
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73095.html
摘要:編譯參見深入理解虛擬機節走進之一自己編譯源碼內存模型運行時數據區域根據虛擬機規范的規定,的內存包括以下幾個運運行時數據區域程序計數器程序計數器是一塊較小的內存空間,他可以看作是當前線程所執行的字節碼的行號指示器。 點擊進入我的博客 1.1 基礎知識 1.1.1 一些基本概念 JDK(Java Development Kit):Java語言、Java虛擬機、Java API類庫JRE(...
摘要:在之后,原來永久代的數據被分到了堆和元空間中。元空間存儲類的元信息,靜態變量和常量池等放入堆中。這樣能在一些場景中顯著提高性能,因為避免了在堆內存和堆外內存來回拷貝數據。 以下內容部分轉載于: CS-Notes showImg(http://ww1.sinaimg.cn/large/005NT19Ply1g385uooqv9j30kd0slmyw.jpg); 程序計數器(Program...
摘要:三對象的內存布局對象在堆中的布局分為三個區域對象頭,實例數據,對齊填充。總結了解內存區域是對的深入學習,以前只知道有堆和棧的區分,現在我們了解到了具體的堆棧的作用。 引言 學習Java也有一段時間了,總感覺有些東西學的不是很精通。例如Java內存區域到底是怎么樣的?程序是怎么跑的?對象是怎么存放的?這些都影響了我對自己的程序運行的熟悉程度。 一. 運行時數據區域 showImg(/im...
摘要:運行時數據區域虛擬機在執行的過程中會把管理的內存劃分為若干個不同的數據區域。方法區的內存收集還是會出現,不過這個區域的內存收集主要是針對常量池的回收和對類型的卸載。當方法區無法滿足內存分配需求時將拋出異常。 運行時數據區域Java虛擬機在執行Java的過程中會把管理的內存劃分為若干個不同的數據區域。這些區域有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機進程的啟動而存在,而有的區...
摘要:堆區堆是虛擬機所管理的內存中最大的一塊,它是被所有線程共享的一塊內存區域,該區域在虛擬機啟動的時候創建。 運行時數據區域 ? ?想要了解jvm,那對其內存分配管理的學習是必不可少的;java虛擬機在執行java程序的時候會把它所管理的內存劃分成若干數據區域。這些區域有著不同的功能、用途、創建/銷毀時間。java虛擬機所分配管理的內存區域如圖1所示 程序計數器 ? ?程序計數器是一塊比較...
閱讀 2083·2023-04-26 02:41
閱讀 2146·2021-09-24 09:47
閱讀 1546·2019-08-30 15:53
閱讀 1205·2019-08-30 13:01
閱讀 1885·2019-08-29 11:27
閱讀 2857·2019-08-28 17:55
閱讀 1740·2019-08-26 14:00
閱讀 3377·2019-08-26 10:18