走进JVM【二】理解JVM内存区域
引言
對于C++程序員,內(nèi)存分配與回收的處理一直是令人頭疼的問題。Java由于自身的自動內(nèi)存管理機制,使得管理內(nèi)存變得非常輕松,不容易出現(xiàn)內(nèi)存泄漏,溢出的問題。
不容易不代表不會出現(xiàn)問題,一旦內(nèi)存泄漏或溢出的情況發(fā)生,調(diào)試起來會變得非常困難。這就要求我們對虛擬機的內(nèi)存區(qū)域有深入的理解。最終能夠判斷內(nèi)存方面的異常發(fā)生時,具體在JVM中的位置。
內(nèi)存區(qū)域
JVM運行時,首先需要類加載器(ClassLoader)?加載所需類的字節(jié)碼,加載完畢交由執(zhí)行引擎執(zhí)行,執(zhí)行過程中需要一段空間來存儲數(shù)據(jù)(類比CPU與主存)。這段內(nèi)存空間的分配和釋放過程正是我們所關(guān)心的,稱為運行時數(shù)據(jù)區(qū)。
對于CS相關(guān)從業(yè)者,深入理解操作系統(tǒng)的內(nèi)存的層次結(jié)構(gòu),分配與垃圾收集過程都是大有裨益的。同理,欲定位內(nèi)存問題的出現(xiàn)區(qū)域,必須剖析運行時數(shù)據(jù)區(qū)。
運行時數(shù)據(jù)區(qū)
如上圖所示,運行時數(shù)據(jù)區(qū)包括:程序計數(shù)器(即PC寄存器),Java 虛擬機棧(VM Stack),Java 堆(Heap),方法區(qū)(Method Area),本地方法棧(Native Method Stack)。下面帶領(lǐng)大家深入理解各個數(shù)據(jù)區(qū)域。
JVM實際上就是一臺虛擬的計算機,目的是為了實現(xiàn)"一次編譯,處處執(zhí)行"。所以,在理解運行時數(shù)據(jù)區(qū)時,完全可以與操作系統(tǒng)系統(tǒng) 內(nèi)存,寄存器類比學(xué)習(xí)。
程序計數(shù)器
每條虛擬機中的線程都有自己的寄存器,稱之為程序計數(shù)器(PC)。為了保證線程之間的獨立性,因而PC內(nèi)的空間是線程私有的。
- 線程私有:只能本線程訪問的區(qū)域,其他線程無權(quán)訪問。
程序計數(shù)器的作用
虛擬機中的多線程通過線程輪轉(zhuǎn)調(diào)度,為每條線程分配時間片來實現(xiàn)并發(fā)執(zhí)行。同一時刻,處理機只能執(zhí)行一條線程。當(dāng)切換到另外一條線程時,若不保存當(dāng)前未執(zhí)行完線程的執(zhí)行位置,下次處理機再執(zhí)行這條線程時,又要重新開始執(zhí)行。這種情況顯然是不能容忍的。
引入程序計數(shù)器的目的,就是為了記錄線程的執(zhí)行情況,便于下次切換后進行線程恢復(fù)。
程序計數(shù)器的機制
如何記錄線程的執(zhí)行情況? 其實也并不復(fù)雜,只需要記錄正在執(zhí)行的虛擬機字節(jié)碼指令的地址。如果運行的是Native(本地)方法,計數(shù)器的值為Undefined。
程序計數(shù)器是唯一沒有OutOfMemoryError異常的區(qū)域。
Java 虛擬機棧
每個Java方法執(zhí)行時,需要分配內(nèi)存空間來存儲局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口等信息。將這部分內(nèi)存稱之為棧幀(Stack Frame)。虛擬機棧用于存儲棧幀,是Java方法執(zhí)行的內(nèi)存模型。
顯然我們需要為每個執(zhí)行的方法分配棧空間,因此Java虛擬機棧也是線程私有的。
虛擬機棧的作用
虛擬機棧記錄Java方法執(zhí)行的過程。每個方法開始執(zhí)行時,為之創(chuàng)建一個棧幀記錄信息;方法執(zhí)行到完成的過程,對應(yīng)棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表
局部變量表是棧幀中的重要部分。存放編譯期定義的基本數(shù)據(jù)類型,?對象引用(相當(dāng)于對象地址),及returnAddress類型(字節(jié)碼指令地址)。
局部變量表空間在編譯期間分配,執(zhí)行方法的過程中不會改變其大小。
異常
本地方法棧
本地方法棧與虛擬機棧類似,區(qū)別是虛擬機棧記錄執(zhí)行的Java方法,本地方法棧則記錄Native方法。
本地方法棧同樣會拋出StackOverflowError與OutOfMemoryError異常。
Java 堆
Java堆用于存儲對象實例,為所有對象分配內(nèi)存空間。
所有對象實例都要在堆上分配空間,因此Java堆是所有線程的共享區(qū)域。對象的生命周期結(jié)束后,Java堆還要負責(zé)內(nèi)存回收,因此Java堆也常被稱之為GC堆(Garbage Collected Heap)。
內(nèi)存模型
從內(nèi)存回收的角度,Java堆可以分為新生代(Young Generation)與老生代(Old Generation)。這種劃分的方式,是為了更好的回收內(nèi)存(老生代內(nèi)存會被優(yōu)先回收)。
如圖,新生代還可以分為Eden空間、From Survivor空間、To Survivor空間。
永久代(Permanent Generation)用于存儲靜態(tài)類型數(shù)據(jù),與垃圾收集器關(guān)系不大。
注意:本圖展示的是JVM堆的內(nèi)存模型,JVM堆內(nèi)存包括Java堆區(qū)域?和?永久代區(qū)域。因此,永久代不屬于Java堆。
異常
Java堆同樣可擴展(-Xmx與-Xms參數(shù))。若堆中內(nèi)存已無法為對象實例分配且無法再擴展,拋出OutOfMemoryError異常。
方法區(qū)
方法區(qū)存儲類信息、常量、靜態(tài)變量等數(shù)據(jù),是線程共享的區(qū)域。為與Java堆區(qū)分,方法區(qū)還有一個別名Non-Heap(非堆)。
方法區(qū)≠永久代
方法區(qū)就是永久代?并非如此。
HotSpot虛擬機選擇用永久代來實現(xiàn)方法區(qū),從而省去了為方法區(qū)編寫內(nèi)存管理代碼的工作。這只是一種實現(xiàn)方式,其他虛擬機(BEA JRockit,IBM J9)都不存在永久代這一概念。
通過永久代來實現(xiàn)方法區(qū)容易造成內(nèi)存溢出,未來也可能會被替代。
在虛擬機規(guī)范中,方法區(qū)的實現(xiàn)沒有明確的規(guī)定,因此不能將方法區(qū)等同于永久代。
異常
當(dāng)方法區(qū)無法滿足內(nèi)存分配的需要時,拋出OutOfMemoryError異常。
運行時常量池
運行時常量池(Runtime Constant Pool)用于存放編譯期生成的各種字面量和符號引用。
運行時常量池具備動態(tài)性,使得運行期間也可將新的常量放入池中。例如String類的intern()?方法。
package intern;public class Main1 {public static void main(String[] args) {String s0= "I'm coding"; String s1=new String("I'm coding"); String s2=new String("I'm coding"); system.out.println( s0==s1 ); System.out.println( s0==s1.intern()); s2=s2.intern(); System.out.println( s0==s2 ); } }輸出結(jié)果
false true true 本例中,s0直接保存在常量池,s1與s2的對象實例存儲在Java堆中。==直接比較對象的hashCode,因此第一行輸出false。s1.intern()方法返回s1在常量池中的引用,沒有則創(chuàng)建。
s1存放的字符串已經(jīng)在常量池中存在,直接返回s0的引用,第二行輸出true。
同理,s2接收了s2.intern()的返回值,字符串值與s0相同,第三行輸出true。
運行時常量池是方法區(qū)的一部分,因此受方法區(qū)內(nèi)存的限制。當(dāng)無法申請到內(nèi)存時,拋出OutOfMemoryError異常。
總結(jié)
對于JVM的內(nèi)存管理, 最重要的還是與OS內(nèi)存管理知識進行類比以及結(jié)合實踐來學(xué)習(xí)。理解JVM內(nèi)存區(qū)域的目的也是為了在工程中出現(xiàn)內(nèi)存相關(guān)異常時能夠準確的定位所在區(qū)域,及時處理。
后續(xù)我們將在本文的基礎(chǔ)上來理解對象的創(chuàng)建過程以及OutOfMemoryError異常。
總結(jié)
以上是生活随笔為你收集整理的走进JVM【二】理解JVM内存区域的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Microsoft VBScript 编
- 下一篇: 分布式系统部署、监控与进程管理的几重境界