一、前言
經(jīng)過(guò)一番思想斗爭(zhēng),我決定好好的學(xué)習(xí)一下JVM,而對(duì)于一個(gè)JVM的初學(xué)者《深入理解Java虛擬機(jī)》當(dāng)然是必須拜讀的神作,所以這個(gè)專(zhuān)欄暫時(shí)會(huì)記錄我閱讀時(shí)的筆記吧,以后有可能真正深入學(xué)習(xí)Java虛擬機(jī)后,可能會(huì)有一些自己研究的成果,不過(guò)這估計(jì)是很久以后的事情了,看過(guò)這本書(shū)的也可以接機(jī)復(fù)習(xí)一下相關(guān)的知識(shí),沒(méi)有看過(guò)書(shū)的,我盡量把我所學(xué)到的知識(shí)寫(xiě)的通俗易懂一些,不過(guò)還是及其推薦閱讀一下《深入理解Java虛擬機(jī)》這本書(shū),當(dāng)然閱讀這本書(shū)之前需要學(xué)習(xí)過(guò)計(jì)算機(jī)系統(tǒng)、計(jì)算機(jī)組成原理,如果沒(méi)有相關(guān)的知識(shí)背景,可能會(huì)看起來(lái)很困難,這里同時(shí)推薦一本書(shū)《深入理解計(jì)算機(jī)系統(tǒng)》,豆瓣評(píng)分9.9的神作,對(duì)于一個(gè)非底層程序員來(lái)說(shuō),這本書(shū)就把底層所有需要知道的知識(shí)全部講解了,最后當(dāng)然是如果有錯(cuò)誤,希望指正,我會(huì)立即更改,以免誤導(dǎo)他人,好了那我就開(kāi)始記錄我的讀書(shū)筆記了。
這里需要說(shuō)明一下,《深入理解Java虛擬機(jī)》這本書(shū)之講解到了JDK1.7,所以如果出現(xiàn)和文章不同的內(nèi)容,可能是版本高于1.7的原因。
二、Java內(nèi)存區(qū)域的劃分
我在一年前開(kāi)始學(xué)習(xí)Java的時(shí)候,馬士兵的視頻上就總是講解對(duì)象是存儲(chǔ)在堆中,引用是存放在棧上的,在看了這本書(shū)之后,發(fā)現(xiàn)這種想法是不準(zhǔn)確的,Java虛擬機(jī)的內(nèi)存區(qū)域分別為:方法區(qū)、虛擬機(jī)棧、本地方法棧、堆、程序計(jì)數(shù)器。
1、程序計(jì)數(shù)器
學(xué)習(xí)過(guò)計(jì)算機(jī)系統(tǒng)的應(yīng)該都知道程序計(jì)數(shù)器是什么東西,程序計(jì)數(shù)器用來(lái)存放計(jì)算機(jī)需要執(zhí)行的下一條指令的地址,在JVM中,功能也是這樣,用來(lái)存放JVM下一條虛擬機(jī)字節(jié)碼指令的地址,虛擬機(jī)字節(jié)碼指令就是JVM中的指令集,不過(guò)為了支持Java的多線程,每個(gè)線程都會(huì)有一個(gè)自己的程序計(jì)數(shù)器,各個(gè)線程之間互不影響,所以這部分內(nèi)存是線程私有的。另外因?yàn)镴VM在運(yùn)行Java程序時(shí)可能會(huì)調(diào)用Native方法(本地方法,也就是當(dāng)前計(jì)算機(jī)系統(tǒng)的API),所以如果執(zhí)行的是本地方法,程序計(jì)數(shù)器的值為空。因?yàn)槌绦蛴?jì)數(shù)器只占用很小的一部分內(nèi)存空間,所以并不會(huì)發(fā)生內(nèi)存溢出的情況。
2、Java虛擬機(jī)棧
我們通常所說(shuō)的引用存放在棧上,棧就指的是虛擬機(jī)棧,虛擬機(jī)棧同樣是線程私有的,虛擬機(jī)棧的作用是控制方法的執(zhí)行,我們可以想象當(dāng)我們運(yùn)行一個(gè)方法時(shí),就相當(dāng)于把所有運(yùn)行該方法需要的數(shù)據(jù),一股腦的打包到一起,然后壓入虛擬機(jī)棧中,方法運(yùn)行完成后,再出棧,虛擬機(jī)棧就是用來(lái)控制方法的執(zhí)行的。而運(yùn)行一個(gè)方法所需要的信息有很多,這里會(huì)把所有的信息打包后放到一個(gè)棧幀中,棧幀中主要放了:局部變量表(就是方法中定義的局部參數(shù)、還有形參)、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口。你可能對(duì)上面的這些名詞不是很懂,但是其實(shí)無(wú)所謂,現(xiàn)在只是有個(gè)印象,以后這些名詞都會(huì)深入的講解,現(xiàn)在只需要有個(gè)印象。
3、本地方法棧
本地方法棧我們一聽(tīng)名字就可以猜出和本地方法有關(guān),其實(shí)和虛擬機(jī)棧類(lèi)似,本地方法棧主要用來(lái)控制本地方法的運(yùn)行。
4、Java堆
我們從初學(xué)Java開(kāi)始,應(yīng)該就會(huì)接觸到Java堆,我們會(huì)知道Java中的對(duì)象在運(yùn)行期間就是放在堆中的,我們從籠統(tǒng)的堆深入的學(xué)習(xí)一下,看一看Java堆到底是什么,但是這里也只是概覽一下,后面的文章會(huì)有更詳細(xì)的講解。
Java堆是線程共享的,所有線程的對(duì)象都創(chuàng)建在一個(gè)Java堆中,現(xiàn)在隨著技術(shù)的進(jìn)步,可能有部分對(duì)象并不一定在堆上,但是大部分的對(duì)象都是存儲(chǔ)在堆中。Java堆是垃圾收集器(GC)的重點(diǎn)關(guān)注對(duì)象,根據(jù)GC采用的收集算法,Java堆可以分為:新生代和老年代(更細(xì)致的分法再將GC的時(shí)候細(xì)說(shuō)),從內(nèi)存分配的角度看,線程共享的Java堆中可能會(huì)根據(jù)線程劃分線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer ,TLAB)其實(shí)就是給每個(gè)線程分配一塊內(nèi)存,避免線程中對(duì)象的沖突。當(dāng)然以上的分法都不會(huì)影響堆中存放對(duì)象這個(gè)事實(shí),只是為了更好地垃圾回收,或者更快的分配內(nèi)存。我們可以通過(guò)-Xmx和-Xms這兩個(gè)虛擬機(jī)參數(shù)來(lái)規(guī)定堆所占用內(nèi)存空間的最大值和最小值。
5、方法區(qū)
現(xiàn)在我們已經(jīng)知道了在程序運(yùn)行期間,對(duì)象是放在堆中的,而方法執(zhí)行所需要的數(shù)據(jù)是存放在虛擬機(jī)棧中的,調(diào)用本地方法的數(shù)據(jù)存放在本地方法棧中,那么我們程序中的類(lèi)、常量、靜態(tài)變量等信息存放在哪里呢?答案是方法區(qū)。
方法區(qū)是線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、編譯器編譯后的代碼等數(shù)據(jù),在JDK1.8之前,這部分內(nèi)存區(qū)域通常叫做永久代,但是在JDK1.8之后,就再也沒(méi)有永久代了,這部分區(qū)域在書(shū)中被稱為方法區(qū),我另外看了一篇文章:Java永久代去哪兒了,上面叫做元空間,不過(guò)這里還是稱呼為方法區(qū)。因?yàn)榉椒▍^(qū)中的數(shù)據(jù)生命周期普遍比較長(zhǎng),所以垃圾收集行為比較少見(jiàn),主要是對(duì)常量池的回收和類(lèi)的卸載。
方法區(qū)中有一部分是運(yùn)行時(shí)常量池,用來(lái)存放程序運(yùn)行期間的常量值。
6、直接內(nèi)存
直接內(nèi)存并不屬于Java的內(nèi)存區(qū)域,但是卻和Java有關(guān),我們?cè)谶M(jìn)行I/O操作的時(shí)候,可能會(huì)使用本地方法直接分配Java堆外的內(nèi)存,可以提高I/O操作的性能,但是這部分內(nèi)存并不屬于Java堆,受限于主機(jī)的內(nèi)存有限,可能會(huì)導(dǎo)致內(nèi)存溢出。
三、初探對(duì)象的創(chuàng)建
在我們編寫(xiě)程序時(shí),創(chuàng)建一個(gè)對(duì)象經(jīng)常就是new一個(gè)對(duì)象,但是在虛擬機(jī)中,對(duì)象是如何創(chuàng)建的呢?我們來(lái)研究一下。
當(dāng)虛擬機(jī)遇到一個(gè)new指令時(shí),首先會(huì)去方法區(qū)的運(yùn)行時(shí)常量池中查看是否有該類(lèi)的符號(hào)引用(這里的符號(hào)引用指的是類(lèi)的全限定名),如果找不到,說(shuō)明沒(méi)有這個(gè)類(lèi),如果有,說(shuō)明有這個(gè)類(lèi),然后繼續(xù)檢查這個(gè)類(lèi)是否已經(jīng)被虛擬機(jī)加載、解析、初始化過(guò),如果沒(méi)有,就會(huì)進(jìn)行這些操作,將類(lèi)加載到方法區(qū)中。
在進(jìn)行過(guò)上面的檢查后,虛擬機(jī)會(huì)為新生的對(duì)象分配內(nèi)存,在分配內(nèi)存時(shí),如果Java堆中的堆存是絕對(duì)規(guī)整的(就是用過(guò)的內(nèi)存和空閑的內(nèi)存是分開(kāi)的,然后中間用一個(gè)指針表示分界線),那么分配內(nèi)存就是把指針調(diào)整一下,把空閑的區(qū)域分出一部分來(lái)當(dāng)做新對(duì)象的內(nèi)存,這種分配方式稱為“指針碰撞”,如果內(nèi)存并不是規(guī)整的,使用過(guò)的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那么虛擬機(jī)就需要維護(hù)一個(gè)表格來(lái)記錄那些內(nèi)存時(shí)使用過(guò)的,那些是空閑的,然后從空閑的內(nèi)存中取出足夠大的一部分作為新對(duì)象的內(nèi)存空間,這種方法叫做“空閑列表”,選擇哪種分配方法是由Java堆是否規(guī)整決定的,而Java堆是否規(guī)整是由垃圾收集器是否帶有壓縮整理功能決定的。
至于如何在分配內(nèi)存的時(shí)候?qū)崿F(xiàn)線程間的安全,一種是使用CAS配上失敗重試來(lái)保證線程安全(如果不知道什么是CAS可以自己百度一下),另一種就是上面提到過(guò)的線程分配緩沖。
內(nèi)存分配完成后,虛擬機(jī)將分配到的內(nèi)存空間全部初始化為零值,接下來(lái),虛擬機(jī)對(duì)對(duì)象進(jìn)行必要的設(shè)置,將對(duì)象所屬哪個(gè)類(lèi),對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息存放在對(duì)象的對(duì)象頭中,、至此為止,虛擬機(jī)層面的對(duì)象創(chuàng)建完成,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了。
四、對(duì)象的內(nèi)存布局
我們現(xiàn)在來(lái)學(xué)習(xí)一下對(duì)象在內(nèi)存中的儲(chǔ)存布局,可以分為三個(gè)區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)齊填充。
1、對(duì)象頭
對(duì)象頭中儲(chǔ)存了兩部分信息,第一部分存儲(chǔ)了對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),比如哈希嗎、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。另一部分就是該對(duì)象的類(lèi)型指針,用于確定該對(duì)象是哪個(gè)類(lèi)的實(shí)例,如果該對(duì)象是一個(gè)數(shù)組,還會(huì)包含數(shù)組的長(zhǎng)度信息。
2、實(shí)例數(shù)據(jù)
就是該對(duì)象在程序代碼中所定義的各種類(lèi)型的字段內(nèi)容(包括繼承自父類(lèi)的),注意對(duì)象的方法并不存儲(chǔ)在這里,方法存儲(chǔ)在虛擬機(jī)棧中。
3、對(duì)齊填空
就是為了確保對(duì)象的內(nèi)存大小必須是8字節(jié)的整數(shù)倍。
五、對(duì)象的訪問(wèn)定位
對(duì)象的訪問(wèn)定位主要包含兩種:
1、句柄
Java堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中 存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自的具體地址信 息,如下圖所示。

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