細(xì)說JVM(Java內(nèi)存區(qū)域劃分AND初探對象的創(chuàng)建)

一、前言

經(jīng)過一番思想斗爭,我決定好好的學(xué)習(xí)一下JVM,而對于一個JVM的初學(xué)者《深入理解Java虛擬機(jī)》當(dāng)然是必須拜讀的神作,所以這個專欄暫時會記錄我閱讀時的筆記吧,以后有可能真正深入學(xué)習(xí)Java虛擬機(jī)后,可能會有一些自己研究的成果,不過這估計是很久以后的事情了,看過這本書的也可以接機(jī)復(fù)習(xí)一下相關(guān)的知識,沒有看過書的,我盡量把我所學(xué)到的知識寫的通俗易懂一些,不過還是及其推薦閱讀一下《深入理解Java虛擬機(jī)》這本書,當(dāng)然閱讀這本書之前需要學(xué)習(xí)過計算機(jī)系統(tǒng)、計算機(jī)組成原理,如果沒有相關(guān)的知識背景,可能會看起來很困難,這里同時推薦一本書《深入理解計算機(jī)系統(tǒng)》,豆瓣評分9.9的神作,對于一個非底層程序員來說,這本書就把底層所有需要知道的知識全部講解了,最后當(dāng)然是如果有錯誤,希望指正,我會立即更改,以免誤導(dǎo)他人,好了那我就開始記錄我的讀書筆記了。
這里需要說明一下,《深入理解Java虛擬機(jī)》這本書之講解到了JDK1.7,所以如果出現(xiàn)和文章不同的內(nèi)容,可能是版本高于1.7的原因。

二、Java內(nèi)存區(qū)域的劃分

我在一年前開始學(xué)習(xí)Java的時候,馬士兵的視頻上就總是講解對象是存儲在堆中,引用是存放在棧上的,在看了這本書之后,發(fā)現(xiàn)這種想法是不準(zhǔn)確的,Java虛擬機(jī)的內(nèi)存區(qū)域分別為:方法區(qū)、虛擬機(jī)棧、本地方法棧、堆、程序計數(shù)器。

1、程序計數(shù)器

學(xué)習(xí)過計算機(jī)系統(tǒng)的應(yīng)該都知道程序計數(shù)器是什么東西,程序計數(shù)器用來存放計算機(jī)需要執(zhí)行的下一條指令的地址,在JVM中,功能也是這樣,用來存放JVM下一條虛擬機(jī)字節(jié)碼指令的地址,虛擬機(jī)字節(jié)碼指令就是JVM中的指令集,不過為了支持Java的多線程,每個線程都會有一個自己的程序計數(shù)器,各個線程之間互不影響,所以這部分內(nèi)存是線程私有的。另外因為JVM在運(yùn)行Java程序時可能會調(diào)用Native方法(本地方法,也就是當(dāng)前計算機(jī)系統(tǒng)的API),所以如果執(zhí)行的是本地方法,程序計數(shù)器的值為空。因為程序計數(shù)器只占用很小的一部分內(nèi)存空間,所以并不會發(fā)生內(nèi)存溢出的情況。

2、Java虛擬機(jī)棧

我們通常所說的引用存放在棧上,棧就指的是虛擬機(jī)棧,虛擬機(jī)棧同樣是線程私有的,虛擬機(jī)棧的作用是控制方法的執(zhí)行,我們可以想象當(dāng)我們運(yùn)行一個方法時,就相當(dāng)于把所有運(yùn)行該方法需要的數(shù)據(jù),一股腦的打包到一起,然后壓入虛擬機(jī)棧中,方法運(yùn)行完成后,再出棧,虛擬機(jī)棧就是用來控制方法的執(zhí)行的。而運(yùn)行一個方法所需要的信息有很多,這里會把所有的信息打包后放到一個棧幀中,棧幀中主要放了:局部變量表(就是方法中定義的局部參數(shù)、還有形參)、操作數(shù)棧、動態(tài)鏈接、方法出口。你可能對上面的這些名詞不是很懂,但是其實無所謂,現(xiàn)在只是有個印象,以后這些名詞都會深入的講解,現(xiàn)在只需要有個印象。

3、本地方法棧

本地方法棧我們一聽名字就可以猜出和本地方法有關(guān),其實和虛擬機(jī)棧類似,本地方法棧主要用來控制本地方法的運(yùn)行。

4、Java堆

我們從初學(xué)Java開始,應(yīng)該就會接觸到Java堆,我們會知道Java中的對象在運(yùn)行期間就是放在堆中的,我們從籠統(tǒng)的堆深入的學(xué)習(xí)一下,看一看Java堆到底是什么,但是這里也只是概覽一下,后面的文章會有更詳細(xì)的講解。
Java堆是線程共享的,所有線程的對象都創(chuàng)建在一個Java堆中,現(xiàn)在隨著技術(shù)的進(jìn)步,可能有部分對象并不一定在堆上,但是大部分的對象都是存儲在堆中。Java堆是垃圾收集器(GC)的重點關(guān)注對象,根據(jù)GC采用的收集算法,Java堆可以分為:新生代和老年代(更細(xì)致的分法再將GC的時候細(xì)說),從內(nèi)存分配的角度看,線程共享的Java堆中可能會根據(jù)線程劃分線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer ,TLAB)其實就是給每個線程分配一塊內(nèi)存,避免線程中對象的沖突。當(dāng)然以上的分法都不會影響堆中存放對象這個事實,只是為了更好地垃圾回收,或者更快的分配內(nèi)存。我們可以通過-Xmx-Xms這兩個虛擬機(jī)參數(shù)來規(guī)定堆所占用內(nèi)存空間的最大值和最小值。

5、方法區(qū)

現(xiàn)在我們已經(jīng)知道了在程序運(yùn)行期間,對象是放在堆中的,而方法執(zhí)行所需要的數(shù)據(jù)是存放在虛擬機(jī)棧中的,調(diào)用本地方法的數(shù)據(jù)存放在本地方法棧中,那么我們程序中的類、常量、靜態(tài)變量等信息存放在哪里呢?答案是方法區(qū)。
方法區(qū)是線程共享的內(nèi)存區(qū)域,它用于存儲虛擬機(jī)加載的類信息、常量、靜態(tài)變量、編譯器編譯后的代碼等數(shù)據(jù),在JDK1.8之前,這部分內(nèi)存區(qū)域通常叫做永久代,但是在JDK1.8之后,就再也沒有永久代了,這部分區(qū)域在書中被稱為方法區(qū),我另外看了一篇文章:Java永久代去哪兒了,上面叫做元空間,不過這里還是稱呼為方法區(qū)。因為方法區(qū)中的數(shù)據(jù)生命周期普遍比較長,所以垃圾收集行為比較少見,主要是對常量池的回收和類的卸載。
方法區(qū)中有一部分是運(yùn)行時常量池,用來存放程序運(yùn)行期間的常量值。

6、直接內(nèi)存

直接內(nèi)存并不屬于Java的內(nèi)存區(qū)域,但是卻和Java有關(guān),我們在進(jìn)行I/O操作的時候,可能會使用本地方法直接分配Java堆外的內(nèi)存,可以提高I/O操作的性能,但是這部分內(nèi)存并不屬于Java堆,受限于主機(jī)的內(nèi)存有限,可能會導(dǎo)致內(nèi)存溢出。

三、初探對象的創(chuàng)建

在我們編寫程序時,創(chuàng)建一個對象經(jīng)常就是new一個對象,但是在虛擬機(jī)中,對象是如何創(chuàng)建的呢?我們來研究一下。
當(dāng)虛擬機(jī)遇到一個new指令時,首先會去方法區(qū)的運(yùn)行時常量池中查看是否有該類的符號引用(這里的符號引用指的是類的全限定名),如果找不到,說明沒有這個類,如果有,說明有這個類,然后繼續(xù)檢查這個類是否已經(jīng)被虛擬機(jī)加載、解析、初始化過,如果沒有,就會進(jìn)行這些操作,將類加載到方法區(qū)中。
在進(jìn)行過上面的檢查后,虛擬機(jī)會為新生的對象分配內(nèi)存,在分配內(nèi)存時,如果Java堆中的堆存是絕對規(guī)整的(就是用過的內(nèi)存和空閑的內(nèi)存是分開的,然后中間用一個指針表示分界線),那么分配內(nèi)存就是把指針調(diào)整一下,把空閑的區(qū)域分出一部分來當(dāng)做新對象的內(nèi)存,這種分配方式稱為“指針碰撞”,如果內(nèi)存并不是規(guī)整的,使用過的內(nèi)存和空閑的內(nèi)存相互交錯,那么虛擬機(jī)就需要維護(hù)一個表格來記錄那些內(nèi)存時使用過的,那些是空閑的,然后從空閑的內(nèi)存中取出足夠大的一部分作為新對象的內(nèi)存空間,這種方法叫做“空閑列表”,選擇哪種分配方法是由Java堆是否規(guī)整決定的,而Java堆是否規(guī)整是由垃圾收集器是否帶有壓縮整理功能決定的。
至于如何在分配內(nèi)存的時候?qū)崿F(xiàn)線程間的安全,一種是使用CAS配上失敗重試來保證線程安全(如果不知道什么是CAS可以自己百度一下),另一種就是上面提到過的線程分配緩沖。
內(nèi)存分配完成后,虛擬機(jī)將分配到的內(nèi)存空間全部初始化為零值,接下來,虛擬機(jī)對對象進(jìn)行必要的設(shè)置,將對象所屬哪個類,對象的哈希碼、對象的GC分代年齡等信息存放在對象的對象頭中,、至此為止,虛擬機(jī)層面的對象創(chuàng)建完成,一個新的對象已經(jīng)產(chǎn)生了。

四、對象的內(nèi)存布局

我們現(xiàn)在來學(xué)習(xí)一下對象在內(nèi)存中的儲存布局,可以分為三個區(qū)域:對象頭、實例數(shù)據(jù)、對齊填充。

1、對象頭

對象頭中儲存了兩部分信息,第一部分存儲了對象自身的運(yùn)行時數(shù)據(jù),比如哈希嗎、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等。另一部分就是該對象的類型指針,用于確定該對象是哪個類的實例,如果該對象是一個數(shù)組,還會包含數(shù)組的長度信息。

2、實例數(shù)據(jù)

就是該對象在程序代碼中所定義的各種類型的字段內(nèi)容(包括繼承自父類的),注意對象的方法并不存儲在這里,方法存儲在虛擬機(jī)棧中。

3、對齊填空

就是為了確保對象的內(nèi)存大小必須是8字節(jié)的整數(shù)倍。

五、對象的訪問定位

對象的訪問定位主要包含兩種:

1、句柄

Java堆中將會劃分出一塊內(nèi)存來作為句柄池,reference中 存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信 息,如下圖所示。


20170709144433534.jpg
2、直接指針

使用直接指針訪問,Java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而reference中存儲的直接就是對象地址,如下圖所示。


20170709144642209.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容