1.問題
- 1、JAVA文本檔案如何被翻譯成CLASS二進制檔案?
- 2、如何了解CLASS檔案的組成結構?
- 3、虛拟機如何加載使用類檔案的生命周期?
- 4、虛拟機系列診斷工具如何使用?
- 5、虛拟機記憶體淘汰機制?
- 6、虛拟機指令集架構?
2.關鍵詞
編譯,魔數,常量池,字面量,資料表,堆棧,方法區,程式計數器,記憶體引用,記憶體溢出,垃圾回收器,新生區,永久區,指令集
3.全文概要
上一篇我們介紹了代碼如何被翻譯成機器級程式,然後逐條送到CPU執行。但是現代硬體的指令集架構千差萬别,不同機器上運作相同代碼往往會出現指令集相容問題。虛拟機在這個層面上把各種細節封裝好,提供通用的接口供上層應用調用。封裝好指令集架構的同時提供各種記憶體淘汰機制。本文将從宏觀及微觀角度來介紹類檔案結構、虛拟機加載類檔案機制,類檔案生命周期及位元組碼加載引擎,更加立體的加深對虛拟機工作的認識。
4.CLASS檔案結構分析
從我們學習JAVA語言的第一天起,就執行過JAVA/JAVAC指令。JAVAC就是把我們寫好的字尾為.java的文本檔案編譯成字尾為.class的位元組碼檔案。上一章我們介紹代碼本質的時候就了解到JAVA語言的文法元素。java檔案我們可以通過文本編輯器打開,裡面也是我們熟悉的java代碼,符合了java語言的文法規範。但是對于class裡面的内容,我們要陌生很多。上一章我們知道代碼通過編譯器翻譯成機器指令,那class檔案會不會也是java虛拟機翻譯成的指令呢?
其實當java檔案被編譯成class檔案後,就跟java語言沒什麼關系了。指令執行引擎是JVM虛拟機,其他程式設計語言,比如Scala,Python等都可以編譯成class檔案,然後放到JVM來執行。這麼說來,我們更加有必要探究class檔案的本質了。
4.1 CLASS檔案示例
我們先從微觀的角度來介紹class檔案的結構。先寫一個簡單的java文本檔案,然後編譯成class檔案,來觀察class的結構組成。
先定義一個接口檔案,Add.java檔案如下:
package com.lzh.jvm; public interface Add{ int add(int i,int j); }
再寫一個接口的實作類AddImpl.java,這個基本包含我們日常經常使用的檔案結構:
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的檔案目錄,然後在目前目錄先後編譯com/lzh/jvm/Add.java檔案和com/lzh/jvm/AddImpl.java檔案。得到了Add.class檔案和AddImpl.class檔案。
Add.java二進制檔案:
image
Add.class二進制檔案:
image
AddImpl.java二進制檔案:
image
AddImpl.class二進制檔案:
image
以上四個圖是用WinHex二進制編輯工具打開的,左邊是檔案的二進制編碼,右邊是ASCII标準編碼,是以隻能表示英式鍵盤上的字元,出現中文的話則顯示亂碼。為了閱讀友善,工具展示的是16進制的格式,兩個16進制的編碼表示一個位元組空間(8位)。直覺上我們可以看出來java檔案占用的存儲空間比class要少很多,這也符合我們上一章介紹的代碼翻譯過程。本質上計算機并不認識java檔案裡面的内容,java屬于進階語言,裡面的文法更為接近人類的語言,但是對于計算機來說全難以了解。是以需要把java檔案的内容翻譯成jvm認識的檔案格式。進階語言高度抽象了語言元素,翻譯為機器指令則要花費更多的“口舌”來指導計算機一步步執行代碼語句。下一節我們來解釋class檔案的結構,進而了解jvm如何了解執行class的内容。
4.2 class檔案結構說明
本節我們将以上圖給的AddImpl.class為例子來介紹類的結構。從結構上來看,class檔案隻存放兩種類型資料,分别為基礎字段和表。
- 基礎字段:用于描述數字,引用,數值或字元串的無符号數,類型為u1,u2,u4,u8表示占用位元組數
- 表:隻有一行的可變列數的表結構,每個字段可以是基礎字段或其他表的索引
4.2.1 魔數
用于判斷檔案類型,通常我們以檔案字尾來判别檔案類型,但是如果修改字尾就會導緻安全問題。class以4個位元組的空間作為開端,來标明class的類型,CA FE BA BE表示class類型的檔案。
4.2.2 版本數
魔數後面緊接着4個位元組表示jdk版本号。
- 次版本号:前兩個字段0x0000
- 主版本号:後兩個字段0x0035,轉換十進制為53,對應jdk1.9
4.2.3 常量池
常量池顧名思義是用于存放字元串常量,字元串常量包含:
- 字面量:字元串,常量
- 引用符合:類/接口全限定名,字段/方法名稱和修飾符
我們知道class本質是一些表的集合,同樣常量池也不例外,隻不過存放在常量池位置的表有特定的類型,共有11種類型,如下表(圖檔引用《深入了解Java虛拟機 JVM進階特性與最佳實踐 》):
image
每個表的表結構說明如下:
image
這11種類型的表第一個字段統一為标志字段tag,占用u1一個位元組,用于表示該表存放的資料類型。
首先進入常量池開始的兩個位元組(u2)表示的是常量池的長度,也就是表的個數。
我們可以看到例子中常量池個數為0x0017,轉換為十進制為23,由于第0個表為保留索引,表示沒引用到任何字元串,是以實際表的索引是從1開始計算,也就是1~23共22個表。
我們先觀察AddImpl.class常量池,分析第1張表的表結構。查表可知緊接着表個數後面的u1位置為0A,轉換為十進制為10,該表類型為CONSTANT_Methodref_info,觀察表結構可知接下來的兩個u2位置屬于該表的字段,這兩個字段都是表索引類型,0x0003表示引用第3個表,0x0013表示引用第19個表。
然後該表結束緊接着是第2張表第一個表,該表tag為07是CONSTANT_Class_info類型,第二個空間為u2的字段值為0x0014,引用第20個表。
接着分析第3張表,根據同樣的方法,一直可以把常量池的表結構分析完。常量池的作用就是把源代碼所有文本資料都集中在常量池這個區間位置内,裡面各個表之間互相引用,統一管理文本資料。由于表之間的引用,最後文本資料都是存放在CONSTANT_Class_info表裡面,而該表規定文本長度的字段length空間是u2類型,占用2個位元組,空間2的16次方,65536/1024=64K,是以java的變量或方法名大小不能超過64K。
4.2.4 通路标志
修飾類或接口的限定标志
在常量池結束後緊接着2個位元組的通路标志,共32個标志位。
4.2.5 類/父類/接口索引集合
類索引、父類索引與接口索引集合:指向常量池的CONSTANT_Class_info表,再由CONSTANT_Class_info表裡面的index指向特定CONSTANT_Utf8_info表的bytes字段的字面量。
4.5.6 字段表集合
字段表集合:
字段表結構如下
數組用 [ 表示,字段表用來表示類裡面所有變量(不包括方法裡面的局部變量)
4.5.7 方法表集合
方法表集合:
方法表結構如下
4.5.8 屬性表集合
屬性表集合
方法體裡面的内容編譯為Code屬性,code表結構如下
Code,Exceptions,LineNumberTable,LocalVariableTable,SourceFile,ConstantValue,InnerClasses,Deprecated,Synthetic
class檔案就像是一個産品的模具,把模具制造出來的過程就是把class加載到jvm記憶體的過程,然後jvm再照着class模具的樣子印出對象來。重點在于模具的設計,其實模具被生産出來也是需要它本身有一套模具。這就是class嚴格的結構規範,class檔案結構規範給出了各個方面的要求,隻有按照這個要求造出來的模具才是可用的,才可以被用來制造産品,不然連産品線都上不去,就如同jvm判斷class不符合規範而拒絕加載。
5.類檔案生命周期
類加載時機
類初始化的時機,大部分為被動初始化,用不到的時候都不會初始化。
類加載過程
- 加載:全限定名檢索二進制位元組流(不止class檔案)->讀取至方法區->在堆上生成class對應的對象
- 驗證:檔案格式驗證(符合class檔案規範)->中繼資料驗證(語義分析)->位元組碼驗證(方法體校驗)->符号引用驗證。可以用-Xverify:none來跳過類加載驗證
- 準備:類變量配置設定記憶體設定初值,并未進行指派操作
- 解析:針對類接口,字段,方法的符合引用進行解析比對。類解析,接口解析,字段解析,類方法解析,接口方法解析,
- 初始化:執行類構造器
()方法,按源碼順序執行所有static的語句。沒有靜态變量或者static語句的類将不會有()。
類加載器
啟動類加載器,擴充類加載器,應用程式類加載器
類加載器采用雙親委派機制來讀取類檔案,破壞雙親委派模型如:OSGI服務由自定義類加載器機制實作。每個OSGI子產品(Bundle)都有自己的加載器
6.虛拟機診斷工具
虛拟機性能監控與故障處理工具,給一個系統定位問題的時候,知識,經驗是基礎,資料是依據,工具就是處理資料的手段。
JDK的指令行工具
- 虛拟機程序狀況工具:jps -lvm
- 虛拟機統計資訊監視工具:jstat -gc pid interval count
- java配置資訊工具:jinfo -flag pid
- java記憶體映像工具:jmap -dump:format=b,file=java.bin pid
生成堆轉儲檔案
- 虛拟機堆轉儲快照分析工具:jhat file 分析堆轉儲檔案,通過浏覽器通路分析檔案
- java堆棧跟蹤工具:jstack [ option ] vmid
用于生成虛拟機目前時刻的線程快照threaddump或者Javacore
JDK的可視化工具
- jconsole
- jvisualvm
7.虛拟機記憶體淘汰機制
本節從宏觀的角度講解JVM記憶體結構、記憶體配置設定運作政策,垃圾回收機制。
7.1虛拟機記憶體分布
java記憶體區域與記憶體溢出
jvm記憶體區域:方法區,虛拟機棧,本地方法棧,堆,程式計數器;
- 程式計數器:位元組碼行号訓示器,每個線程需要一個程式計數器
- 虛拟機棧:方法執行時建立棧幀(存儲局部變量,操作棧,動态連結,方法出口)編譯時期就能确定占用空間大小,線程請求的棧深度超過jvm運作深度時抛StackOverflowError,當jvm棧無法申請到空閑記憶體時抛OutOfMemoryError,通過-Xss,-Xsx來配置初始記憶體
- 本地方法棧:執行本地方法,如作業系統api接口
- 堆:存放對象的空間,通過-Xmx,-Xms配置堆大小,當堆無法申請到記憶體時抛OutOfMemoryError
- 方法區:存儲類資料,常量,常量池,靜态變量,通過MaxPermSize參數配置
- 對象通路:初始化一個對象,其引用存放于棧幀,對象存放于堆記憶體,對象包含屬性資訊和該對象父類、接口等類型資料(該類型資料存儲在方法區空間,對象擁有類型資料的位址)
7.2記憶體回收算法
記憶體回收概述:
虛拟機棧、本地棧和程式計數器在編譯完畢後已經可以确定所需記憶體空間,程式執行完畢後也會自動釋放所有記憶體空間,是以不需要進行動态回收優化。
jvm記憶體調優主要針對堆和方法區兩大區域的記憶體。
引用:強Strong,軟sfot,弱weak,虛phantom,強引用不會回收,軟引用在記憶體達到溢出邊界時回收,弱引用在每次回收周期時回收,虛引用專門被标記為回收對象。
記憶體配置設定與回收政策
- 對象優先在Eden區配置設定:
- 新生對象回收政策Minor GC(頻繁)
- 老年代對象回收政策Full GC/Major GC(慢)
- 大對象直接進入老年代:
超過3m的對象直接進入老年區 -XX:PretenureSizeThreshold=3145728(3M)
- 長期存貨對象進入老年區:
Survivor區中的對象經曆一次Minor GC年齡增加一歲,超過15歲進入老年區
-XX:MaxTenuringThreshold=15
- 動态對象年齡判定:設定Survivor區對象占用一半空間以上的對象進入老年區
垃圾收集算法
标記-清除、複制、标記-整理、分代收集(新生用複制,老年用标記-整理)
7.3記憶體收集器
- serial收集器:單線程,主要用于client模式
- ParNew收集器:多線程版的serial,主要用于server模式
- Parallel Scavenge收集器:線程可控吞吐量(使用者代碼時間/使用者代碼時間+垃圾收集時間),自動調節吞吐量,使用者新生代記憶體區
- Serial Old收集器:老年版本serial
- Parallel Old收集器:老年版本Parallel Scavenge
- CMS(Concurrent Mark Sweep)收集器:停頓時間短,并發收集
- G1收集器:分塊标記整理,不産生碎片
8.虛拟機指令集架構(執行引擎)
8.1虛拟機位元組碼執行引擎
運作時棧幀結構
每個方法調用開始到執行完成的過程,對應這一個棧幀在虛拟機棧裡面從入棧到出棧的過程。
- 棧幀包含:局部變量表,操作數棧,動态連接配接,方法傳回
- 方法調用
方法調用不等于方法執行,而且确定調用方法的版本。
- 方法調用位元組碼指令:invokestatic,invokespecial,invokevirtual,invokeinterface
- 靜态分派:靜态類型,實際類型,編譯器重載時通過參數的靜态類型來确定方法的版本。(選方法)
- 動态分派:invokevirtual指令把類方法符号引用解析到不同直接引用上,來确定棧頂的實際對象(選對象)
- 單分派:靜态多分派,相同指令有多個方法版本。
- 多分派:動态單分派,方法接受者隻能确定唯一一個。
基于棧的位元組碼解釋
解釋執行:
基于棧指令集與基于寄存器的指令集:
基于本地解釋器執行過程
類加載 執行子系統案例
tomcat類加載,OSGI熱插拔,位元組碼生成技術,動态代理,Retrotranslator
9.虛拟機實作機制進化過程
程式編譯與代碼優化
早期編譯(編譯期)
- javac編譯器:解析與符号表填充,注解處理,生成位元組碼
- java文法糖:文法糖有助于代碼開發,但是編譯後就會解開糖衣,還原到基礎文法的class二進制檔案
重載要求方法具備不同的特征簽名(不包括傳回值),但是class檔案中,隻要描述不是完全一緻的方法就可以共存,如:
public String foo(List<String> arg){ final int var = 0; return ""; } public int foo(List<Integer> arg){ int var = 0; return 0; }
晚期編譯(運作期)
- HotSpot虛拟機内的即時編譯
解析模式 -Xint
編譯模式 -Xcomp
混合模式 Mixed mode
分層編譯:解釋執行 -> C1(Client Compiler)編譯 -> C2編譯(Server Compiler)
觸發條件:基于采樣的熱點探測,基于計數器的熱點探測
10.總結
由于JVM涉及内容較深且廣,篇幅有限無法深入分析細節。本文從微觀方面分析了作為原材料的CLASS檔案的結構,又從宏觀方面闡述了JVM是如何消化每一個進入的CLASS。JVM自定義了一套邏輯上的指令集,這也呼應了之前我們介紹的計算機如何運作一文,現代計算機性能有了長足的發展,但是本質上還是完備的諾依曼體系架構。随着量子計算的突飛猛進,相信未來的計算模型也會有革命性的突破。
轉載于:https://blog.51cto.com/13883927/2364980