Java虛擬機(jī)

1 虛擬機(jī)概述

1.1 虛擬機(jī)產(chǎn)生原由

JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫(xiě),JVM是一種用于計(jì)算設(shè)備的規(guī)范壮不,是一臺(tái)虛擬機(jī),即虛構(gòu)的計(jì)算機(jī)。

JVM屏蔽了與具體操作系統(tǒng)平臺(tái)相關(guān)的信息钮热,使Java程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼)校辩,就可以在多種平臺(tái)上不加修改地運(yùn)行窘问。JVM在執(zhí)行字節(jié)碼時(shí),實(shí)際上最終還是把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行宜咒。

1.2 JRE/JDK/JVM概念

  • JRE
    JRE(Java Runtime Environment惠赫,Java運(yùn)行環(huán)境),也就是Java平臺(tái)故黑。所有的Java程序都要在JRE下才能運(yùn)行儿咱。

  • JDK
    JDK(Java Development Kit,Java開(kāi)發(fā)工具包)场晶,是程序開(kāi)發(fā)者用來(lái)編譯混埠、調(diào)試java程序的開(kāi)發(fā)工具包。JDK的工具也是java程序诗轻,需要JRE環(huán)境的支持才能運(yùn)行钳宪。

  • JVM
    JVM(Java Virtual Machine,Java虛擬機(jī)),是JRE的一部分吏颖。它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī)搔体,在實(shí)際的計(jì)算機(jī)上模擬各種計(jì)算機(jī)的功能,其設(shè)計(jì)時(shí)有運(yùn)行時(shí)字節(jié)碼指令集侦高,運(yùn)行時(shí)數(shù)據(jù)區(qū)域嫉柴,執(zhí)行引擎,以及本地庫(kù)接口奉呛。

1.3 整體框架結(jié)構(gòu)

JVM框架結(jié)構(gòu).png
  • 類裝載器(Class Loader):用來(lái)裝載.class文件计螺;
  • 運(yùn)行時(shí)數(shù)據(jù)區(qū)域:方法區(qū),堆瞧壮,虛擬機(jī)棧登馒,程序計(jì)數(shù)器,本地方法棧咆槽;
  • 執(zhí)行引擎:執(zhí)行字節(jié)碼或本地方法陈轿;

2 Java代碼編譯

Java源代碼是不能被機(jī)器識(shí)別的,需要先經(jīng)過(guò)編譯器編譯成JVM可以執(zhí)行的.class字節(jié)碼文件秦忿,再由解釋器解釋運(yùn)行麦射。即:Java源文件(.java) -- Java編譯器 --> Java字節(jié)碼文件 (.class) -- Java解釋器 --> 執(zhí)行。流程圖如下:


Java編譯過(guò)程.png

2.1 詞法分析器

將源碼轉(zhuǎn)化為T(mén)oken流灯谣。讀取源代碼潜秋,從源文件的第一個(gè)字符開(kāi)始,按照java語(yǔ)法規(guī)范依次找出package胎许,import峻呛,類定義,屬性辜窑,方法定義等钩述,最后構(gòu)建出一個(gè)抽象語(yǔ)法樹(shù)乒融。

package compile;

/**
 * 詞法解析器
 */
 public class Cifa{
     int a;
     int c = a + 1;
 }

轉(zhuǎn)化為T(mén)oken流:


Token流.png

2.2 語(yǔ)法分析器

語(yǔ)法解析器要將Token流組建成更加結(jié)構(gòu)化的語(yǔ)法樹(shù)威蕉,也就是將這些Token流中的單詞裝成一句話,完整的語(yǔ)句揩尸。

package compile;

/**
 * 語(yǔ)法
 */
public class Yufa {
    int a;
    private int c = a + 1;
    
    //getter
    public int getC() {
        return c;
    }
    //setter
    public void setC(int c) {
        this.c = c;
    }
}
語(yǔ)法樹(shù).png

2.3 語(yǔ)義分析器

將語(yǔ)法樹(shù)轉(zhuǎn)化為注解語(yǔ)法樹(shù)所禀,即在這顆語(yǔ)法樹(shù)上做一些處理方面。
步驟

  • 給類添加默認(rèn)構(gòu)造函數(shù)(由com.sun.tools.javac.comp.Enter類完成)
  • 處理注解(由com.sun.tools.javac.processing.JavacProcessingEnvironment類完成)
  • 檢查語(yǔ)義的合法性并進(jìn)行邏輯判斷(由com.sun.tools.javac.comp.Attr完成)
    ① 變量的類型是否匹配;
    ② 變量在使用前是否初始化北秽;
    ③ 能夠推導(dǎo)出泛型方法的參數(shù)類型;
    ④ 字符串常量合并最筒;
  • 數(shù)據(jù)流分析(由com.sun.tools.javac.comp.Flow類完成)
    ① 檢驗(yàn)變量是否被正確賦值(eg.有返回值的方法必須確定有返回值)贺氓;
    ② 保證final變量不會(huì)被重復(fù)修飾;
    ③ 確定方法的返回值類型;
    ④ 所有的檢查型異常是否拋出或捕獲辙培;
    ⑤ 所有的語(yǔ)句都要被執(zhí)行到(return后邊的語(yǔ)句就不會(huì)被執(zhí)行到蔑水,除了finally塊兒);
  • 對(duì)語(yǔ)法樹(shù)進(jìn)行語(yǔ)義分析(由com.sun.tools.javac.comp.Flow執(zhí)行)
    ① 去掉無(wú)用的代碼扬蕊,如只有永假的if代碼塊搀别;
    ② 變量的自動(dòng)轉(zhuǎn)換,如將int自動(dòng)包裝為Integer類型尾抑;
    ③ 去除語(yǔ)法糖歇父,將foreach的形式轉(zhuǎn)化為更簡(jiǎn)單的for循環(huán);

2.4 代碼生成器

生成語(yǔ)法樹(shù)后再愈,接下來(lái)Javac會(huì)調(diào)用com.sun.tools.javac.jvm.Gen類遍歷語(yǔ)法樹(shù)榜苫,生成Java字節(jié)碼。
步驟

  • 將java方法中代碼塊轉(zhuǎn)化為符合JVM語(yǔ)法的命令形式翎冲;
  • 按照J(rèn)VM的文件組織格式將字節(jié)碼輸出到以class為拓展名的文件中垂睬;

3 字節(jié)碼

3.1 字節(jié)碼指令集

機(jī)器指令的格式.png

3.2 .class文件結(jié)構(gòu)

Java編譯器編譯成后綴為.class的文件,該類型的文件是由字節(jié)組成的文件抗悍,又叫字節(jié)碼文件驹饺。那么,class字節(jié)碼文件里面到底是有什么呢缴渊?它又是怎樣組織的呢赏壹?讓我們先來(lái)大概了解一下他的組成結(jié)構(gòu)吧。


class字節(jié)碼結(jié)構(gòu)示意圖.png
ClassFile {
u4              magic;
u2              minor_version;
u2              major_version;
u2              constant_pool_count;
cp_info         constant_pool[constant_pool_count-1]
u2              access_flages;
u2              this_class;
u2              super_class;
u2              interfaces_count;
u2              interfaces[interfaces_count];
u2              fields_count;
field_info      fields[fields_count];
u2              methods_count;
method_info     methods[methods_count];
u2              attributes_count;
attribute_info  attributes[attributes_count];
}

4 類加載

4.1 類加載過(guò)程

類加載過(guò)程.png
4.1.1 加載(Loading)
  • 何時(shí)預(yù)加載
    預(yù)加載:在虛擬機(jī)啟動(dòng)的時(shí)候加載疟暖,加載的是JAVA_HOME/lib/下的rt下的.class文件卡儒,是java程序運(yùn)行時(shí)經(jīng)常要用到的一些類,比如java.lang.?以及 java.util.?等俐巴。
    運(yùn)行時(shí)加載:虛擬機(jī)在用到一個(gè).class文件時(shí)骨望,首先會(huì)去內(nèi)存中查找這個(gè).class文件有沒(méi)有被加載,沒(méi)有被加載會(huì)根據(jù)這個(gè)類的全限定名去加載欣舵。

  • 加載階段主要做如下事項(xiàng):
    ① 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流擎鸠。 不一定要從本地的Class文件獲取,可以從jar包缘圈,網(wǎng)絡(luò)劣光,甚至十六進(jìn)制編輯器弄出來(lái)的。開(kāi)發(fā)人員可以重寫(xiě)類加載器的loadClass()方法糟把。
    ② 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)绢涡;
    ③ 在內(nèi)存中生成一個(gè)唯一代表此類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口(一般這個(gè)class對(duì)象會(huì)存儲(chǔ)在堆中遣疯,不過(guò)HotSpot虛擬機(jī)比較特殊雄可,這個(gè)Class對(duì)象是放在方法區(qū)中的);

說(shuō)明:類的數(shù)據(jù)流來(lái)源于
1> 從zip包中獲取,如jar数苫,ear聪舒,war格式等;
2> 從網(wǎng)絡(luò)上獲取虐急,典型應(yīng)用就是Applet箱残;
3> 運(yùn)行時(shí)計(jì)算生成,典型應(yīng)用就是動(dòng)態(tài)代理技術(shù)止吁;
4> 由其他文件生成被辑,典型應(yīng)用就是JSP,即由JSP生成對(duì)應(yīng)的.class文件赏殃;

4.1.2 認(rèn)證(Verifying)

這一階段目的為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求敷待,并且不會(huì)危害虛擬機(jī)自身的安全。
文件格式驗(yàn)證仁热,其實(shí)就是驗(yàn)證字節(jié)流是否符合Class文件規(guī)范榜揖,符合規(guī)范通過(guò)驗(yàn)證才能保證輸入的字節(jié)流能正確的被解析并存儲(chǔ)到方法區(qū)。如魔數(shù)(0xCAFEBABE)開(kāi)頭抗蠢、主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)等举哟。

元數(shù)據(jù)驗(yàn)證,此階段開(kāi)始就不是直接操作字節(jié)流迅矛,而是讀取方法區(qū)里的信息妨猩,元數(shù)據(jù)驗(yàn)證大概就是驗(yàn)證是否符合Java語(yǔ)言規(guī)范;

字節(jié)碼驗(yàn)證秽褒,是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段壶硅,主要目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是否合法销斟,符合邏輯庐椒。JDK6之后做了優(yōu)化,不再驗(yàn)證蚂踊,可以通過(guò)-XX:-UseSplitVerifier關(guān)閉優(yōu)化约谈。

符號(hào)引用驗(yàn)證,此階段可以看做是類自己身意外的信息進(jìn)行匹配性校驗(yàn)犁钟。如:符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類棱诱;符號(hào)引用中的類、字段涝动、方法的訪問(wèn)性(private迈勋、protected、public醋粟、default)是否可被當(dāng)前類訪問(wèn)靡菇。

4.1.3 準(zhǔn)備(Preparing)

為類變量分配內(nèi)存并賦初值的階段担败。注意這里僅包括類變量(被static修飾的變量),而不是包括實(shí)例變量镰官。
以下是數(shù)據(jù)類型的零值:

數(shù)據(jù)類型 零值
int 0
long 0L
short (short)0
char 'u0000'
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null
// 舉個(gè)例子:
public static int value = 123;

在這個(gè)階段中,value的值是0而不是123吗货,給value賦值為123的動(dòng)作將在初始化階段進(jìn)行泳唠。

4.1.4 解析(Resolving)

解析是虛擬機(jī)將常量池中的符號(hào)引用轉(zhuǎn)換為直接引用的過(guò)程。
符號(hào)引用:屬于編譯原理方面的概念宙搬,符號(hào)引用包括了下面三類常量:① 類和接口的全限定名笨腥; ② 字段的名稱和描述符; ③ 方法的名稱和描述符勇垛;
直接引用:直接指向目標(biāo)的指針脖母、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的闲孤,同一個(gè)符號(hào)引用在不同的虛擬機(jī)示例上翻譯出來(lái)的直接引用一般不會(huì)相同谆级。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)存在在內(nèi)存中了讼积。

4.1.5 初始化(Initializing)

在前面的類加載過(guò)程中肥照,除了在加載階段,用戶應(yīng)用程序可以通過(guò)自己定義類加載參與之外勤众,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制舆绎。到了這個(gè)初始化階段,才真正開(kāi)始執(zhí)行類中定義的Java程序代碼(或者說(shuō)是字節(jié)碼)们颜。

初始化過(guò)程就是執(zhí)行類構(gòu)造器<clinit>()的過(guò)程吕朵,就是將類變量賦予用戶指定的值。
虛擬機(jī)規(guī)范定義了“有且僅有”5種會(huì)觸發(fā)初始化的場(chǎng)景:
① 遇到new窥突、getstatic努溃、putstatic或invokestatic這四條字節(jié)碼指令時(shí),如果類沒(méi)有進(jìn)行初始化波岛,則要先觸發(fā)初始化茅坛;
② 使用java.lang.reflect包中的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候;
③ 初始化類時(shí)则拷,若發(fā)現(xiàn)其父類還沒(méi)有初始化贡蓖,則先觸發(fā)父類的初始化;
④ 虛擬機(jī)啟動(dòng)的時(shí)候煌茬,虛擬機(jī)會(huì)先初始化用戶指定的包含main()方法的那個(gè)類斥铺;
⑤ 當(dāng)使用JDK 1.7的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic坛善、REF_putStatic晾蜘、REF_invokeStatic的方法句柄邻眷,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化剔交。

4.2 類加載器

JVM的類加載通過(guò)ClassLoader及其子類來(lái)完成的肆饶,類的層次關(guān)系和加載順序如下圖所示:


類加載過(guò)程.png
4.2.1 類加載器種類
加載器種類.png
  1. 引導(dǎo)類加載器(Bootstrap ClassLoader)
    JVM的根ClassLoader,由C++實(shí)現(xiàn)岖常,JVM啟動(dòng)時(shí)即初始化此ClassLoader驯镊,用以加載JVM運(yùn)行時(shí)所需要的系統(tǒng)類,這些系統(tǒng)類在{JRE_HOME}/lib目錄下竭鞍。由于類加載器是使用平臺(tái)相關(guān)的底層C/C++語(yǔ)言實(shí)現(xiàn)的板惑, 所以該加載器不能被Java代碼訪問(wèn)到。但是偎快,我們可以查詢某個(gè)類是否被引導(dǎo)類加載器加載過(guò)冯乘。

一般而言,{JRE_HOME}/lib下存放著JVM正常工作所需要的系統(tǒng)類晒夹,如下表所示:

文件名 描述
rt.jar 運(yùn)行環(huán)境包裆馒,rt即runtime,J2SE 的類定義都在這個(gè)包內(nèi)
charsets.jar 字符集支持包
jce.jar 是一組包丐怯,它們提供用于加密领追、密鑰生成和協(xié)商以及 Message Authentication Code(MAC)算法的框架和實(shí)現(xiàn)
jsse.jar 安全套接字拓展包Java(TM) Secure Socket Extension
classlist 該文件內(nèi)表示是引導(dǎo)類加載器應(yīng)該加載的類的清單
net.properties JVM 網(wǎng)絡(luò)配置信息

引導(dǎo)類加載器(Bootstrap ClassLoader) 加載系統(tǒng)類后,JVM內(nèi)存會(huì)呈現(xiàn)如下格局:


引導(dǎo)類加載器過(guò)程.png
  • 引導(dǎo)類加載器將類信息加載到方法區(qū)中响逢,以特定方式組織绒窑,對(duì)于某一個(gè)特定的類而言,在方法區(qū)中它應(yīng)該有 運(yùn)行時(shí)常量池舔亭、類型信息些膨、字段信息、方法信息钦铺、類加載器的引用订雾,對(duì)應(yīng)class實(shí)例的引用等信息。
  • 類加載器的引用矛洞,由于這些類是由引導(dǎo)類加載器(Bootstrap Classloader)進(jìn)行加載的洼哎,而 引導(dǎo)類加載器是有C++語(yǔ)言實(shí)現(xiàn)的,所以是無(wú)法訪問(wèn)的沼本,故而該引用為NULL噩峦。
  • 對(duì)應(yīng)class實(shí)例的引用, 類加載器在加載類信息放到方法區(qū)中后抽兆,會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的Class 類型的實(shí)例放到堆(Heap)中, 作為開(kāi)發(fā)人員訪問(wèn)方法區(qū)中類定義的入口和切入點(diǎn)识补。
  1. 擴(kuò)展類加載器(Extension ClassLoader)
    該加載器是用于加載 java 的拓展類 ,拓展類一般會(huì)放在 {JRE_HOME}/lib/ext/ 目錄下辫红,或java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(kù)凭涂,用來(lái)提供除了系統(tǒng)類之外的額外功能祝辣。

拓展類加載器是是整個(gè)JVM加載器的Java代碼可以訪問(wèn)到的類加載器的最頂端,即是超級(jí)父加載器切油,拓展類加載器是沒(méi)有父類加載器的蝙斜。

  1. 應(yīng)用程序類加載器(Application ClassLoader)
    該類加載器是用于加載用戶代碼,是用戶代碼的入口澎胡。一般情況乍炉,如果我們沒(méi)有自定義類加載器默認(rèn)就是用這個(gè)加載器。

應(yīng)用類加載器將拓展類加載器當(dāng)成自己的父類加載器滤馍,當(dāng)其嘗試加載類的時(shí)候,首先嘗試讓其父加載器-拓展類加載器加載底循;如果拓展類加載器加載成功巢株,則直接返回加載結(jié)果Class<T> instance。加載失敗熙涤,則會(huì)詢問(wèn)是否引導(dǎo)類加載器已經(jīng)加載了該類阁苞;只有沒(méi)有加載的時(shí)候,應(yīng)用類加載器才會(huì)嘗試自己加載祠挫。

  1. 自定義類加載器(User ClassLoader)
    應(yīng)用程序自定義的加載器那槽,如tomcat,jboss都會(huì)實(shí)現(xiàn)自己的ClassLoader等舔;
4.2.2 雙親委派

JVM在加載類時(shí)默認(rèn)采用的是雙親委派機(jī)制骚灸。通俗的講,如果一個(gè)類加載器收到了類加載的請(qǐng)求慌植,它首先不會(huì)自己去加載這個(gè)類甚牲,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一層的類加載器都是如此蝶柿,這樣所有的加載請(qǐng)求都會(huì)被傳送到頂層的啟動(dòng)類加載器中丈钙。如果父類加載器可以完成類加載任務(wù),就成功返回交汤;如果父類加載器無(wú)法完成此加載任務(wù)時(shí)雏赦,才自己去加載。
① 委托父類加載器幫忙加載芙扎;
② 父類加載器加載不了星岗,則查詢引導(dǎo)類加載器有沒(méi)有加載過(guò)該類;
③ 如果引導(dǎo)類加載器沒(méi)有加載過(guò)該類戒洼,則當(dāng)前的類加載器應(yīng)該自己加載該類伍茄;
④ 若加載成功,返回 對(duì)應(yīng)的Class<T> 對(duì)象施逾;若失敗敷矫,拋出異忱瘢“ClassNotFoundException”。
一般情況下曹仗,雙親加載模型如下所示:


雙親加載模型.png

注意:雙親委派模型中的"雙親"并不是指它有兩個(gè)父類加載器的意思榨汤,一個(gè)類加載器只應(yīng)該有一個(gè)父加載器。上面的步驟中怎茫,有兩個(gè)角色:1)父類加載器(parent classloader):它可以替子加載器嘗試加載類收壕;2)引導(dǎo)類加載器(bootstrap classloader): 子類加載器只能判斷某個(gè)類是否被引導(dǎo)類加載器加載過(guò),而不能委托它加載某個(gè)類轨蛤;換句話說(shuō)蜜宪,就是子類加載器不能接觸到引導(dǎo)類加載器,引導(dǎo)類加載器對(duì)其他類加載器而言是透明的祥山。

JDK源碼看java.lang.ClassLoader的核心方法 loadClass()的實(shí)現(xiàn):

//提供class類的二進(jìn)制名稱表示圃验,加載對(duì)應(yīng)class,加載成功缝呕,則返回表示該類對(duì)應(yīng)的Class<T> instance 實(shí)例
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}


protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先澳窑,檢查是否已經(jīng)被當(dāng)前的類加載器記載過(guò)了,如果已經(jīng)被加載供常,直接返回對(duì)應(yīng)的Class<T>實(shí)例
        Class<?> c = findLoadedClass(name);
            //初次加載
            if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //如果有父類加載器摊聋,則先讓父類加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    // 沒(méi)有父加載器,則查看是否已經(jīng)被引導(dǎo)類加載器加載栈暇,有則直接返回
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 父加載器加載失敗麻裁,并且沒(méi)有被引導(dǎo)類加載器加載,則嘗試該類加載器自己嘗試加載
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 自己嘗試加載
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        //是否解析類 
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

相對(duì)應(yīng)地源祈,我們可以整理出雙親模型的工作流程圖:

4.2.3 舉例說(shuō)明

5 內(nèi)存模型

運(yùn)行時(shí)數(shù)據(jù)區(qū).png

5.1 Stack區(qū)

5.1.1 程序計(jì)數(shù)器(Program Counter)

線程私有悲立,指示當(dāng)前線程執(zhí)行到的字節(jié)碼行數(shù)。JVM多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的新博。在一個(gè)確定的時(shí)刻薪夕,一個(gè)處理器(或者說(shuō)多核處理器的一個(gè)內(nèi)核)只會(huì)執(zhí)行一條線程中的命令。因此赫悄,為了正常的切換線程原献,每個(gè)線程都會(huì)有一個(gè)獨(dú)立的PC,各線程的PC不會(huì)互相影響埂淮。

如果線程在執(zhí)行的是Java方法姑隅,那么PC記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。如果正在執(zhí)行的不是Java方法即Native方法倔撞,那么PC的值為undefined讲仰。

PC的內(nèi)存區(qū)域是唯一的沒(méi)有規(guī)定任何OutOfMemoryError的Java虛擬機(jī)規(guī)范中的區(qū)域。

5.1.2 Java虛擬機(jī)棧(Java VM Stack)

線程私有痪蝇,生命周期跟線程相同鄙陡。虛擬機(jī)棧描述Java方法執(zhí)行的內(nèi)存模型冕房,每個(gè)方法被執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),棧幀會(huì)利用局部變量數(shù)組存儲(chǔ)局部變量(Local Variables)趁矾,操作棧(Operand Stack)耙册,方法出口(Return Value),動(dòng)態(tài)連接(Current Class Constant Pool Reference)等信息毫捣。

5.1.2.1 棧幀
棧幀.png
  • 局部變量數(shù)組
    存儲(chǔ)了編譯可知的八個(gè)基本類型(int, boolean, char, short, byte, long, float, double)详拙,對(duì)象引用(根據(jù)不同的虛擬機(jī)實(shí)現(xiàn)可能是引用地址的指針或者一個(gè)handle),returnAddress類型蔓同。64位的long和double會(huì)占用兩個(gè)Slot饶辙,其余類型會(huì)占用一個(gè)Slot。在編譯期間斑粱,局部變量所需的空間就會(huì)完成分配弃揽,動(dòng)態(tài)運(yùn)行期間不會(huì)改變所需的空間。

  • 操作棧
    在執(zhí)行字節(jié)碼指令時(shí)會(huì)被用到珊佣,這種方式類似于原生的CPU寄存器,大部分JVM把時(shí)間花費(fèi)在操作棧的花費(fèi)上披粟,操作棧和局部變量數(shù)組會(huì)頻繁的交換數(shù)據(jù)咒锻。

  • 動(dòng)態(tài)連接
    控制著運(yùn)行時(shí)常量池和棧幀的連接。所有方法和類的引用都會(huì)被當(dāng)作符號(hào)的引用存在常量池中守屉。符號(hào)引用是實(shí)際上并不指向物理內(nèi)存地址的邏輯引用惑艇。
    JVM 可以選擇符號(hào)引用解析的時(shí)機(jī),一種是當(dāng)類文件加載并校驗(yàn)通過(guò)后拇泛,這種解析方式被稱為饑餓方式滨巴。另外一種是符號(hào)引用在第一次使用的時(shí)候被解析,這種解析方式稱為惰性方式俺叭。無(wú)論如何 恭取,JVM 必須要在第一次使用符號(hào)引用時(shí)完成解析并拋出可能發(fā)生的解析錯(cuò)誤。
    綁定是將對(duì)象域熄守、方法蜈垮、類的符號(hào)引用替換為直接引用的過(guò)程。綁定只會(huì)發(fā)生一次裕照,一旦綁定攒发,符號(hào)引用會(huì)被完全替換。如果一個(gè)類的符號(hào)引用還沒(méi)有被解析晋南,那么就會(huì)載入這個(gè)類惠猿。每個(gè)直接引用都被存儲(chǔ)為相對(duì)于存儲(chǔ)結(jié)構(gòu)(與運(yùn)行時(shí)變量或方法的位置相關(guān)聯(lián)的)偏移量。

對(duì)Java虛擬機(jī)棧這個(gè)區(qū)域负间,Java虛擬機(jī)規(guī)范規(guī)定了兩種異常:1)線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度偶妖,拋出StackOverFlow異常姜凄。2)對(duì)于支持動(dòng)態(tài)擴(kuò)展的虛擬機(jī),當(dāng)擴(kuò)展無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出OutOfMemory異常餐屎。

5.1.3 本地方法棧(Native Method Stack)

主要為Native方法服務(wù)檀葛,在JVM規(guī)范中,沒(méi)有對(duì)它的實(shí)現(xiàn)做具體規(guī)定腹缩。

5. 2 Non Heap區(qū)

5.2.1 代碼緩存(Code Cache)

用于編譯和存儲(chǔ)那些被 JIT 編譯器編譯成原生代碼的方法屿聋。

5.2.2 方法區(qū)(Method Area)
方法區(qū)內(nèi)容.png

線程間共享,用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息藏鹊、常量润讥、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)盘寡。它屬于非堆區(qū)(Non Heap)楚殿,和Java堆區(qū)分開(kāi)。

對(duì)于存在永久代(Permanent)概念的虛擬機(jī)(HotSpot)而言竿痰,方法區(qū)處于永久代脆粥。Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的規(guī)定很寬松,甚至可以不實(shí)現(xiàn)GC影涉。不過(guò)并非進(jìn)入方法區(qū)的數(shù)據(jù)就會(huì)永久存在了变隔,這塊區(qū)域的內(nèi)存回收主要為常量池的回收和類型的卸載。這個(gè)區(qū)域的回收處理不善也會(huì)導(dǎo)致嚴(yán)重的內(nèi)存泄漏蟹倾。當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí)也會(huì)拋出OutOfMemoryError匣缘。

注:運(yùn)行時(shí)常量池用于存放編譯期生成的各種字面量與符號(hào)引用,如String類型常量就存放在常量池鲜棠。

5.2.2.1 類信息(Class Data)

類信息存儲(chǔ)在方法區(qū)肌厨,其主要構(gòu)成為運(yùn)行時(shí)常量池(Run-Time Constant Pool)和方法(Method Code)。
一個(gè)編譯后的類文件包括以下結(jié)構(gòu):

結(jié)構(gòu) 解釋
magic, minor_version, major_version 類文件的版本信息和用于編譯這個(gè)類的 JDK 版本豁陆。
constant_pool 類似于符號(hào)表柑爸,盡管它包含更多數(shù)據(jù)。下面有更多的詳細(xì)描述盒音。
access_flags 提供這個(gè)類的描述符列表竖配。
this_class 提供這個(gè)類全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar里逆。
super_class 提供這個(gè)類的父類符號(hào)引用的常量池索引进胯。
interfaces 指向常量池的索引數(shù)組,提供那些被實(shí)現(xiàn)的接口的符號(hào)引用原押。
fields 提供每個(gè)字段完整描述的常量池索引數(shù)組胁镐。
methods 指向constant_pool的索引數(shù)組,用于表示每個(gè)方法簽名的完整描述。如果這個(gè)方法不是抽象方法也不是 native 方法盯漂,那么就會(huì)顯示這個(gè)函數(shù)的字節(jié)碼颇玷。
attributes 不同值的數(shù)組,表示這個(gè)類的附加信息就缆,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解帖渠。
  • 運(yùn)行時(shí)常量池(Run-Time Constant Pool)
    Class文件中有類的版本,字段竭宰,方法空郊,接口等描述信息和用于存放編譯期生成的各種字面量和符號(hào)引用。這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中切揭。Java虛擬機(jī)規(guī)范對(duì)Class的細(xì)節(jié)有著嚴(yán)苛的要求而對(duì)運(yùn)行時(shí)常量池的實(shí)現(xiàn)不做要求狞甚。一般來(lái)說(shuō)除了翻譯的Class,翻譯出來(lái)的直接引用也會(huì)存在運(yùn)行時(shí)常量池中廓旬。

運(yùn)行時(shí)常量池具備動(dòng)態(tài)性哼审,即運(yùn)行時(shí)也可將新的常量放入池中,比如String類的intern()方法孕豹。常量池?zé)o法申請(qǐng)到足夠的內(nèi)存分配時(shí)也會(huì)拋出OutOfMemoryError涩盾。

5.3 Heap區(qū)

5.3.1 堆(Heap)

線程間共享,用于存儲(chǔ)對(duì)象實(shí)例励背,可以通過(guò)-Xmx和-Xms控制堆的大小春霍。如下圖所示:


GC堆.png

Java堆是垃圾收集器管理的主要區(qū)域,因而也被稱為GC堆椅野。收集器采用分代回收法终畅,GC堆可以分為新生代(Yong Generation)和老生代(Old Generation)籍胯。新生代包括Eden Space和Survivor Space竟闪。

  • 新生代(Young)
    新生代主要存放新創(chuàng)建的對(duì)象,通痴壤牵可以劃分為Eden Space和Survivor Space炼蛤。Eden空間不足時(shí)會(huì)把存活的對(duì)象轉(zhuǎn)移到Survivor,可用-XX:SurvivorRatio控制Eden和Survivor的比例蝶涩。
  • 舊生代(Tenured)
    存放經(jīng)過(guò)多次垃圾回收后仍舊存活的對(duì)象理朋;

根據(jù)Java虛擬機(jī)規(guī)范,堆所在的物理內(nèi)存區(qū)間可以是不連續(xù)的绿聘,只要邏輯連續(xù)就可以嗽上。實(shí)現(xiàn)時(shí)既可以是固定大小,也可以是可擴(kuò)展的熄攘。如果堆無(wú)法擴(kuò)展時(shí)兽愤,就會(huì)拋出OutOfMemoryError。

6 執(zhí)行引擎機(jī)制

Java字節(jié)碼是由執(zhí)行引擎來(lái)完成,流程圖如下所示:


執(zhí)行引擎.png

主要的執(zhí)行技術(shù):解釋浅萧,即時(shí)編譯逐沙,自適應(yīng)優(yōu)化、芯片級(jí)直接執(zhí)行洼畅。

  • 解釋:屬于第一代JVM吩案;
  • 即時(shí)編譯:屬于第二代JVM;
  • 自適應(yīng)優(yōu)化:吸取第一代和第二代的經(jīng)驗(yàn)帝簇,采用兩者結(jié)合的方式徘郭;

7 垃圾回收

將內(nèi)存中不再被使用的對(duì)象進(jìn)行回收,GC中用于回收的方法稱為收集器己儒,由于GC需要消耗一些資源和時(shí)間崎岂,Java在對(duì)對(duì)象的生命周期特征進(jìn)行分析后,按照新生代闪湾、舊生代的方式來(lái)進(jìn)行對(duì)象的收集冲甘,以盡可能的縮短GC對(duì)應(yīng)用造成的暫停。

  • Minor GC:對(duì)新生代的對(duì)象收集途样;
  • Full GC:對(duì)舊生代的對(duì)象收集江醇;

不同的對(duì)象引用類型, GC會(huì)采用不同的方法進(jìn)行回收何暇,JVM對(duì)象的引用分為了四種類型:

  • 強(qiáng)引用:默認(rèn)情況下陶夜,對(duì)象采用的均為強(qiáng)引用(這個(gè)對(duì)象的實(shí)例沒(méi)有其他對(duì)象引用,GC時(shí)才會(huì)被回收)裆站。
  • 軟引用:軟引用是Java中提供的一種比較適合于緩存場(chǎng)景的應(yīng)用(只有在內(nèi)存不夠用的情況下才會(huì)被GC)条辟。
  • 弱引用:在GC時(shí)一定會(huì)被GC回收。
  • 虛引用:由于虛引用只是用來(lái)得知對(duì)象是否被GC宏胯。

參考資料

[1] 詳細(xì)介紹Java虛擬機(jī)(JVM)
[2] Java基礎(chǔ):Java虛擬機(jī)(JVM)
[3] Java 虛擬機(jī)底層原理知識(shí)總結(jié)
[4] 深入分析 Javac 編譯原理
[5] 《Java虛擬機(jī)原理圖解》 1.1羽嫡、class文件基本組織結(jié)構(gòu) ★★
[6] JVM 類加載機(jī)制及雙親委派模型
[7] 虛擬機(jī)類加載機(jī)制
[8] 理解虛擬機(jī)jvm的工作原理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肩袍,隨后出現(xiàn)的幾起案子杭棵,更是在濱河造成了極大的恐慌,老刑警劉巖氛赐,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件魂爪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡艰管,警方通過(guò)查閱死者的電腦和手機(jī)滓侍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)牲芋,“玉大人撩笆,你說(shuō)我怎么就攤上這事尔破。” “怎么了浇衬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵懒构,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我耘擂,道長(zhǎng)胆剧,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任醉冤,我火速辦了婚禮秩霍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蚁阳。我一直安慰自己铃绒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布螺捐。 她就那樣靜靜地躺著颠悬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪定血。 梳的紋絲不亂的頭發(fā)上赔癌,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音澜沟,去河邊找鬼灾票。 笑死,一個(gè)胖子當(dāng)著我的面吹牛茫虽,可吹牛的內(nèi)容都是我干的刊苍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼濒析,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼正什!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起悼枢,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤埠忘,失蹤者是張志新(化名)和其女友劉穎脾拆,沒(méi)想到半個(gè)月后馒索,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡名船,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年绰上,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渠驼。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜈块,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情百揭,我是刑警寧澤爽哎,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站器一,受9級(jí)特大地震影響课锌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜祈秕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一渺贤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧请毛,春花似錦志鞍、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至仙蚜,卻和暖如春玻孟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鳍征。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工黍翎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人艳丛。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓匣掸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親氮双。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碰酝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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