程序員從宏觀、微觀角度淺析JVM虛擬機!

1.問題

  • 一、JAVA文本文件如何被翻譯成CLASS二進制文件?
  • 二、如何理解CLASS文件的組成結構?
  • 三、虛擬機如何加載使用類文件的生命週期?
  • 四、虛擬機系列診斷工具如何使用?
  • 五、虛擬機內存淘汰機制?
  • 六、虛擬機指令集架構?

2.關鍵詞

編譯,魔數,常量池,字面量,數據表,堆棧,方法區,程序計數器,內存引用,內存溢出,垃圾回收器,新生區,永久區,指令集java

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文件的本質了。api

4.1 CLASS文件示例

咱們先從微觀的角度來介紹class文件的結構。先寫一個簡單的java文本文件,而後編譯成class文件,來觀察class的結構組成。數組

先定義一個接口文件,Add.java文件以下:瀏覽器

package com.lzh.jvm; public interface Add{ int add(int i,int j); }tomcat

再寫一個接口的實現類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二進制文件:架構

Add.class二進制文件:

AddImpl.java二進制文件:

AddImpl.class二進制文件:

以上四個圖是用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高級特性與最佳實踐 》):

每一個表的表結構說明以下:

這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自定義了一套邏輯上的指令集,這也呼應了以前咱們介紹的計算機如何運行一文,現代計算機性能有了長足的發展,可是本質上仍是完備的諾依曼體系架構。隨着量子計算的日新月異,相信將來的計算模型也會有革命性的突破。

最後,若是有想一塊兒學習Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術的

能夠來一下個人QQ羣架構華山論劍:836442475【點擊進入】,好友都會在裏面交流,分享一些學習的方法和須要注意的小細節,天天準時講10年架構師分享經驗,Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術  

相關文章
相關標籤/搜索