運(yùn)行時(shí)數(shù)據(jù)區(qū)域
JVM 在執(zhí)行 Java 程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域有各自的用途疆偿,以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而一直存在猾瘸,有些區(qū)域則是依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀勉盅。
JVM 所管理的內(nèi)存會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
Java 堆
Java 堆(Java Heap) 是虛擬機(jī)所管理的內(nèi)存中最大的一塊铆农。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域牺氨,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此區(qū)域的唯一目的就是存放對(duì)象實(shí)例墩剖,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存波闹。
為什么是幾乎呢?從實(shí)現(xiàn)角度來看涛碑,隨著 Java 語言的發(fā)展精堕,現(xiàn)在已經(jīng)能看到一些跡象表明日后可能出現(xiàn)值類型的支持,即使只考慮現(xiàn)在蒲障,由于JIT編譯器的發(fā)展和”逃逸分析”技術(shù)的逐漸成熟歹篓,棧上分配、標(biāo)量替換等優(yōu)化技術(shù)使得對(duì)象一定分配在堆上這件事情已經(jīng)變得不那么絕對(duì)了揉阎。
方法區(qū)
方法區(qū)(Method Area)與 Java 堆一樣庄撮,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息毙籽、常量洞斯、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)坑赡。
程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間烙如,它可以看作是當(dāng)前線程索所執(zhí)行的字節(jié)碼的行號(hào)指示器。
Java 虛擬機(jī)棧
Java 虛擬機(jī)棧(JVM Stacks)毅否,與程序計(jì)數(shù)器一樣也是線程私有的亚铁,它的生命周期與線程相同。虛擬機(jī)棧描述的是 Java 方法執(zhí)行的線程內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候螟加,Java 虛擬機(jī)都會(huì)同步創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表徘溢、操作數(shù)棧、方法出口燈信息捆探。每一個(gè)方法被調(diào)用直至執(zhí)行完畢的過程然爆,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
本地方法棧
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的黍图,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù)曾雕,而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。
內(nèi)存中的棧(stacks)雌隅、堆(heap)和方法區(qū)(method area)的用法
通常我們定義一個(gè)基本數(shù)據(jù)類型的變量翻默,一個(gè)對(duì)象的引用,還有就是函數(shù)調(diào)用的現(xiàn)場(chǎng)保存都使用JVM中的椙∑穑空間修械。
數(shù)據(jù)類型包括 boolean、byte检盼、char肯污、short、int吨枉、float蹦渣、double。
引用對(duì)象是 reference 類型貌亭,它并不等同于對(duì)象本身柬唯,可能是一個(gè)指向?qū)ο笃鹗嫉刂返闹羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置
現(xiàn)場(chǎng)保護(hù)和現(xiàn)場(chǎng)恢復(fù)源于匯編中主程序和子程序之間的調(diào)用和返回圃庭,和CPU中斷機(jī)制有關(guān)锄奢。
主程序和子程序通常是分別編制的,所以它們所使用的寄存器往往會(huì)發(fā)生沖突剧腻。如果主程序在調(diào)用子程序之前的某個(gè)寄存器內(nèi)容在從子程序返回后還有用拘央,而子程序又恰好使用了同一個(gè)寄存器,這就破壞了該寄存器的原有內(nèi)容书在,因而造成程序運(yùn)行錯(cuò)誤灰伟,這是不允許的。為避免這種錯(cuò)誤的發(fā)生儒旬,在一進(jìn)入子程序后栏账,就應(yīng)該把子程序所需要使用的寄存器內(nèi)容保存在堆棧中,此過程稱作現(xiàn)場(chǎng)保護(hù)栈源。在退出子程序前把寄存器內(nèi)容恢復(fù)原狀发笔,此過程稱作現(xiàn)場(chǎng)恢復(fù)。現(xiàn)場(chǎng)保護(hù)與現(xiàn)場(chǎng)恢復(fù)分別使用壓棧和彈出指令實(shí)現(xiàn)凉翻。
而通過new關(guān)鍵字和構(gòu)造器創(chuàng)建的對(duì)象則放在堆空間了讨,堆是垃圾收集器管理的主要區(qū)域。
由于現(xiàn)在的垃圾收集器都采用分代收集算法制轰,所以堆空間還可以細(xì)分為新生代和老年代前计,再具體一點(diǎn)可以分為 Eden、Survivor(又可分為From Survivor和To Survivor)垃杖、Tenured男杈。
方法區(qū)和堆都是各個(gè)線程共享的內(nèi)存區(qū)域,用于存儲(chǔ)已經(jīng)被JVM加載的類信息调俘、常量伶棒、靜態(tài)變量旺垒、JIT編譯器編譯后的代碼緩存等數(shù)據(jù)。
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分肤无,Class 文件中除了有類的版本先蒋、字段、方法宛渐、接口等描述信息外竞漾,還有常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號(hào)引用窥翩,這些內(nèi)容會(huì)在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中业岁。
棧空間操作起來最快但是棧很小寇蚊,通常大量的對(duì)象都是放在堆空間笔时,棧和堆的大小都可以通過 JVM 的啟動(dòng)參數(shù)來進(jìn)行調(diào)整,椪贪叮空間用光了會(huì)引發(fā) StackOverflowError 異常糊闽,而堆和常量池空間不足則會(huì)引發(fā) OutOfMemoryError 異常。
舉個(gè)例子:
String str = new String("hello");
上面的語句中變量 str 放在棧上爹梁,用 new 創(chuàng)建出來的字符串對(duì)象放在堆上右犹,而“hello”這個(gè)字面量是放在方法區(qū)的。
補(bǔ)充
運(yùn)行時(shí)常量池相當(dāng)于 Class 文件常量池具有動(dòng)態(tài)性姚垃,Java 語言并不要求常量一定只有編譯期間才能產(chǎn)生念链,運(yùn)行期間也可以將新的常量放入池中,String 類的 intern() 方法就是這樣的积糯。
查看以下代碼執(zhí)行結(jié)果在java7之前和之后的版本中運(yùn)行結(jié)果是否一致:
String s1 = new StringBuilder("go")
.append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja")
.append("va").toString();
System.out.println(s2.intern() == s2);
運(yùn)行結(jié)果:
java7 之前:false,false.
java7 之后:true false
原因:對(duì)于"s1.intern() == s1"掂墓,java7之前,由于運(yùn)行時(shí)常量池存放在方法區(qū)內(nèi)看成,變量s1通過new關(guān)鍵字創(chuàng)建的對(duì)象君编,所以s1是堆空間的引用;而s1.intern()返回的是方法區(qū)中的運(yùn)行時(shí)常量池的引用川慌,因此結(jié)果是false吃嘿;java7之后運(yùn)行時(shí)常量池存放在堆中,因此"s1=new StringBuilder("go").append("od").toString()"會(huì)將"good"直接放到運(yùn)行時(shí)常量池梦重,同時(shí)返回該引用給到s1兑燥;而調(diào)用"s1.intern()"返回的同樣是運(yùn)行時(shí)常量池中該對(duì)象的引用,所以是true琴拧。
對(duì)于"s2.intern() == s2"降瞳,是因?yàn)樵诰幾g后java常量池中已經(jīng)"java"對(duì)象,所以"s2.intern()"返回的始終都是方法區(qū)常量池的對(duì)象引用蚓胸,所以結(jié)果都是false挣饥。