分析JVM的組成結構

一、JavaSE體系

  • JavaSE,Java 平臺標準版,為 Java EE 和 Java ME 提供瞭基礎。
  • JDK:Java 開發工具包,JDK 是 JRE 的超集,包含 JRE 中的所有內容,以及開發程序所需的編譯器和調試程序等工具。
  • JRE:Java SE 運行時環境 ,提供庫、Java 虛擬機和其他組件來運行用 Java 編程語言編寫的程序。主要類庫,包括:程序部署發佈、用戶界面工具類、繼承庫、其他基礎庫,語言和工具基礎庫。
  • JVM:Java 虛擬機,負責 JavaSE 平臺的硬件和操作系統無關性、編譯執行代碼(字節碼)和平臺安全性。

二、運行時數據區

  • 線程私有:程序計數器、虛擬機棧、本地方法棧。
  • 線程共享:堆、方法區。

三、程序計數器

3.1、什麼是程序計數器

程序計數器是一塊較小的內存空間,它的作用可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裡字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。 — 摘自《深入理解Java虛擬機》

3.2、程序計數器有什麼特點

  • 程序計數器會隨著線程的啟動而創建,各線程之間獨立存儲,互不影響。
  • 當前線程執行的字節碼的行號指示器。
  • 如果線程正在執行的是一個 Java 方法,則指明當前線程執行的代字節碼行數。
  • 如果正在執行的是 Natvie 方法(本地方法),這個計數器值則為空(Undefined)。
  • 占用較小的內存空間,此內存區域是唯一一個不會出現 OutOfMemoryError(內存溢出) 情況的區域。

3.3、用個例子來說明

請無視我文章中取得類名,為瞭方便實驗演示,命名怎麼快怎麼來。

public class Jvm1 {

    public int test(){
        int a = 100;
        int b = 200;
        return a + b;
    }
}

這樣一個類, javac Jvm1.java,編譯成Jvm1.class文件。

再使用 javap 反匯編工具javap -c Jvm1.class看下.class文件中數據格式。

這個就是前面提到的 當前線程執行的字節碼的行號,而程序計數器則記錄的這個數字。

  • 當然這也解釋瞭程序計數器不存在 OutOfMemoryError 的原因,因為它記錄的隻是數字,占用空間少。
  • 同時也解釋瞭為什麼執行的是一個 Java 方法時,則指明當前線程執行的代字節碼行數。
  • 而執行 native方法 時程序計數器為 Undefined,因為 native方法 是大多是通過C實現並未編譯成需要執行的字節碼指令,所以在計數器中當然是空。

四、虛擬機棧

  • 棧有什麼特點? 先進後出。
  • 虛擬機棧是每個線程私有的,線程在運行時,在執行每個方法的時候都會打包成一個 棧幀,存儲瞭 局部變量表,操作數據棧,動態鏈接,方法出口等信息,然後放入棧。每個時刻正在執行的當前方法就是虛擬機棧頂的棧楨。方法的執行就對應著棧幀在虛擬機棧中入棧和出棧的過程。
  • 棧的大小缺省為 1M,可用參數 –Xss 調整大小,例如-Xss256k。

一個例子來看看執行 每個方法入棧出棧 的過程。

public class Jvm2 {

    public static void main(String[] args) {
        A();
    }

    public static void A() {
        System.out.println("A開始");
        // 此處省略100行代碼
        B(); // 調用B方法
        System.out.println("A結束");
    }

    public static void B() {
        System.out.println("B開始");
        // 此處省略100行代碼
        C(); // 調用B方法
        System.out.println("B結束");
    }

    public static void C() {
        System.out.println("C開始");
        // 此處省略100行代碼
        System.out.println("C結束");
    }
}

輸出:

A開始

B開始

C開始

C結束

B結束

A結束

4.1、局部變量表

  • 顧名思義就是局部變量的表,用於存放我們的局部變量的。
  • 主要存放我們的 Java 的八大基礎數據類型,如果是局部的一些對象,比如我們的 Object 對象,我們隻需要存放它的一個引用地址即可。(基本數據類型、對象引用、returnAddress 類型)。

4.2、操作數據棧

  • 存放我們方法執行的操作數的,它就是一個棧,先進後出的棧結構。
  • 操作數棧,就是用來操作的,操作的的元素可以是任意的 java 數據類型。
  • 所以我們知道一個方法剛剛開始的時候,這個方法的操作數棧就是空的,操作數棧運行方法是會一直運行入棧/出棧的操作。

數據重疊優化

虛擬機概念模型中每二個棧幀都是相互獨立的,但在實際應用是我們知道一個方法調用另一個方法時,往往存在參數傳遞,這種做法在虛擬機實現過程中會做一些優化,具體做法如下:令兩個棧幀出現一部分重疊。讓下面棧幀的一部分操作數棧與上面棧幀的部分局部變量表重疊在一起,進行方法調用時就可以共用一部分數據,無須進行額外的參數復制傳遞。

4.3、動態鏈接

需要類加載、運行時才能確定具體的方法。

棧幀中會持有一個引用(符號引用),該引用指向某個具體方法。

符號引用是一個地址位置的代號,在編譯的時候我們是不知道某個方法在運行的時候是放到哪裡的,這時我用代號 com/enjoy/pojo/User.Say:()V 指代某個類的方法,將來可以把符號引用轉換成直接引用進行真實的調用。用符號引用轉化成直接引用的解析時機,把解析分為兩大類:

  • 靜態解析:符號引用在類加載階段或者第一次使用的時候就直接轉換成直接引用。
  • 動態連接:符號引用在每次運行期間轉換為直接引用,即每次運行都重新轉換。

4.4、方法出口

1、正常返回(調用程序計數器中的地址作為返回)三步曲

  • 恢復上層方法的局部變量表和操作數棧
  • 把返回值(如果有的話)壓入調用者棧幀的操作數棧中
  • 調整 PC 計數器的值以指向方法調用指令後面的一條指令

2、異常返回

指方法執行過程中遇到異常,並且這個異常在方法體內部沒有得到處理,導致方法退出

4.5、棧溢出

  • java.lang.StackOverflowError:一般的方法調用是很難出現的,如果出現瞭要考慮是否有 無限遞歸 ,虛擬機棧帶給我們的啟示:方法的執行因為要打包成棧楨,所以天生要比實現同樣功能的循環慢,所以樹的遍歷算法中:遞歸和非遞歸(循環來實現)都有存在的意義。遞歸代碼簡潔,非遞歸代碼復雜但是速度較快。
  • OutOfMemoryError:不斷建立線程(一般演示不出,演示出來機器也死瞭)。

五、本地方法棧

  • 本地方法棧和虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的 Native 方法服務。
  • 虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。
  • 本地方法棧 native 方法通過 JNI 調用到瞭底層的 C/C++(c/c++可以觸發匯編語言,然後驅動硬件)。
  • 當一個JVM創建的線程調用native方法後,JVM不再為其在虛擬機棧中創建棧幀,JVM隻是簡單地動態鏈接並直接調用native方法。

六、方法區

主要存儲類信息、常量池、靜態變量、即時編譯期編譯後的代碼等數據。

永久代和元空間:

方法區在 jdk1.7 及其之前又背稱為永久代,jdk1.8 又被稱為元空間,怎麼理解呢?

1.jdk1.8移除瞭永久代,新增瞭元空間。

2.可以理解為方法區是一個規范,但是具體怎麼實現要看具體的jvm怎麼實現。

3.就類似於提供瞭一個接口方法(規范),隻要實現瞭這個接口的類,那麼就要去實現裡面接口方法(具體實現就是各種版本jvm之間和版本之間的差異瞭)。

4.各種版本jvm 。

  • HotSpot VM(SUN) 以前使用范圍最廣的Java虛擬機。
  • JRockit VM(BEA) 號稱世界上最快的JVM 。
  • Dalvik VM(Google) google自己開發的。
  • HotSPont VM(ORACLE) 目前以前使用范圍最廣的Java虛擬機。

5.版本差異(jdk1.7, jdk1.8) 。

參數設置:

  • jdk1.7 及以前:-XX:PermSize;-XX:MaxPermSize;
  • jdk1.8 以後:-XX:MetaspaceSize; -XX:MaxMetaspaceSize
  • jdk1.8 以後大小就隻受本機總內存的限制

七、堆

  • 幾乎所有對象都分配在堆內存,也是垃圾回收發生的主要區域。
  • 堆內存由多個線程共享。堆內存隨著JVM啟動而創建。

參數設置:

-Xms:堆的最小值

-Xmx:堆的最大值

-Xmn:新生代的大小

-XX:NewSize;新生代最小值

-XX:MaxNewSize:新生代最大值

八、運行時常量池

8.1、符號引用

  • 一個 java 類(假設為 People 類)被編譯成一個 class 文件時,如果 People 類引用瞭 Tool 類,但是在編譯時 People 類並不知道引用類的實際內存地址,因此隻能使用符號引用來代替。
  • 而在類裝載器裝載 People 類時,此時可以通過虛擬機獲取 Tool 類的實際內存地址,因此便可以既將符號 org.simple.Tool 替換為 Tool 類的實際內存地址,及直接引用地址。
  • 即在編譯時用符號引用來代替引用類,在加載時再通過虛擬機獲取該引用類的實際地址。
  • 以一組符號來描述所引用的目標,符號可以是任何形式的字面量,隻要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局是無關的,引用的目標不一定已經加載到內存中。

8.2、字面量

  • 文本字符串 String a = “abc”,這個 abc 就是字面量。
  • 八種基本類型 int a = 1; 這個 1 就是字面量。
  • 聲明為 final 的常量。

8.3、jvm各版本運行時常量池變化

  • 運行時常量池:Class 文件中的常量池(編譯器生成的各種字面量和符號引用)會在類加載後被放入這個區域。
  • JDK1.6:運行時常量池在方法區(永久代)中。
  • JDK1.7:運行時常量池在堆中。
  • JDK1.8:去永久代:使用元空間(空間大小隻受制於機器的內存)替代永久代。

8.4、直接內存

內存對象分配在JVM中堆以外的內存,也可以稱為直接內存,這些內存直接受操作系統管理(而不是JVM),這樣做的好處是能夠在一定程度上減少垃圾回收對應用程序造成的影響。

  • 使用 Native 函數庫直接分配堆外內存(NIO)。
  • 並不是 JVM 運行時數據區域的一部分,但是會被頻繁使用(可以通過-XX:MaxDirectMemorySize 來設置(默認與堆內存最大值一樣,也會出現 OOM 異常)。
  • 避免瞭在 Java 堆和 Native 堆中來回復制數據,能夠提高效率。

以上就是分析JVM的組成結構的詳細內容,更多關於JVM組成結構的資料請關註WalkonNet其它相關文章!

推薦閱讀: