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)
- 類裝載器(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í)行。流程圖如下:
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流:
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;
}
}
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é)碼指令集
3.2 .class文件結(jié)構(gòu)
Java編譯器編譯成后綴為.class的文件,該類型的文件是由字節(jié)組成的文件抗悍,又叫字節(jié)碼文件驹饺。那么,class字節(jié)碼文件里面到底是有什么呢缴渊?它又是怎樣組織的呢赏壹?讓我們先來(lái)大概了解一下他的組成結(jié)構(gòu)吧。
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ò)程
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)系和加載順序如下圖所示:
4.2.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)類加載器將類信息加載到方法區(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)识补。
- 擴(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)有父類加載器的蝙斜。
- 應(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ì)嘗試自己加載祠挫。
- 自定義類加載器(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”。
一般情況下曹仗,雙親加載模型如下所示:
注意:雙親委派模型中的"雙親"并不是指它有兩個(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)存模型
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 棧幀
局部變量數(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)
線程間共享,用于存儲(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控制堆的大小春霍。如下圖所示:
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í)行技術(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的工作原理 √