Java 虛擬機–內存模型

一、JVM 內存模型

本節來分析 Java 對象如何進行分配回收

JVM 運行時數據區主要由線程私有區域線程共享區域組成。

  1. 線程私有區域:
  • 虛擬機棧
  • 本地方法棧
  • 程序計數器

2.線程共享區域:

  • 方法區

下面繪製一個草圖來描述 JVM 運行數據區的組成:

JVM 運行數據區

1.1、線程私有區域

線程私有區域組成爲:

  • 程序計數器
  • 虛擬機棧
  • 本地方法棧

1.1.1、程序計數器

什麼是程序計數器呢?

因爲 Java 本身就是一個多線程的,每一個線程都有一個程序計數器, CPU 在切換線程時,會使用程序計數器記錄下當前線程正在執行的字節碼指令的地址(行號),這樣線程再次回來工作時,就知道執行到哪個位置了。

爲了更加深入的理解程序計數器,下面來看這樣一段代碼:

demo

通過 javap -c -l MMDemo.class 得到對應字節碼:

程序計數器

這個 Code 對應的這些數就是程序計數器了。

1.1.2、虛擬機棧

虛擬棧屬於線程私有部分,在線程內部中一般會調用很多方法,而每一個方法使用一個棧幀來描述。

下面用一個草圖來描述一下棧幀虛擬機棧的關係:

虛擬機棧是由多個棧幀組成,每調用一個方法就相當於有一個棧幀入棧到虛擬機棧中。

棧幀

1.1.3、棧幀的組成

在前面描述過,在線程中,一個方法被調用就會一個棧幀被壓入虛擬機棧中。棧幀就是用來描述這個方法,一個棧幀是由局部變量表操作數棧返回值地址動態鏈接組成。

下面還是回到上面示例,結合草圖,看它們之間的關係:

虛擬機棧

局部變量表:

方法內部聲明的變量存放表

32位地址,尋址空間爲 4G 。如果需要存放64位的數據,需要使用高位和地位表示。

局部變量表

下面是 pay() 生成的局部變量表:

  • this 表示當前對象
  • i
  • obj

操作數棧:

對局部變量表中的變量進行出棧入棧的操作。

返回值地址:

一個方法被執行之後,有一個返回值,返回給對應的調用處。

動態鏈接:

主要對應的多態,只有代碼執行時才知道具體的實現類是那個對象。

1.1.4、StackOverflowError

這個異常想必很多人都遇過,字面意思就是棧溢出。我們通過上面的分析我們知道,虛擬機棧如果不斷出現棧幀入棧,當虛擬機棧空間達到上限,那麼就會出現 StackOverflowError

下面來模擬這個錯誤的產生:

public class StackOverflowError {
    private static int count = 0;

    public static void main(String[] args) {
        try {
            recursion();
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }

    public static void recursion() {
        count++;
        recursion();
    }
}

StackOverflowError

如果是死循環出現這樣的錯誤StackOverflowError,那麼通過 -Xss 參數的設置也是沒有用。

當然,如果是因爲虛擬機棧空間比較少到導致頻繁出現這個錯誤,那麼是可以合理的調節這個參數的。

例如設置參數:-Xss164K

1.1.5、本地方法棧

虛擬機棧對應的方法是 Java 方法,而本地方法棧對應的是 native 方法。其他方面應該和虛擬機棧差不多。

1.2、線程共享區域

1.2.1、方法區

方法區所存放的數據爲類信息,常量靜態變量,即時編譯後的代碼

方法區

在 JDK1.8 以下的 JVM 中,方法區就是對應的永久代,而 JDK1.8 之後方法區就變成了元空間(MetaSpace)

1.2.2、堆空間

在堆空間中主要存放的是通過 new 創建出來的對象或者數組。

##1.3、JVM 內存模型-堆

在 JVM 內存模型中,將線程共享部分分爲了堆空間和方法區,下面主要來看堆空間是如何進一步劃分的。

在下面這張草圖中,JVM 堆空間按照分代思想劃分爲新生代老年代兩部分,它們兩者空間分別爲堆空間的1/3和2/3。

JVM 內存模型

對於新生代這個區域又進一步進行劃分爲 Eden區from區to區這三個區域。

  • 對象或者數組的創建優先在 Eden 區分配內存空間。
  • 如果新創建的大對象在新生代放不下,那麼會直接移入老年代空間。
  • 在每一次進行 Minor GC 之後,Eden 區的垃圾對象就會被回收,並且存活的對象會進入 from區 或者 to區。在每次 Minor GC 之後存活的對象的年齡會累加,當多次 GC 之後,對象的年齡達到 15 ,那麼將進入老年代。
  • 如果 Eden 區存活的對象太大,放不進 from 或者 to 中,那麼將進入老年代。

注意:在新生代中發生的 GC 爲 Minor GC 而老年代發生的 GC 爲 Full GC, Full GC 比 Minor GC 的效率要低。

1.4、垃圾回收算法

GC 是如何判斷一個對象是否需要被回收呢?

在 JVM 中主要有兩種判斷方法:

  • 對象引用記數法

當一個對象被一個變量引用時,那麼引用計數累加。如果一個對象沒有被其他變量引用,那麼就是需要被 GC 回收的。但是這裏有一個弊端,那就是對象間相互引用導致無法被回收的問題。

  • 可達性分析算法

從一個 GCRoot 開始遍歷,當一個對象到 GCRoot 沒有直達路徑時,就標記爲不可達,是需要被 GC 回收的。

那什麼對象可以作爲 GCRoot 呢?

  • 局部變量表所引用的對象
  • 靜態變量/常量引用的對象
  • 本地方法引用的對象

JVM垃圾收集算法

  • 標記清除法

標記清除算法分爲兩個階段:

標記階段負責將將可回收的對象進行標記出來,清除階段負責將這些標記出來的對象進行回收。從下面的圖可以看出,這種算法會造成內存碎片。

標記清除算法

  • 複製算法:

它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則導致效率降低。

複製算法

  • 標記整理法:

複製收集算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

標記整理算法

  • 分代收集算法:

根據分代思想對堆區劃分位新生代和老年代,針對這個兩個區只用不同的回收算法。
新生代:使用複製算法
老年代:使用標記清理算法或者標記整理算法

總結

記錄於2019年3月27日

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章