說明
接下來會(huì)用三個(gè)篇幅來學(xué)習(xí)總結(jié) JVM,所涉及內(nèi)容如下圖:

定義
JVM 全稱 Java Virtual Machine,也就是我們耳熟能詳?shù)?Java 虛擬機(jī)。它能識(shí)別 .class后綴的文件,并且能夠?qū)⑵浣馕龀删唧w的機(jī)器指令,最終調(diào)用操作系統(tǒng)上的函數(shù),完成我們想要的操作。
特性
JVM 的存在使得 Java 程序具有運(yùn)行平臺(tái)無關(guān)性的特點(diǎn)。
JVM、JRE、JDK 的關(guān)系
JVM只是一個(gè)翻譯,把Class翻譯成機(jī)器識(shí)別的代碼,但是需要注意,JVM 不會(huì)自己生成代碼,需要大家編寫代碼,同時(shí)需要很多依賴類庫,這個(gè)時(shí)候就需要用到JRE。
JRE是什么,它除了包含JVM之外,提供了很多的類庫(就是我們說的jar包,它可以提供一些即插即用的功能,比如讀取或者操作文件,連接網(wǎng)絡(luò),使用I/O等等之類的)這些東西就是JRE提供的基礎(chǔ)類庫。JVM 標(biāo)準(zhǔn)加上實(shí)現(xiàn)的一大堆基礎(chǔ)類庫,就組成了 Java 的運(yùn)行時(shí)環(huán)境,也就是我們常說的 JRE(Java Runtime Environment)。
但對(duì)于程序員來說,JRE還不夠。我寫完要編譯代碼,還需要調(diào)試代碼,還需要打包代碼、有時(shí)候還需要反編譯代碼。所以我們會(huì)使用JDK,因?yàn)镴DK還提供了一些非常好用的小工具,比如 javac(編譯代碼)、java、jar (打包代碼)、javap(反編譯<反匯編>)等。這個(gè)就是JDK。
其實(shí)他們是種包含關(guān)系,如下圖:

角色
Java程序運(yùn)行在操作系統(tǒng)上,JVM 的作用是將源程序編譯后的class、jar 包等字節(jié)碼文件,翻譯生成機(jī)器指令。如下圖:

構(gòu)成
JVM 是由,類裝載器子系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū)、執(zhí)行引擎、本地方法接口和垃圾收集模塊等五大模塊組成。如下圖:

類加載器
單獨(dú)成章
執(zhí)行引擎
單獨(dú)成章
本地方法接口
后續(xù)單獨(dú)成章
垃圾回收器
見 JVM(下)
運(yùn)行時(shí)數(shù)據(jù)區(qū)
見上圖,運(yùn)行時(shí)數(shù)據(jù)區(qū)是在程序運(yùn)行過程中操作系統(tǒng)劃分給 JVM 的一塊內(nèi)存區(qū)域,分為 5 個(gè)部分,虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器、堆和方法區(qū)。這 5 部分又歸為 2 種,其中前 3 個(gè)為線程獨(dú)有區(qū),而堆和方法區(qū)為線程共享區(qū)域。
程序計(jì)數(shù)器
每個(gè)線程都有一個(gè)單獨(dú)的程序計(jì)數(shù)器,用來記錄當(dāng)前線程執(zhí)行的字節(jié)碼的位置。其實(shí)如果不考慮 OS 多線程的話,是不需要程序計(jì)數(shù)器的,一個(gè)線程可以直接跑完結(jié)束。但是現(xiàn)實(shí)情況同時(shí)運(yùn)行的線程數(shù)目大于 OS 核心數(shù) *2 時(shí),就需要 CPU 時(shí)間片輪轉(zhuǎn)來分配線程輪流執(zhí)行,這個(gè)時(shí)候就需要它來記錄每個(gè)線程的執(zhí)行位置了。它有以下幾個(gè)特點(diǎn)需要注意:
- 程序計(jì)數(shù)器具有線程隔離性
- 程序計(jì)數(shù)器占用的內(nèi)存空間非常小,可以忽略不計(jì),而且它是運(yùn)行時(shí)數(shù)據(jù)區(qū)唯一不會(huì)出現(xiàn) OOM 的區(qū)域
- 程序執(zhí)行的時(shí)候,程序計(jì)數(shù)器是有值的,其記錄的是程序正在執(zhí)行的字節(jié)碼的地址
- 執(zhí)行native本地方法時(shí),程序計(jì)數(shù)器的值為空。原因是native方法是java通過jni調(diào)用本地C/C++庫來實(shí)現(xiàn),非java字節(jié)碼實(shí)現(xiàn),所以無法統(tǒng)計(jì)
虛擬機(jī)棧
棧,是一種后入先出的數(shù)據(jù)結(jié)構(gòu),其中存放一個(gè)或者多個(gè),當(dāng)前線程正在執(zhí)行的方法包裝成的棧幀,這些棧幀是按照后入在上的順序存放的。當(dāng)前執(zhí)行的棧幀在最上邊。每個(gè)棧幀是由局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、完成出口組成的。如下虛擬機(jī)棧示意圖:

我們來寫一段代碼,來看一下,方法是怎樣在棧中執(zhí)行的:
public class Person {
public int work(){
int x = 1;
int y = 2;
int z = (x + y)*10;
return z;
}
public static void main(String[] args){
Person person = new Person();
person.work();
}
}
我們將上邊代碼在編輯器運(yùn)行一遍,在項(xiàng)目根目錄的 out文件夾下對(duì)應(yīng)的包名會(huì)有編譯好的 Person.class 文件,找到該文件拖到終端命令行,class 文件同級(jí)目錄下執(zhí)行javap -c Person.class 反匯編命令,會(huì)在終端得到該段代碼的匯編指令如下:

我們先口述一下這段代碼的執(zhí)行過程:
1、main-code-0: new 首先 JVM 會(huì)先將這個(gè) Person.class 元數(shù)據(jù)加載到 內(nèi)存中的方法區(qū) (Method Area) 中(信息都包括常量池信息,方法的定義 以及編譯后的方法實(shí)現(xiàn)的二進(jìn)制形式的機(jī)器指令,所有的線程共享一個(gè)方法區(qū),從中讀取方法定義和方法的指令集),將其引用壓入操作數(shù)棧(new指令并不能完全創(chuàng)建一個(gè)對(duì)象,對(duì)象只有在初,只有在調(diào)用初始化方法完成后(也就是調(diào)用了invokespecial指令之后),對(duì)象才創(chuàng)建成功)
2、main-code-3: dup 將操作數(shù)棧定的數(shù)據(jù)復(fù)制一份,并壓入棧,此時(shí)棧中有兩個(gè)引用值
3、main-code-4: invokespecial pop出棧引用值,調(diào)用其構(gòu)造函數(shù),完成對(duì)象的初始化
4、main-code-7: astore_1pop出棧引用值,將 person 賦值給局部變量表中的 index = 1的位置
5、main-code-8: aload_1將局部變量表中的 person 壓入棧,因?yàn)?person 調(diào)用了 work方法
6、main-code-9: invokespecial將 person 出棧,調(diào)用 main 中的 invokespecial 指令:
首先進(jìn)行方法符號(hào)引用校驗(yàn),查找是否有 work() 這個(gè)方法的定義
然后為新的方法調(diào)用創(chuàng)建新的棧幀,JVM 會(huì)為此方法 greeting 創(chuàng)建一個(gè)新的棧幀(VM stack),并根據(jù) greeting 中操作數(shù)棧的大小和局部變量的數(shù)量分別創(chuàng)建相應(yīng)大小的操作數(shù)棧;然后將此棧幀推到虛擬機(jī)棧的棧頂。
更新PC指令計(jì)數(shù)器的值,將當(dāng)前 PC 程序計(jì)數(shù)器的值記錄到 greeting 棧幀中,當(dāng) greeting 執(zhí)行完成后,以便恢復(fù)PC值。更新PC的值,使下一條執(zhí)行的指令地址指向 greeting 方法的指令開始部分。
這條語句會(huì)使當(dāng)前的 main 方法執(zhí)行暫停,使 JVM 進(jìn)入對(duì) greeting 方法的執(zhí)行當(dāng)中當(dāng) greeting 方法執(zhí)行完成后,才會(huì)恢復(fù) PC 程序計(jì)數(shù)器的值指向當(dāng)前下一條指令。
7、此時(shí),main 方法的棧幀被壓在下邊,work 方法的棧幀在棧頂開始執(zhí)行
8、以下截圖為 work 方法的執(zhí)行過程:

work 棧幀執(zhí)行時(shí)示意圖如下所示:

9、work-code-12: ireturn 將計(jì)算結(jié)果出棧并且壓入到該方法的調(diào)用的棧中也就是 main 的棧幀的操作數(shù)棧中,將 work 棧幀彈出
10、main-code-12: pop將棧頂數(shù)值彈出
11、main-code-13: return 方法執(zhí)行結(jié)束,main 棧幀彈出
-
還可以參考其他示例
本地方法棧
本地方法棧跟 Java 虛擬機(jī)棧的功能類似,Java 虛擬機(jī)棧用于管理 Java 函數(shù)的調(diào)用,而本地方法棧則用于管理本地方法的調(diào)用。但本地方法并不是用 Java 實(shí)現(xiàn)的,而是由 C 語言實(shí)現(xiàn)的。
堆
堆是 JVM 上最大的內(nèi)存區(qū)域,這塊區(qū)域該進(jìn)程的所有線程共享,我們申請(qǐng)的幾乎所有的對(duì)象和數(shù)組,都是在這里存儲(chǔ)的。我們常說的垃圾回收,操作的對(duì)象就是堆。堆空間一般是程序啟動(dòng)時(shí),就申請(qǐng)了,但是并不一定會(huì)全部使用。
那一個(gè)對(duì)象創(chuàng)建的時(shí)候,到底是在堆上分配,還是在棧上分配呢?這和兩個(gè)方面有關(guān):對(duì)象的類型和在 Java 類中存在的位置。
Java 的對(duì)象可以分為基本數(shù)據(jù)類型和普通對(duì)象。
對(duì)于普通對(duì)象來說,JVM 會(huì)首先在堆上創(chuàng)建對(duì)象,然后在其他地方使用的其實(shí)是它的引用。比如,把這個(gè)引用保存在虛擬機(jī)棧的局部變量表中。
對(duì)于基本數(shù)據(jù)類型來說(byte、short、int、long、float、double、char),有兩種情況。當(dāng)你在方法體內(nèi)聲明了基本數(shù)據(jù)類型的對(duì)象,它就會(huì)在棧上直接分配。其他情況,都是在堆上分配。
方法區(qū)
1、方法區(qū)的發(fā)展過程
方法區(qū)存放類信息、靜態(tài)變量、常量和即時(shí)編譯器編譯后的代碼等線程共享數(shù)據(jù)。
我個(gè)人在學(xué)習(xí) JVM 分區(qū)中感覺方法區(qū)是最不容易理解的,因?yàn)樾枰獎(jiǎng)討B(tài)的去觀察它的變化。
方法區(qū)經(jīng)常被稱作永久代和靜態(tài)存儲(chǔ)區(qū),其實(shí)HotSpot 虛擬機(jī)在 JDK8 以前使用永久代來實(shí)現(xiàn)方法區(qū),但在其它虛擬機(jī)中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一說。因此,方法區(qū)只是 JVM 中規(guī)范的一部分,可以說,在 HotSpot 虛擬機(jī)中,設(shè)計(jì)人員使用了永久代來實(shí)現(xiàn)了 JVM 規(guī)范的方法區(qū)。
在JDk8中,取消了永久代,用元空間代替之。也就是說,用元空間來實(shí)現(xiàn)方法區(qū)。
為什么要用元空間代替永久區(qū):
字符串存在永久代中,容易出現(xiàn)性能問題和內(nèi)存溢出
永久代大小不容易確定,PermSize 指定太小容易造成永久代OOM
永久代會(huì)為 GC 帶來不必要的復(fù)雜度,并且回收效率偏低
便于將 HotSpot 與 JRockit 合二為一 (JRockit 中并沒有永久代)
2、方法區(qū)的組成
方法區(qū)經(jīng)常和靜態(tài)變量、常量池等,同時(shí)出現(xiàn),而常量池,又有靜態(tài)常量池、動(dòng)態(tài)常量池、字符串常量池、class文件常量池等,而這些常量還可以分為字面量和符號(hào)引用兩類。而類信息是不是都保存在常量池中?這些概念很多,邏輯也比較亂,我們需要好好捋一捋。先分別看一下上邊的概念
常量是指用 final 修飾的成員變量,值一旦給定就無法改變
java 3 種變量及其存放位置分別為 靜態(tài)變量(獨(dú)立于方法之外的變量,用 static 修飾)存放在方法區(qū),實(shí)例變量(方法外部,無 static 修飾)作為對(duì)象的一部分,存放在堆中,局部變量(方法內(nèi)部變量)保存于棧中,棧隨線程的創(chuàng)建而被分配。
final 修飾的 3 種變量都會(huì)將字面量或者引用放置到方法區(qū)的靜態(tài)常量池中,此處能展開,如果修飾的是基本類型數(shù)據(jù),則方法區(qū)常量池存放的是字面量,但如果是對(duì)象引用或者 String,在執(zhí)行時(shí)會(huì)將字符引用替換成直接引用并且將其移至動(dòng)態(tài)常量池中。
字面量包括:文本字符串、final 修飾的常量、基本數(shù)據(jù)類型的值
符號(hào)引用包括:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符
字符串常量池中存放字符串的引用,字符串對(duì)象存放在堆中。此處也可以展開,字符串有兩種創(chuàng)建方式分別為:
String str=“abc”;和String str = new String(“abc”);這兩種是有區(qū)別的,前者會(huì)優(yōu)先去常量池中尋找,如果找到,直接返回,找不到才會(huì)去創(chuàng)建。后者在類加載時(shí)會(huì)先創(chuàng)建一個(gè) “abc” 的字符串創(chuàng)建在常量池中,在執(zhí)行到 new 才會(huì)調(diào)用 String 的構(gòu)造函數(shù),同時(shí) String 對(duì)象中的 char 數(shù)組將會(huì)引用常量池中字符串,在堆內(nèi)存中創(chuàng)建一個(gè) String 對(duì)象,最后,str 將引用 String 對(duì)象 。這個(gè)過程需要好好理解一下。class 文件常量池存放類的元數(shù)據(jù)比如,類的方法代碼、訪問權(quán)限、全限定名等。 class 對(duì)象是在堆中的。
3、總結(jié):
以上這些比較分散混亂,我們來對(duì)照類的加載過程簡(jiǎn)單做一下總結(jié),我們這里只關(guān)注在類的生命周期中,方法區(qū)中數(shù)據(jù)的變化,不過多討論類的生命周期。

加載:在加載之前,堆中的 classloader 其實(shí)已經(jīng)將 A 的元數(shù)據(jù)裝載到了方法區(qū),這時(shí)方法區(qū)已經(jīng)將類信息、字段信息、方法信息等放到了靜態(tài)常量池中,加載其實(shí)是對(duì)應(yīng)匯編指令invokespecial 的,此時(shí)應(yīng)該是執(zhí)行了 new A(); jvm 會(huì)根據(jù)方法區(qū)中的類的全限命名獲取該類的二進(jìn)制流,并且將此二進(jìn)制流靜態(tài)的存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),還會(huì)在堆中創(chuàng)建一個(gè) A.class 的對(duì)象作為此方法區(qū)中此類各種數(shù)據(jù)的訪問入口。
驗(yàn)證:此過程是驗(yàn)證二進(jìn)制流是否符合虛擬機(jī)的各項(xiàng)規(guī)范方法區(qū)的值并沒有變化
準(zhǔn)備:此過程 JVM 為類的靜態(tài)變量分配內(nèi)存并初始化為默認(rèn)值
解析:該階段把類在常量池中的符號(hào)引用轉(zhuǎn)為直接引用
初始化:為類的靜態(tài)變量賦予程序設(shè)定的初始值
其實(shí)在加載之后,方法區(qū)中的常量只有數(shù)據(jù)結(jié)構(gòu)的改變,從靜態(tài)常量變?yōu)檫\(yùn)行時(shí)常量。只有在類被卸載后(很難)方法區(qū)中的常量才會(huì)被回收(前提是方法區(qū)實(shí)現(xiàn)了垃圾回收),而靜態(tài)變量我們知道,它們的生命周期和進(jìn)程一致,只有程序停止運(yùn)行才會(huì)被回收。
詳細(xì)方法區(qū)的學(xué)習(xí)可以看這篇文章
直接內(nèi)存
不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域;如果使用了NIO,這塊區(qū)域會(huì)被頻繁使用,在java堆內(nèi)可以用directByteBuffer對(duì)象直接引用并操作。
這塊內(nèi)存不受 java 堆大小限制,但受本機(jī)總內(nèi)存的限制,可以通過-XX:MaxDirectMemorySize來設(shè)置(默認(rèn)與堆內(nèi)存最大值一樣),所以也會(huì)出現(xiàn)OOM異常。