Java内存区域分布
本文是《深入理解Java虛擬機(jī)》中第二章的讀書總結(jié)。
看了幾個星期《深入理解Java虛擬機(jī)》,覺得有必要將看到的東西記錄下來,以便日后看看。
Java是平臺無關(guān)的語言,也就是說Java代碼可以運(yùn)行在不同的機(jī)器上,要實(shí)現(xiàn)這種“一次編碼,處處運(yùn)行”的目標(biāo),Java使用虛擬機(jī)來消除平臺多樣性。Java相比于C和C++不同的一個地方在于,Java程序員不需要手動回收內(nèi)存,而C/C++需要使用delete/free代碼來手動釋放不使用的內(nèi)存。那么,這里首先介紹一下Java虛擬機(jī)的內(nèi)存區(qū)域是如何組織的。
Java虛擬機(jī)在執(zhí)行Java程序的過程中會把管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時間。Java虛擬機(jī)中內(nèi)存的總體分布如圖所示:
一共包括方法區(qū)、虛擬機(jī)棧、本地方法棧、堆、程序計(jì)數(shù)器等部分。其中,方法區(qū)和堆是所有線程共享的內(nèi)存區(qū)域,而虛擬機(jī)棧、本地方法棧和程序計(jì)數(shù)器是線程私有的內(nèi)存區(qū)域,線程間不共享這部分的數(shù)據(jù)。
1、程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。Java虛擬機(jī)的程序計(jì)數(shù)器和CPU的程序計(jì)數(shù)器類似,CPU中執(zhí)行的是機(jī)器碼,使用程序計(jì)數(shù)器來指示下一條指令的地址;類似的,在虛擬機(jī)的概念模型里,字節(jié)碼解釋器工作時就是通過改變這個計(jì)數(shù)器的值來選定下一條要執(zhí)行的字節(jié)碼指令的行號,因?yàn)镴ava代碼編譯后的Class文件中的字節(jié)碼指令是一行一行的,這部分的內(nèi)容會在后面介紹。Java程序的分支、循環(huán)、跳轉(zhuǎn)、異常處理和線程恢復(fù)等基礎(chǔ)功能都需要程序計(jì)數(shù)器來完成。
由于程序計(jì)數(shù)器和字節(jié)碼指令的執(zhí)行有關(guān),而每個線程都有各自要執(zhí)行的字節(jié)碼指令。Java虛擬機(jī)的多線程是通過為每個線程輪流分配處理器的使用時間來實(shí)現(xiàn)的,同一時刻一個處理器只會執(zhí)行一條線程中的指令。因此,為了處理器切換線程后能夠從正確的地方繼續(xù)執(zhí)行,每個線程都應(yīng)該有自己的程序計(jì)數(shù)器,這樣多線程執(zhí)行的時候才不會相互影響。即,程序計(jì)數(shù)器是線程私有的內(nèi)存。
如果線程正在執(zhí)行的是一個Java方法,這個計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,這個計(jì)數(shù)器值則為空。此區(qū)域是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
2、Java虛擬機(jī)棧
和程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,生命周期與線程相同。在Java虛擬機(jī)中,每個方法的執(zhí)行都會創(chuàng)建一個棧幀(Stack Frame),棧幀用來存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出入口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)中入棧到出棧的信息。
Java每一個線程都會涉及到很多的方法調(diào)用,因此每個線程都有自己的虛擬機(jī)棧,即線程私有。
棧幀中的局部變量表存放了編譯期間可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long和double)、對象引用(Reference類型,不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?#xff0c;也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
局部變量表中存放數(shù)據(jù)的基本單元是局部變量空間(Slot),除了64為長度的long和double使用兩個slot外,其他所有類型的數(shù)據(jù)都使用一個slot。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個方法時,這個方法所需要的空間大小是完全確定的,在方法運(yùn)行期間不會改變局部變量表的大小。
Java虛擬機(jī)規(guī)范對Java虛擬機(jī)棧規(guī)定了兩個異常情況:如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)棧可以動態(tài)擴(kuò)展,如果擴(kuò)展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。
3、本地方法棧
本地方法棧(Native Method Stack)和虛擬機(jī)棧的作用非常相似,區(qū)別就是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java代碼服務(wù),而本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)。和虛擬機(jī)棧一樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。同時,本地方法棧也是線程私有的。
4、Java堆
對于大多數(shù)應(yīng)用來說,Java堆(Java Heap)是Java虛擬機(jī)鎖管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊區(qū)域,在虛擬機(jī)啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。隨著JIT編譯器的發(fā)展和逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化。
Java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為“GC堆”。從內(nèi)存回收的角度看,由于現(xiàn)在的垃圾收集器基本采用分代收集算法,所以Java堆還可以分為新生代和老年代;再細(xì)致的有Eden空間、From Survivor空間和To Survivor空間等。對于垃圾回收的內(nèi)容會在后面介紹。從內(nèi)存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。不過無論怎么劃分,都與存放內(nèi)容無關(guān),無論哪個區(qū)域,存儲的仍都是對象實(shí)例。
Java堆可以處理物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。在實(shí)現(xiàn)上,堆既可以是大小固定的,也可以是可擴(kuò)展的,不過當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(使用-Xmx和-Xms控制)。如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法擴(kuò)展時,將會拋出OutOfMemoryError異常。
5、方法區(qū)
方法區(qū)(Method Area)也是線程共享的內(nèi)存區(qū)域,用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量和即時編譯器編譯后的代碼等數(shù)據(jù)。
Java虛擬機(jī)規(guī)范對方法區(qū)的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。相對而言,垃圾收集行為在這個區(qū)域是較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就永久存在了。這個區(qū)域的內(nèi)存回收目標(biāo)主要針對常量池的回收和對類型的卸載。一般來說,由于類型的卸載的條件非常苛刻,所以這部分的垃圾回收成績不令人滿意。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。
6、運(yùn)行時常量池
運(yùn)行時常量池(Running Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放編譯期間生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放。Java虛擬機(jī)規(guī)范沒有做任何關(guān)于此部分的細(xì)節(jié)要求,一般來說,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運(yùn)行時常量池中。
運(yùn)行時常量池相對于Class文件常量池的另一個重要特征是具備動態(tài)性,Java語言并不要求常量一定只有編譯期間才能產(chǎn)生,也就是并非欲置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常量池,運(yùn)行期間也可能將新的常量池放入池中。
既然運(yùn)行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法申請到內(nèi)存時會拋出OutOfMemoryError異常。
7、直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁的使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn),所以也放在這里介紹。
在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場合中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。
由于直接使用本機(jī)內(nèi)存,所以當(dāng)內(nèi)存不夠時也會拋出OutOfMemoryError異常。
總結(jié)
以上是生活随笔為你收集整理的Java内存区域分布的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 战地2042游戏闪退怎么办
- 下一篇: HotSpot虚拟机在Java堆中对对象