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

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

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

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