程序員從宏觀(guān)、微觀(guān)角度淺析JVM虛擬機(jī)!
1.問(wèn)題
- JAVA文本文件如何被翻譯成CLASS二進(jìn)制文件?
- 如何理解CLASS文件的組成結(jié)構(gòu)?
- 虛擬機(jī)如何加載使用類(lèi)文件的生命周期?
- 虛擬機(jī)系列診斷工具如何使用?
- 虛擬機(jī)內(nèi)存淘汰機(jī)制?
- 虛擬機(jī)指令集架構(gòu)?
2.關(guān)鍵詞
編譯,魔數(shù),常量池,字面量,數(shù)據(jù)表,堆棧,方法區(qū),程序計(jì)數(shù)器,內(nèi)存引用,內(nèi)存溢出,垃圾回收器,新生區(qū),***區(qū),指令集
3.全文概要
本文將從宏觀(guān)及微觀(guān)角度來(lái)介紹類(lèi)文件結(jié)構(gòu)、虛擬機(jī)加載類(lèi)文件機(jī)制,類(lèi)文件生命周期及字節(jié)碼加載引擎,更加立體的加深對(duì)虛擬機(jī)工作的認(rèn)識(shí)。
4.CLASS文件結(jié)構(gòu)分析
從我們學(xué)習(xí)JAVA語(yǔ)言的***天起,就執(zhí)行過(guò)JAVA/JAVAC命令。JAVAC就是把我們寫(xiě)好的后綴為.java的文本文件編譯成后綴為.class的字節(jié)碼文件。上一章我們介紹代碼本質(zhì)的時(shí)候就了解到JAVA語(yǔ)言的語(yǔ)法元素。java文件我們可以通過(guò)文本編輯器打開(kāi),里面也是我們熟悉的java代碼,符合了java語(yǔ)言的語(yǔ)法規(guī)范。但是對(duì)于class里面的內(nèi)容,我們要陌生很多。上一章我們知道代碼通過(guò)編譯器翻譯成機(jī)器指令,那class文件會(huì)不會(huì)也是java虛擬機(jī)翻譯成的指令呢?
其實(shí)當(dāng)java文件被編譯成class文件后,就跟java語(yǔ)言沒(méi)什么關(guān)系了。指令執(zhí)行引擎是JVM虛擬機(jī),其他編程語(yǔ)言,比如Scala,Python等都可以編譯成class文件,然后放到JVM來(lái)執(zhí)行。這么說(shuō)來(lái),我們更加有必要探究class文件的本質(zhì)了。
4.1 CLASS文件示例
我們先從微觀(guān)的角度來(lái)介紹class文件的結(jié)構(gòu)。先寫(xiě)一個(gè)簡(jiǎn)單的java文本文件,然后編譯成class文件,來(lái)觀(guān)察class的結(jié)構(gòu)組成。
先定義一個(gè)接口文件,Add.java文件如下:
- package com.lzh.jvm;
- public interface Add{
- int add(int i,int j);
- }
- 再寫(xiě)一個(gè)接口的實(shí)現(xiàn)類(lèi)AddImpl.java,這個(gè)基本包含我們?nèi)粘=?jīng)常使用的文件結(jié)構(gòu):
- package com.lzh.jvm;
- public class AddImpl implements Add{
- public static final int TOP = 100;
- private String point;
- public int add(int i,int j){
- return i + j;
- }
- }
由于存在包名定義我們需要建好com/lzh/jvm的文件目錄,然后在當(dāng)前目錄先后編譯com/lzh/jvm/Add.java文件和com/lzh/jvm/AddImpl.java文件。得到了Add.class文件和AddImpl.class文件。
Add.java二進(jìn)制文件:

Add.class二進(jìn)制文件:

AddImpl.java二進(jìn)制文件:

AddImpl.class二進(jìn)制文件:

以上四個(gè)圖是用WinHex二進(jìn)制編輯工具打開(kāi)的,左邊是文件的二進(jìn)制編碼,右邊是ASCII標(biāo)準(zhǔn)編碼,所以只能表示英式鍵盤(pán)上的字符,出現(xiàn)中文的話(huà)則顯示亂碼。為了閱讀方便,工具展示的是16進(jìn)制的格式,兩個(gè)16進(jìn)制的編碼表示一個(gè)字節(jié)空間(8位)。
直觀(guān)上我們可以看出來(lái)java文件占用的存儲(chǔ)空間比class要少很多,這也符合我們上一章介紹的代碼翻譯過(guò)程。本質(zhì)上計(jì)算機(jī)并不認(rèn)識(shí)java文件里面的內(nèi)容,java屬于高級(jí)語(yǔ)言,里面的語(yǔ)法更為接近人類(lèi)的語(yǔ)言,但是對(duì)于計(jì)算機(jī)來(lái)說(shuō)全難以理解。所以需要把java文件的內(nèi)容翻譯成jvm認(rèn)識(shí)的文件格式。
高級(jí)語(yǔ)言高度抽象了語(yǔ)言元素,翻譯為機(jī)器指令則要花費(fèi)更多的“口舌”來(lái)指導(dǎo)計(jì)算機(jī)一步步執(zhí)行代碼語(yǔ)句。下一節(jié)我們來(lái)解釋class文件的結(jié)構(gòu),從而理解jvm如何理解執(zhí)行class的內(nèi)容。
4.2 class文件結(jié)構(gòu)說(shuō)明
本節(jié)我們將以上圖給的AddImpl.class為例子來(lái)介紹類(lèi)的結(jié)構(gòu)。從結(jié)構(gòu)上來(lái)看,class文件只存放兩種類(lèi)型數(shù)據(jù),分別為基礎(chǔ)字段和表。
- 基礎(chǔ)字段:用于描述數(shù)字,引用,數(shù)值或字符串的無(wú)符號(hào)數(shù),類(lèi)型為u1,u2,u4,u8表示占用字節(jié)數(shù)
- 表:只有一行的可變列數(shù)的表結(jié)構(gòu),每個(gè)字段可以是基礎(chǔ)字段或其他表的索引
4.2.1 魔數(shù)
用于判斷文件類(lèi)型,通常我們以文件后綴來(lái)判別文件類(lèi)型,但是如果修改后綴就會(huì)導(dǎo)致安全問(wèn)題。class以4個(gè)字節(jié)的空間作為開(kāi)端,來(lái)標(biāo)明class的類(lèi)型,CA FE BA BE表示class類(lèi)型的文件。
4.2.2 版本數(shù)
魔數(shù)后面緊接著4個(gè)字節(jié)表示jdk版本號(hào)。
- 次版本號(hào):前兩個(gè)字段0x0000
- 主版本號(hào):后兩個(gè)字段0x0035,轉(zhuǎn)換十進(jìn)制為53,對(duì)應(yīng)jdk1.9
4.2.3 常量池
常量池顧名思義是用于存放字符串常量,字符串常量包含:
- 字面量:字符串,常量
- 引用符合:類(lèi)/接口全限定名,字段/方法名稱(chēng)和修飾符
我們知道class本質(zhì)是一些表的集合,同樣常量池也不例外,只不過(guò)存放在常量池位置的表有特定的類(lèi)型,共有11種類(lèi)型,如下表(圖片引用《深入理解Java虛擬機(jī) JVM高級(jí)特性與***實(shí)踐 》):

每個(gè)表的表結(jié)構(gòu)說(shuō)明如下:

這11種類(lèi)型的表***個(gè)字段統(tǒng)一為標(biāo)志字段tag,占用u1一個(gè)字節(jié),用于表示該表存放的數(shù)據(jù)類(lèi)型。
首先進(jìn)入常量池開(kāi)始的兩個(gè)字節(jié)(u2)表示的是常量池的長(zhǎng)度,也就是表的個(gè)數(shù)。
我們可以看到例子中常量池個(gè)數(shù)為0x0017,轉(zhuǎn)換為十進(jìn)制為23,由于第0個(gè)表為保留索引,表示沒(méi)引用到任何字符串,所以實(shí)際表的索引是從1開(kāi)始計(jì)算,也就是1~23共22個(gè)表。
我們先觀(guān)察AddImpl.class常量池,分析第1張表的表結(jié)構(gòu)。查表可知緊接著表個(gè)數(shù)后面的u1位置為0A,轉(zhuǎn)換為十進(jìn)制為10,該表類(lèi)型為CONSTANT_Methodref_info,觀(guān)察表結(jié)構(gòu)可知接下來(lái)的兩個(gè)u2位置屬于該表的字段,這兩個(gè)字段都是表索引類(lèi)型,0x0003表示引用第3個(gè)表,0x0013表示引用第19個(gè)表。
然后該表結(jié)束緊接著是第2張表***個(gè)表,該表tag為07是CONSTANT_Class_info類(lèi)型,第二個(gè)空間為u2的字段值為0x0014,引用第20個(gè)表。
接著分析第3張表,根據(jù)同樣的方法,一直可以把常量池的表結(jié)構(gòu)分析完。常量池的作用就是把源代碼所有文本數(shù)據(jù)都集中在常量池這個(gè)區(qū)間位置內(nèi),里面各個(gè)表之間相互引用,統(tǒng)一管理文本數(shù)據(jù)。由于表之間的引用,***文本數(shù)據(jù)都是存放在CONSTANT_Class_info表里面,而該表規(guī)定文本長(zhǎng)度的字段length空間是u2類(lèi)型,占用2個(gè)字節(jié),空間2的16次方,65536/1024=64K,所以java的變量或方法名大小不能超過(guò)64K。
4.2.4 訪(fǎng)問(wèn)標(biāo)志
修飾類(lèi)或接口的限定標(biāo)志
在常量池結(jié)束后緊接著2個(gè)字節(jié)的訪(fǎng)問(wèn)標(biāo)志,共32個(gè)標(biāo)志位。
4.2.5 類(lèi)/父類(lèi)/接口索引集合
類(lèi)索引、父類(lèi)索引與接口索引集合:指向常量池的CONSTANT_Class_info表,再由CONSTANT_Class_info表里面的index指向特定CONSTANT_Utf8_info表的bytes字段的字面量。
4.5.6 字段表集合
字段表集合:
字段表結(jié)構(gòu)如下
數(shù)組用 [ 表示,字段表用來(lái)表示類(lèi)里面所有變量(不包括方法里面的局部變量)
4.5.7 方法表集合
方法表集合:
方法表結(jié)構(gòu)如下
4.5.8 屬性表集合
屬性表集合
方法體里面的內(nèi)容編譯為Code屬性,code表結(jié)構(gòu)如下
Code,Exceptions,LineNumberTable,LocalVariableTable,SourceFile,ConstantValue,InnerClasses,Deprecated,Synthetic
class文件就像是一個(gè)產(chǎn)品的模具,把模具制造出來(lái)的過(guò)程就是把class加載到j(luò)vm內(nèi)存的過(guò)程,然后jvm再照著class模具的樣子印出對(duì)象來(lái)。重點(diǎn)在于模具的設(shè)計(jì),其實(shí)模具被生產(chǎn)出來(lái)也是需要它本身有一套模具。這就是class嚴(yán)格的結(jié)構(gòu)規(guī)范,class文件結(jié)構(gòu)規(guī)范給出了各個(gè)方面的要求,只有按照這個(gè)要求造出來(lái)的模具才是可用的,才可以被用來(lái)制造產(chǎn)品,不然連產(chǎn)品線(xiàn)都上不去,就如同jvm判斷class不符合規(guī)范而拒絕加載。
5.類(lèi)文件生命周期
類(lèi)加載時(shí)機(jī)
類(lèi)初始化的時(shí)機(jī),大部分為被動(dòng)初始化,用不到的時(shí)候都不會(huì)初始化。
類(lèi)加載過(guò)程
- 加載:全限定名檢索二進(jìn)制字節(jié)流(不止class文件)->讀取至方法區(qū)->在堆上生成class對(duì)應(yīng)的對(duì)象
- 驗(yàn)證:文件格式驗(yàn)證(符合class文件規(guī)范)->元數(shù)據(jù)驗(yàn)證(語(yǔ)義分析)->字節(jié)碼驗(yàn)證(方法體校驗(yàn))->符號(hào)引用驗(yàn)證。可以用-Xverify:none來(lái)跳過(guò)類(lèi)加載驗(yàn)證
- 準(zhǔn)備:類(lèi)變量分配內(nèi)存設(shè)置初值,并未進(jìn)行賦值操作
- 解析:針對(duì)類(lèi)接口,字段,方法的符合引用進(jìn)行解析匹配。類(lèi)解析,接口解析,字段解析,類(lèi)方法解析,接口方法解析,
- 初始化:執(zhí)行類(lèi)構(gòu)造器
()方法,按源碼順序執(zhí)行所有static的語(yǔ)句。沒(méi)有靜態(tài)變量或者static語(yǔ)句的類(lèi)將不會(huì)有()。
類(lèi)加載器
啟動(dòng)類(lèi)加載器,擴(kuò)展類(lèi)加載器,應(yīng)用程序類(lèi)加載器
類(lèi)加載器采用雙親委派機(jī)制來(lái)讀取類(lèi)文件,破壞雙親委派模型如:OSGI服務(wù)由自定義類(lèi)加載器機(jī)制實(shí)現(xiàn)。每個(gè)OSGI模塊(Bundle)都有自己的加載器
6.虛擬機(jī)診斷工具
虛擬機(jī)性能監(jiān)控與故障處理工具,給一個(gè)系統(tǒng)定位問(wèn)題的時(shí)候,知識(shí),經(jīng)驗(yàn)是基礎(chǔ),數(shù)據(jù)是依據(jù),工具就是處理數(shù)據(jù)的手段。
JDK的命令行工具
- 虛擬機(jī)進(jìn)程狀況工具:jps -lvm
- 虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具:jstat -gc pid interval count
- java配置信息工具:jinfo -flag pid
- java內(nèi)存映像工具:jmap -dump:format=b,file=java.bin pid
生成堆轉(zhuǎn)儲(chǔ)文件
- 虛擬機(jī)堆轉(zhuǎn)儲(chǔ)快照分析工具:jhat file 分析堆轉(zhuǎn)儲(chǔ)文件,通過(guò)瀏覽器訪(fǎng)問(wèn)分析文件
- java堆棧跟蹤工具:jstack [ option ] vmid
用于生成虛擬機(jī)當(dāng)前時(shí)刻的線(xiàn)程快照threaddump或者Javacore
JDK的可視化工具
- jconsole
- jvisualvm
7.虛擬機(jī)內(nèi)存淘汰機(jī)制
本節(jié)從宏觀(guān)的角度講解JVM內(nèi)存結(jié)構(gòu)、內(nèi)存分配運(yùn)行策略,垃圾回收機(jī)制。
7.1虛擬機(jī)內(nèi)存分布
java內(nèi)存區(qū)域與內(nèi)存溢出
jvm內(nèi)存區(qū)域:方法區(qū),虛擬機(jī)棧,本地方法棧,堆,程序計(jì)數(shù)器;
- 程序計(jì)數(shù)器:字節(jié)碼行號(hào)指示器,每個(gè)線(xiàn)程需要一個(gè)程序計(jì)數(shù)器
- 虛擬機(jī)棧:方法執(zhí)行時(shí)創(chuàng)建棧幀(存儲(chǔ)局部變量,操作棧,動(dòng)態(tài)鏈接,方法出口)編譯時(shí)期就能確定占用空間大小,線(xiàn)程請(qǐng)求的棧深度超過(guò)jvm運(yùn)行深度時(shí)拋StackOverflowError,當(dāng)jvm棧無(wú)法申請(qǐng)到空閑內(nèi)存時(shí)拋OutOfMemoryError,通過(guò)-Xss,-Xsx來(lái)配置初始內(nèi)存
- 本地方法棧:執(zhí)行本地方法,如操作系統(tǒng)api接口
- 堆:存放對(duì)象的空間,通過(guò)-Xmx,-Xms配置堆大小,當(dāng)堆無(wú)法申請(qǐng)到內(nèi)存時(shí)拋OutOfMemoryError
- 方法區(qū):存儲(chǔ)類(lèi)數(shù)據(jù),常量,常量池,靜態(tài)變量,通過(guò)MaxPermSize參數(shù)配置
- 對(duì)象訪(fǎng)問(wèn):初始化一個(gè)對(duì)象,其引用存放于棧幀,對(duì)象存放于堆內(nèi)存,對(duì)象包含屬性信息和該對(duì)象父類(lèi)、接口等類(lèi)型數(shù)據(jù)(該類(lèi)型數(shù)據(jù)存儲(chǔ)在方法區(qū)空間,對(duì)象擁有類(lèi)型數(shù)據(jù)的地址)
7.2內(nèi)存回收算法
內(nèi)存回收概述:
虛擬機(jī)棧、本地棧和程序計(jì)數(shù)器在編譯完畢后已經(jīng)可以確定所需內(nèi)存空間,程序執(zhí)行完畢后也會(huì)自動(dòng)釋放所有內(nèi)存空間,所以不需要進(jìn)行動(dòng)態(tài)回收優(yōu)化。
jvm內(nèi)存調(diào)優(yōu)主要針對(duì)堆和方法區(qū)兩大區(qū)域的內(nèi)存。
引用:強(qiáng)Strong,軟sfot,弱weak,虛phantom,強(qiáng)引用不會(huì)回收,軟引用在內(nèi)存達(dá)到溢出邊界時(shí)回收,弱引用在每次回收周期時(shí)回收,虛引用專(zhuān)門(mén)被標(biāo)記為回收對(duì)象。
內(nèi)存分配與回收策略
- 對(duì)象優(yōu)先在Eden區(qū)分配:
- 新生對(duì)象回收策略Minor GC(頻繁)
- 老年代對(duì)象回收策略Full GC/Major GC(慢)
- 大對(duì)象直接進(jìn)入老年代:
超過(guò)3m的對(duì)象直接進(jìn)入老年區(qū) -XX:PretenureSizeThreshold=3145728(3M)
- 長(zhǎng)期存貨對(duì)象進(jìn)入老年區(qū):
Survivor區(qū)中的對(duì)象經(jīng)歷一次Minor GC年齡增加一歲,超過(guò)15歲進(jìn)入老年區(qū)
-XX:MaxTenuringThreshold=15
- 動(dòng)態(tài)對(duì)象年齡判定:設(shè)置Survivor區(qū)對(duì)象占用一半空間以上的對(duì)象進(jìn)入老年區(qū)
垃圾收集算法
標(biāo)記-清除、復(fù)制、標(biāo)記-整理、分代收集(新生用復(fù)制,老年用標(biāo)記-整理)
7.3內(nèi)存收集器
- serial收集器:?jiǎn)尉€(xiàn)程,主要用于client模式
- ParNew收集器:多線(xiàn)程版的serial,主要用于server模式
- Parallel Scavenge收集器:線(xiàn)程可控吞吐量(用戶(hù)代碼時(shí)間/用戶(hù)代碼時(shí)間+垃圾收集時(shí)間),自動(dòng)調(diào)節(jié)吞吐量,用戶(hù)新生代內(nèi)存區(qū)
- Serial Old收集器:老年版本serial
- Parallel Old收集器:老年版本Parallel Scavenge
- CMS(Concurrent Mark Sweep)收集器:停頓時(shí)間短,并發(fā)收集
- G1收集器:分塊標(biāo)記整理,不產(chǎn)生碎片
8.虛擬機(jī)指令集架構(gòu)(執(zhí)行引擎)
8.1虛擬機(jī)字節(jié)碼執(zhí)行引擎
運(yùn)行時(shí)棧幀結(jié)構(gòu)
每個(gè)方法調(diào)用開(kāi)始到執(zhí)行完成的過(guò)程,對(duì)應(yīng)這一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過(guò)程。
- 棧幀包含:局部變量表,操作數(shù)棧,動(dòng)態(tài)連接,方法返回
- 方法調(diào)用
方法調(diào)用不等于方法執(zhí)行,而且確定調(diào)用方法的版本。
- 方法調(diào)用字節(jié)碼指令:invokestatic,invokespecial,invokevirtual,invokeinterface
- 靜態(tài)分派:靜態(tài)類(lèi)型,實(shí)際類(lèi)型,編譯器重載時(shí)通過(guò)參數(shù)的靜態(tài)類(lèi)型來(lái)確定方法的版本。(選方法)
- 動(dòng)態(tài)分派:invokevirtual指令把類(lèi)方法符號(hào)引用解析到不同直接引用上,來(lái)確定棧頂?shù)膶?shí)際對(duì)象(選對(duì)象)
- 單分派:靜態(tài)多分派,相同指令有多個(gè)方法版本。
- 多分派:動(dòng)態(tài)單分派,方法接受者只能確定唯一一個(gè)。
基于棧的字節(jié)碼解釋
解釋執(zhí)行:
基于棧指令集與基于寄存器的指令集:
基于本地解釋器執(zhí)行過(guò)程
類(lèi)加載 執(zhí)行子系統(tǒng)案例
tomcat類(lèi)加載,OSGI熱插拔,字節(jié)碼生成技術(shù),動(dòng)態(tài)代理,Retrotranslator
9.虛擬機(jī)實(shí)現(xiàn)機(jī)制進(jìn)化過(guò)程
程序編譯與代碼優(yōu)化
早期編譯(編譯期)
- javac編譯器:解析與符號(hào)表填充,注解處理,生成字節(jié)碼
- java語(yǔ)法糖:語(yǔ)法糖有助于代碼開(kāi)發(fā),但是編譯后就會(huì)解開(kāi)糖衣,還原到基礎(chǔ)語(yǔ)法的class二進(jìn)制文件
重載要求方法具備不同的特征簽名(不包括返回值),但是class文件中,只要描述不是完全一致的方法就可以共存,如:
- public String foo(List<String> arg){
- final int var = 0;
- return "";
- }
- public int foo(List<Integer> arg){
- int var = 0;
- return 0;
- }
晚期編譯(運(yùn)行期)
- HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯
- 解析模式 -Xint
- 編譯模式 -Xcomp
- 混合模式 Mixed mode
- 分層編譯:解釋執(zhí)行 -> C1(Client Compiler)編譯 -> C2編譯(Server Compiler)
- 觸發(fā)條件:基于采樣的熱點(diǎn)探測(cè),基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
10.總結(jié)
由于JVM涉及內(nèi)容較深且廣,篇幅有限無(wú)法深入分析細(xì)節(jié)。本文從微觀(guān)方面分析了作為原材料的CLASS文件的結(jié)構(gòu),又從宏觀(guān)方面闡述了JVM是如何消化每一個(gè)進(jìn)入的CLASS。JVM自定義了一套邏輯上的指令集,這也呼應(yīng)了之前我們介紹的計(jì)算機(jī)如何運(yùn)行一文,現(xiàn)代計(jì)算機(jī)性能有了長(zhǎng)足的發(fā)展,但是本質(zhì)上還是完備的諾依曼體系架構(gòu)。隨著量子計(jì)算的突飛猛進(jìn),相信未來(lái)的計(jì)算模型也會(huì)有革命性的突破。