運(yùn)行時(shí)數(shù)據(jù)區(qū)
官方文檔:Run-Time Data Areas

關(guān)于內(nèi)存
內(nèi)存拆解

此思維導(dǎo)圖,更多信息參考:內(nèi)存拆解
Java虛擬機(jī)(JVM)
程序計(jì)數(shù)器
由于Java虛擬機(jī)的多線程是通過線程輪流切換、分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一 個(gè)確定的時(shí)刻,一個(gè)處理器(對于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因 此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程 之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
Java虛擬機(jī)棧
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是本地方法棧則是為虛擬機(jī)使用到的本地(Native) 方法服務(wù)。
本地方法棧
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是本地方法棧則是為虛擬機(jī)使用到的本地(Native) 方法服務(wù)。
Java堆
是虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所 有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
在《Java虛擬機(jī)規(guī)范》中對Java堆的描述是:“所有的對象實(shí)例以及數(shù)組都應(yīng)當(dāng)在堆上分配[1]”,而這里筆者寫的“幾乎”是指從實(shí)現(xiàn)角度來看,隨著Java語言的發(fā)展,現(xiàn)在已經(jīng)能看到些許跡象表明日后可能出現(xiàn)值類型的支持,即使只考慮現(xiàn)在,由于即時(shí)編譯技術(shù)的進(jìn)步,尤其是逃逸分析技術(shù)的日漸強(qiáng)大,棧上分配、標(biāo)量替換[2]優(yōu)化手段已經(jīng)導(dǎo)致一些微妙的變化悄然發(fā)生,所以說Java對象實(shí)例都分配在堆上也漸漸變得不是那么絕對了。
Java堆是垃圾收集器管理的內(nèi)存區(qū)域,因此一些資料中它也被稱作“GC堆”(Garbage CollectedHeap,幸好國內(nèi)沒翻譯成“垃圾堆”)。從回收內(nèi)存的角度看,由于現(xiàn)代垃圾收集器大部分都是基于分代收集理論設(shè)計(jì)的,所以Java堆中經(jīng)常會(huì)出現(xiàn)“新生代”“老年代”“永久代”“Eden空間”“From Survivor空間”“To Survivor空間”等名詞,這些概念在本書后續(xù)章節(jié)中還會(huì)反復(fù)登場亮相,在這里筆者想先說明的是這些區(qū)域劃分僅僅是一部分垃圾收集器的共同特性或者說設(shè)計(jì)風(fēng)格而已,而非某個(gè)Java虛擬機(jī)具體實(shí)現(xiàn)的固有內(nèi)存布局,更不是《Java虛擬機(jī)規(guī)范》里對Java堆的進(jìn)一步細(xì)致劃分。不少資料上經(jīng)常寫著類似于“Java虛擬機(jī)的堆內(nèi)存分為新生代、老年代、永久代、Eden、Survivor……”這樣的內(nèi)容。在十年之前(以G1收集器的出現(xiàn)為分界),作為業(yè)界絕對主流的HotSpot虛擬機(jī),它內(nèi)部的垃圾收集器全部都基于“經(jīng)典分代”[3]來設(shè)計(jì),需要新生代、老年代收集器搭配才能工作,在這種背景下,上述說法還算是不會(huì)產(chǎn)生太大歧義。但是到了今天,垃圾收集器技術(shù)與十年前已不可同日而語,HotSpot里面也出現(xiàn)了不采用分代設(shè)計(jì)的新垃圾收集器,再按照上面的提法就有很多需要商榷的地方了。
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的,這點(diǎn)就像我們用磁盤空間去存儲(chǔ)文件一樣,并不要求每個(gè)文件都連續(xù)存放。但對于大對象(典型的如數(shù)組對象),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于實(shí)現(xiàn)簡單、存儲(chǔ)高效的考慮,很可能會(huì)要求連續(xù)的內(nèi)存空間。
Java堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過當(dāng)前主流的Java虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過參數(shù)-Xmx和-Xms設(shè)定)。如果在Java堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),Java虛擬機(jī)將會(huì)拋出OutOfMemoryError異常。
[1] 《Java虛擬機(jī)規(guī)范》中的原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated。
[2] 逃逸分析與標(biāo)量替換的相關(guān)內(nèi)容,請參見第11章的相關(guān)內(nèi)容。
[3] 指新生代(其中又包含一個(gè)Eden和兩個(gè)Survivor)、老年代這種劃分,源自UC Berkeley在20世紀(jì)80年代中期開發(fā)的Berkeley Smalltalk。歷史上有多款虛擬機(jī)采用了這種設(shè)計(jì),包括HotSpot和它的前身Self和Strongtalk虛擬機(jī)(見第1章),原始論文是:https://dl.acm.org/citation.cfm?id=808261。
方法區(qū)
方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然《Java虛擬機(jī)規(guī)范》中把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫作“非堆”(Non-Heap),目的是與Java堆區(qū)分開來。
說到方法區(qū),不得不提一下“永久代”這個(gè)概念,尤其是在JDK 8以前,許多Java程序員都習(xí)慣在HotSpot虛擬機(jī)上開發(fā)、部署程序,很多人都更愿意把方法區(qū)稱呼為“永久代”(PermanentGeneration),或?qū)烧呋鞛橐徽?。本質(zhì)上這兩者并不是等價(jià)的,因?yàn)閮H僅是當(dāng)時(shí)的HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把收集器的分代設(shè)計(jì)擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣使得HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。但是對于其他虛擬機(jī)實(shí)現(xiàn),譬如BEA JRockit、IBM J9等來說,是不存在永久代的概念的。原則上如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受《Java虛擬機(jī)規(guī)范》管束,并不要求統(tǒng)一。但現(xiàn)在回頭來看,當(dāng)年使用永久代來實(shí)現(xiàn)方法區(qū)的決定并不是一個(gè)好主意,這種設(shè)計(jì)導(dǎo)致了Java應(yīng)用更容易遇到內(nèi)存溢出的問題(永久代有-XX:MaxPermSize的上限,即使不設(shè)置也有默認(rèn)大小,而J9和JRockit只要沒有觸碰到進(jìn)程可用內(nèi)存的上限,例如32位系統(tǒng)中的4GB限制,就不會(huì)出問題),而且有極少數(shù)方法(例如String::intern())會(huì)因永久代的原因而導(dǎo)致不同虛擬機(jī)下有不同的表現(xiàn)。當(dāng)Oracle收購BEA獲得了JRockit的所有權(quán)后,準(zhǔn)備把JRockit中的優(yōu)秀功能,譬如Java Mission Control管理工具,移植到HotSpot虛擬機(jī)時(shí),但因?yàn)閮烧邔Ψ椒▍^(qū)實(shí)現(xiàn)的差異而面臨諸多困難??紤]到HotSpot未來的發(fā)展,在JDK 6的時(shí)候HotSpot開發(fā)團(tuán)隊(duì)就有放棄永久代,逐步改為采用本地內(nèi)存(Native Memory)來實(shí)現(xiàn)方法區(qū)的計(jì)劃了[1],到了JDK 7的HotSpot,已經(jīng)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Metaspace)來代替,把JDK 7中永久代還剩余的內(nèi)容(主要是類型信息)全部移到元空間中。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
Java虛擬機(jī)對于Class文件每一部分(自然也包括常量池)的格式都有嚴(yán)格規(guī)定,如每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范上的要求才會(huì)被虛擬機(jī)認(rèn)可、加載和執(zhí)行,但對于運(yùn)行時(shí)常量池,《Java虛擬機(jī)規(guī)范》并沒有做任何細(xì)節(jié)的要求,不同提供商實(shí)現(xiàn)的虛擬機(jī)可以按照自己的需要來實(shí)現(xiàn)這個(gè)內(nèi)存區(qū)域,不過一般來說,除了保存Class文件中描述的符號(hào)引用外,還會(huì)把由符號(hào)引用翻譯出來的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中[1]。
運(yùn)行時(shí)常量池相對于Class文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是說,并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是String類的intern()方法。
既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。
[1] 關(guān)于Class文件格式、符號(hào)引用等概念可參見第6章。
直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn),所以我們放到這里一起講解。
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。
顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到Java堆大小的限制,但是,既然是內(nèi)存,則肯定還是會(huì)受到本機(jī)總內(nèi)存(包括物理內(nèi)存、SWAP分區(qū)或者分頁文件)大小以及處理器尋址空間的限制,一般服務(wù)器管理員配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存去設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略掉直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。
更多內(nèi)容,參考:Java內(nèi)存區(qū)域