這是我們 Java 虛擬機(jī)系列文章的第六篇,Java 虛擬機(jī)總述
Java 虛擬機(jī)子所以被稱(chēng)為”虛擬“的澜搅,就是因?yàn)樗鼉H僅是由一個(gè)規(guī)范來(lái)定義的抽象計(jì)算機(jī)。
1.Java虛擬機(jī)是什么
- 抽象規(guī)范
- 一個(gè)具體的實(shí)現(xiàn)
- 一個(gè)運(yùn)行中的虛擬機(jī)實(shí)例
2.虛擬機(jī)的生命周期
在 Java 虛擬機(jī)內(nèi)部有兩種線(xiàn)程: 守護(hù)線(xiàn)程 和 非守護(hù)線(xiàn)程
守護(hù)線(xiàn)程
由虛擬機(jī)自己使用的邪锌,例如執(zhí)行垃圾回收任務(wù)的線(xiàn)程
非守護(hù)線(xiàn)程
守護(hù)線(xiàn)程之外的其他線(xiàn)程都是非守護(hù)線(xiàn)程
只要還有非守護(hù)線(xiàn)程在運(yùn)行勉躺,虛擬機(jī)就不會(huì)自動(dòng)退出。
結(jié)束虛擬機(jī)的方式 有兩種
- System.exit() 方法退出
- 非守護(hù)線(xiàn)程執(zhí)行完
3.虛擬機(jī)的體系結(jié)構(gòu)
Java 虛擬機(jī)的內(nèi)部體系結(jié)構(gòu)圖
方法區(qū)和堆都是線(xiàn)程共享的
圖5-3 線(xiàn)程專(zhuān)有的運(yùn)行時(shí)數(shù)據(jù)區(qū)
當(dāng)一個(gè)線(xiàn)程被創(chuàng)建時(shí)秃流,它將得到一個(gè)屬于自己的 PC 寄存器(程序計(jì)數(shù)器)已經(jīng)一個(gè) Java 棧赂蕴。當(dāng)線(xiàn)程執(zhí)行一個(gè) Java 方法時(shí), PC 寄存器的值總是指示下一條將被執(zhí)行的指令舶胀, 它的 Java 棧存儲(chǔ)該線(xiàn)程中 Java 方法調(diào)用的狀態(tài)概说,包含它局部變量、被調(diào)用時(shí)傳進(jìn)來(lái)的參數(shù)嚣伐、它的返回值糖赔、以及運(yùn)算的中間結(jié)果。
Java 棧是由許多棧幀(stack frame)組成的轩端,一個(gè)棧幀包含一個(gè) Java 方法調(diào)用的狀態(tài)放典。當(dāng)線(xiàn)程調(diào)用一個(gè) Java 方法時(shí),虛擬機(jī)壓入一個(gè)新的棧幀到該線(xiàn)程的 Java 棧中基茵;當(dāng)方法返回時(shí)奋构,這個(gè)棧幀被從 Java 棧中彈出并拋棄。
Java 虛擬機(jī)沒(méi)有寄存器拱层,其指令集使用 Java 棧來(lái)存儲(chǔ)中間數(shù)據(jù)弥臼。這樣設(shè)計(jì)的原因是保持 Java 虛擬機(jī)的指令集盡量緊湊,同事也便于 Java 虛擬機(jī)在那些只有很少通用寄存器的平臺(tái)上實(shí)現(xiàn)根灯。
這樣基于棧的體系結(jié)構(gòu)径缅,也有助于某些虛擬機(jī)實(shí)現(xiàn)的動(dòng)態(tài)編譯和即時(shí)編譯器的代碼優(yōu)化。
3.1 數(shù)據(jù)類(lèi)型
數(shù)據(jù)類(lèi)型分為兩種:
- 基本類(lèi)型
- 引用類(lèi)型
- 類(lèi)類(lèi)型引用 -- 對(duì)類(lèi)實(shí)例的引用
- 接口類(lèi)型引用 -- 對(duì)實(shí)現(xiàn)了該接口的某個(gè)類(lèi)實(shí)例的引用
- 數(shù)組類(lèi)型引用 -- 對(duì)數(shù)組對(duì)象的引用
Java 語(yǔ)言中的所有基本類(lèi)型同樣也都是 Java 虛擬機(jī)中的基本類(lèi)型烙肺。
只是 boolean 類(lèi)型不一樣纳猪,編譯器吧 Java 源碼編譯為字節(jié)碼時(shí),它用 int 或者 byte 表示 boolean.
returnAddress 只是 Java 虛擬機(jī)中內(nèi)部使用的基本類(lèi)型
3.2 字的長(zhǎng)度
在 Java 虛擬機(jī)中桃笙,最基本的數(shù)據(jù)單元就是字(word), 它的大小是有每個(gè)虛擬機(jī)實(shí)現(xiàn)的設(shè)計(jì)者來(lái)決定的氏堤。
3.3 類(lèi)加載器子系統(tǒng)
負(fù)責(zé)查找并加載類(lèi)型的那部分被稱(chēng)為類(lèi)加載子系統(tǒng)。也就是我們所說(shuō)的類(lèi)加載器部分
Java 虛擬機(jī)有兩種類(lèi)加載器:?jiǎn)?dòng)類(lèi)加載器和用戶(hù)定義類(lèi)加載器怎栽。如果按更詳細(xì)的來(lái)分丽猬,就是啟動(dòng)類(lèi)加載器宿饱、應(yīng)用類(lèi)加載器、拓展類(lèi)加載器和用戶(hù)自定義類(lèi)加器脚祟。
類(lèi)加載器子系統(tǒng)要定位和導(dǎo)入二進(jìn)制 class 文件谬以,還必須負(fù)責(zé)驗(yàn)證被導(dǎo)入類(lèi)的正確性,為類(lèi)變量分配并初始化內(nèi)存由桌,以及幫助解析符號(hào)引用为黎。
執(zhí)行的順序:
- 裝載 -- 查找并加載類(lèi)型的二進(jìn)制數(shù)據(jù)
- 連接 -- 執(zhí)行驗(yàn)證,準(zhǔn)備行您,以及解析(可選)
- 驗(yàn)證铭乾,確保被導(dǎo)入類(lèi)型的正確性
- 準(zhǔn)備,為類(lèi)變量分配內(nèi)存娃循,并將其初始化為默認(rèn)值
- 解析炕檩,把類(lèi)型中的符號(hào)引用轉(zhuǎn)換為直接引用
3.初始化 -- 把類(lèi)變量初始化為正確初始值
命名空間
每個(gè)類(lèi)加載器都有自己的命名空間,其中維護(hù)著由它加載的類(lèi)型
3.4 方法區(qū)
方法區(qū)是存儲(chǔ)關(guān)于被加載類(lèi)型的信息
方法區(qū)是線(xiàn)程共享的捌斧,需要對(duì)方法區(qū)數(shù)據(jù)的訪(fǎng)問(wèn)設(shè)計(jì)為線(xiàn)程安全
方法區(qū)也被稱(chēng)為垃圾收集笛质,因?yàn)樘摂M機(jī)允許通過(guò)用戶(hù)定義的類(lèi)加載器來(lái)加載 Java 程序,當(dāng)一些類(lèi)不要被引用是捞蚂,Java 虛擬機(jī)可以卸載這些不再被引用的類(lèi)妇押,從而使方法區(qū)占據(jù)的內(nèi)存保存最小。
存儲(chǔ)的類(lèi)型信息:
這個(gè)類(lèi)型的全限定名
這個(gè)類(lèi)型的直接超類(lèi)的全限定名(除了 java.lang.Object姓迅, 它沒(méi)有超類(lèi))
這個(gè)類(lèi)型的訪(fǎng)問(wèn)修飾符(public , abstract 或 final 的某個(gè)子集)
任何直接超類(lèi)的接口的全限定名的有序列表
在 Java Class 文件和虛擬機(jī)匯總敲霍, 類(lèi)型名稱(chēng)是以全限定名出現(xiàn)的,例如 java/lang/Object這類(lèi)型的常量池
-
字段信息
包含:- 字段名
- 字段的類(lèi)型
- 字段的修飾符(public, private, protected, static, final, volatile, transient 的某個(gè)子集)
-
方法信息
包含:- 方法名
- 方法的返回類(lèi)型(或 void)
- 方法參數(shù)的數(shù)量和類(lèi)型(按聲明順序)
- 方法的修飾符(public, private, protected, static, fianl, synchronized, native, abstract 的某個(gè)子集)
- 方法的字節(jié)碼(bytecodes)
- 操作數(shù)棧和該方法的棧幀中局部變量區(qū)的大小
- 異常表
除了常量以為的所有類(lèi)(靜態(tài))變量
類(lèi)變量是由所有類(lèi)實(shí)例共享的丁存,即使沒(méi)有任何類(lèi)實(shí)例肩杈,它都可以被訪(fǎng)問(wèn)。
而編譯時(shí)常量(那些 final 聲明及用編譯時(shí)已知的值初始化的類(lèi)變量)會(huì)復(fù)制它的所有常量到自己的常量池中或嵌入到它的字節(jié)碼中解寝。一個(gè)到類(lèi) ClassLoader 的引用
一個(gè)到 Class 類(lèi)的引用
對(duì)于被加載的類(lèi)型(類(lèi)或接口)锋恬,虛擬機(jī)都會(huì)相應(yīng)地為它創(chuàng)建一個(gè) java.lang.Class 類(lèi)的實(shí)例,而且虛擬機(jī)以某種方式把這個(gè)實(shí)例和存在在方法區(qū)中的類(lèi)型數(shù)據(jù)關(guān)聯(lián)起來(lái)编丘。
在 java 程序中,可以得到并使用指向 Class 對(duì)象的引用彤悔。 Class 類(lèi)中的一個(gè)靜態(tài)方法可以讓用戶(hù)得到任何已加載的類(lèi)的 Class 實(shí)例的引用
3.5 堆 heap
Java 虛擬機(jī)實(shí)例中只有一個(gè)堆空間嘉抓,所有線(xiàn)程都將共享這個(gè)堆。 需要考慮多線(xiàn)程訪(fǎng)問(wèn)對(duì)象(堆數(shù)據(jù))的同步問(wèn)題
Java 程序在運(yùn)行時(shí)創(chuàng)建的所有類(lèi)型實(shí)例或數(shù)組都放在同一個(gè)堆中
Java 虛擬機(jī)有一條在堆中分配新對(duì)象的指令晕窑,卻沒(méi)有釋放內(nèi)存的指令抑片,所以釋放內(nèi)存的任務(wù)就交給了垃圾回收機(jī)器處理。
垃圾回收器的主要工作是自動(dòng)回收不再被運(yùn)行的程序引用的對(duì)象所占用的內(nèi)存杨赤。也可能去移動(dòng)那些還在使用的對(duì)象敞斋,以此減少堆碎片截汪。
堆空間可以是不連續(xù)的內(nèi)存空間,可以動(dòng)態(tài)擴(kuò)展或收縮植捎。
3.5.1 堆空間的設(shè)計(jì)
Java 虛擬機(jī)規(guī)范沒(méi)有規(guī)定 Java 對(duì)象在堆中是如何實(shí)現(xiàn)的衙解,它由虛擬機(jī)的實(shí)現(xiàn)者決定。
Java 對(duì)象中包含的基本數(shù)據(jù)由它所屬的類(lèi)及其超類(lèi)聲明的實(shí)例變量組成焰枢。虛擬機(jī)能夠通過(guò)對(duì)象實(shí)例的引用訪(fǎng)問(wèn)相應(yīng)的類(lèi)數(shù)據(jù)(存儲(chǔ)在方法區(qū)的類(lèi)型信息)蚓峦。因此對(duì)象中會(huì)有一個(gè)指向方法區(qū)的指針。
設(shè)計(jì)一:
把堆分成兩部分:句柄池 和 對(duì)象池
一個(gè)對(duì)象引用就是一個(gè)指向方法區(qū)句柄池的本地指針
句柄池包含兩部分:一個(gè)是指向?qū)ο蟪氐闹羔樇贸粋€(gè)指向方法區(qū)類(lèi)型數(shù)據(jù)的指針
優(yōu)點(diǎn):
有利于堆碎片的整理
當(dāng)移動(dòng)對(duì)象池中的對(duì)象時(shí)暑椰,句柄部分只需要更改一下指針指向?qū)ο蟮男碌刂肪涂梢粤?/p>
缺點(diǎn):
每次訪(fǎng)問(wèn)對(duì)象的實(shí)例變量都要經(jīng)過(guò)兩次指針傳遞。
設(shè)計(jì)二
使對(duì)象指針直接指向一組數(shù)據(jù)荐绝,改數(shù)據(jù)包括對(duì)象實(shí)例數(shù)據(jù)以及指向方法區(qū)中的數(shù)據(jù)的指針一汽。
優(yōu)點(diǎn)
訪(fǎng)問(wèn)對(duì)象實(shí)例比較快,只有一次指針傳遞
缺點(diǎn)
整理堆中實(shí)例對(duì)象時(shí)低滩,變得復(fù)雜
3.5.2 虛擬機(jī)必須通過(guò)引用得到類(lèi)數(shù)據(jù)的原因
當(dāng)程序在運(yùn)行時(shí)需要轉(zhuǎn)換某個(gè)對(duì)象引用為另一種類(lèi)型時(shí)召夹,虛擬機(jī)必須要檢查這種是否被允許,被轉(zhuǎn)換的對(duì)象是否的確是被引用的對(duì)象或者它的超類(lèi)型委造。(強(qiáng)轉(zhuǎn)的時(shí)候)
當(dāng)程序在執(zhí)行 instanceof 操作時(shí)戳鹅,虛擬機(jī)也進(jìn)行了同樣的檢查。
3.6 程序計(jì)數(shù)器
每個(gè)線(xiàn)程都有一個(gè)屬于自己的 PC (程序計(jì)數(shù)器)寄存器昏兆, 它在線(xiàn)程啟動(dòng)的時(shí)候被創(chuàng)建枫虏。
PC 寄存器的大小是一個(gè)字長(zhǎng),它可以持有一個(gè)本地指針或持有一個(gè) returnAddress.
當(dāng)線(xiàn)程在執(zhí)行某個(gè) java 方法時(shí)爬虱, PC 寄存器的內(nèi)容是下一條將要被執(zhí)行指令的 “地址”隶债,這個(gè)地址可以是一個(gè)本地指針,也可以是方法字節(jié)碼中相對(duì)于該方法指令的偏移量跑筝。
當(dāng)線(xiàn)程在執(zhí)行一個(gè)本地方法是死讹, PC 寄存器的值是 “undefined”
3.7 Java 棧
Java 棧也是每個(gè)線(xiàn)程獨(dú)有的, Java 棧上的數(shù)據(jù)都是此線(xiàn)程獨(dú)有的
Java 棧以幀的為單位保存線(xiàn)程的運(yùn)行狀態(tài)曲梗。
虛擬機(jī)直接對(duì) Java 棧執(zhí)行兩種操作:以幀為單位的壓棧和出棧赞警。
某個(gè)線(xiàn)程正在執(zhí)行的方法被稱(chēng)為該線(xiàn)程的當(dāng)前方法,當(dāng)前方法使用的棧幀稱(chēng)為當(dāng)前幀虏两,當(dāng)前方法所屬的類(lèi)稱(chēng)為當(dāng)前類(lèi)愧旦,當(dāng)前類(lèi)的常量池被稱(chēng)為當(dāng)前常量池。
每當(dāng)線(xiàn)程調(diào)用一個(gè) Java 方法時(shí)定罢,虛擬機(jī)會(huì)在該線(xiàn)程的 Java 棧中壓入一個(gè)新棧笤虫,這個(gè)新棧就是當(dāng)前幀。
Java 方法通過(guò)兩種方式完成返回。一是通過(guò)正常的 return 返回琼蚯,另外是拋出異常而終止酬凳。無(wú)論是哪一方式返回,都會(huì)彈出當(dāng)前幀釋放遭庶。
棧幀
棧幀由三部分組成:局部變量表(Local variables), 操作棧(Operand) 和 棧數(shù)據(jù)區(qū)
局部變量表
局部變量表是以字長(zhǎng)為單位宁仔,從 0 開(kāi)始計(jì)數(shù)的數(shù)組。
類(lèi)型 int, float, reference 和 returnAddress 的值在數(shù)組中只占一個(gè)字長(zhǎng)度罚拟;
long 和 double 占兩個(gè)字長(zhǎng)度台诗。在訪(fǎng)問(wèn)時(shí)只需要范圍第一個(gè)字的索引值即可。
byte, short, char 和 boolean 在局部變量表中用 int 赐俗。
public static int runClassMethod(int i, long l, float f, double d,
Object o, byte b){
return 0;
}
public int runInstanceMethod(char c, double d, short s, boolean b){
return 0;
}
注意上圖中拉队, runInstanceMethod 方法第一個(gè)變量是 reference ,指向 this ,代表對(duì)象
操作棧
操作棧也是以字為單位的數(shù)組阻逮。
它通過(guò)壓棧和出棧來(lái)訪(fǎng)問(wèn)粱快。
存儲(chǔ)時(shí) byte, short, char 和 boolean 用 int 表示
Java 虛擬機(jī)運(yùn)行方式是基于棧而不是寄存器的。它的指令是從操作棧中取得叔扼。
虛擬機(jī)把操作棧作為它的工作區(qū)事哭,大多數(shù)指令在操作棧中彈出數(shù)據(jù),執(zhí)行運(yùn)算瓜富,然后把結(jié)果壓回操作棧鳍咱。
幀數(shù)據(jù)區(qū)
幀數(shù)據(jù)區(qū)用來(lái)處理支持常量池解析、正常方法返回已經(jīng)異常派發(fā)機(jī)制的數(shù)據(jù)与柑。
3.8 本地方法接口
本地接口方法也就是 JNI
3.9 本地方法棧
本地方法棧是運(yùn)行本地方法的相關(guān)的數(shù)據(jù)區(qū)
3.10 執(zhí)行引擎
執(zhí)行引擎是 Java 虛擬機(jī)的核心谤辜。
執(zhí)行引擎可以理解為一個(gè)抽象的規(guī)范,一個(gè)具體的實(shí)現(xiàn)价捧,一個(gè)正在運(yùn)行的實(shí)例丑念。
運(yùn)行中的 Java 程序每一個(gè)線(xiàn)程都是一個(gè)獨(dú)立的虛擬機(jī)執(zhí)行器引擎的實(shí)例。
執(zhí)行引擎包含指令集结蟋,執(zhí)行技術(shù)脯倚,線(xiàn)程等方法的內(nèi)容構(gòu)成。
指令集
指令集是指 Java 虛擬機(jī)中的指令序列構(gòu)成的方法的字節(jié)碼流嵌屎。
執(zhí)行技術(shù)
執(zhí)行技術(shù)包含:解析推正、即時(shí)編譯,自適應(yīng)優(yōu)化
線(xiàn)程
線(xiàn)程模型也屬于執(zhí)行引擎的一部分宝惰。
至此舔稀,我們 Java 虛擬機(jī)系列文章基本完成了,后面會(huì)是垃圾回收機(jī)制的文章