細説jvm(一)、jvm運行時的數據區域

語言: CN / TW / HK

開篇

從今天開始寫jvm,從這個系列文章,你將能學會jvm內存分佈、垃圾回收算法以及垃圾回收的細節、故障診斷的手段、jvm類加載細節以及字節碼和apm系統原理,以及涉及到一些零散的點例如反射原理等。這個系列主要是偏重於講問題排查,以及GC和後邊的字節碼的東西,所以雖然基礎的東西也會講,但是不會講的那麼細緻,我寫文章從來不是針對小白的,只是想學基礎或者嫌學東西累的人可以直接右上角點關閉。

這個系列的文章會比較多,我不會再把每一篇都寫得特別長,這樣我累你也累。但是為了讓你係統的學習,每個體系還是會盡可能的集中到一塊去講。

一如我以前的風格,廢話不多説,我們直接開始~

jvm在運行期間數據區域大體上有堆、棧、方法區以及程序計數器等,當然這是非常抽象的概括,每一個地方的細節都是非常多的,我們一個一個來看

1、 程序計數器(也叫PC寄存器)

這玩意是一個記錄着當前線程所執行的字節碼的行號指示器,換句話説,就是記錄着當前線程執行到了第幾行字節碼。這玩意有這麼幾個特點:

  1. 線程私有
  2. 佔用內存非常小,不會發生OutOfMemoryError
  3. 如果執行native方法,這裏這個數值就是空,即undifined

上面四點都很容易理解,我們一點一點來説下。第一點,jvm的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現,也就是説,在同一時刻一個處理器內核只會執行一條線程,處理器切換線程時並不會記錄上一個線程執行到哪個位置,所以為了線程切換後依然能恢復到原位,每條線程都需要有各自獨立的程序計數器。第二點,由於這裏只是存個數字或者是undifined,所以當然不會佔多少空間,更不會因為數字太長而發生存不下的現象,所以也沒有內存溢出的可能性。第三點,我們知道,java的native方法的大多是通過C實現並未編譯成需要執行的字節碼指令,所以執行的方法是native方法的話,這裏也就不需要存值了

2、棧

jvm棧是描述java方法執行時的線程內存模型,它是線程私有的,生命週期和線程相同。棧是由一個一個的棧幀組成,而棧幀主要又由四個部分組成,分別是局部變量表,操作數棧,動態鏈接,返回地址。棧的結構圖如圖:

我們還是來一個一個分別説下:

  1. 局部變量表

用於保存方法的參數和方法內部定義的局部變量,最小單位是變量槽(variable slot),每個變量槽可以存放一個基礎數據類型(除long和double),對象的引用和返回地址的數據。

  1. 操作數棧

方法執行的過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧和入棧操作(與 Java 棧中棧幀操作類似)。

  1. 動態鏈接

保存指向運行時常量池中該棧幀所屬方法的引用,以便支持方法調用過程中的動態鏈接。

  1. 返回地址

指的是當前方法被調用的地方,因為方法執行完了總得知道從哪裏繼續執行的嘛

上面的概念可能比較生澀,但是別擔心,我後邊還會回頭來説這些東西的,這節我們先混個臉熟
複製代碼

3、堆

堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。這個區域是用來存放對象實例的,幾乎所有對象實例都會在這裏分配內存。堆是Java垃圾收集器管理的主要區域(GC堆),垃圾收集器實現了對象的自動銷燬。Java堆可以細分為:新生代和老年代;再細緻一點的有Eden空間,From Survivor空間,To Survivor空間等。Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。可以通過-Xmx和-Xms控制。

4、方法區

這塊區域是用來存放被加載的類的信息,常量,靜態變量以及即時編譯後的代碼緩存等。這裏值得説的一個東西是方法區這個名詞其實是jvm的規範中出現的地方,1.8之前,方法區對應的實現是永久代,在1.8之後,hotspot已經用元空間取代了永久代,因為這樣做可以減少發生OOM的機率。

5、運行時常量池和字符串常量池

運行時常量池在1.7之前是被放在永久代中,1.8之後被放在堆內存中。這裏是用來存放基本類型的包裝類的緩存(Integer的-128到127),以及字符串的值。有的人可能還知道個class常量池,這裏需要強調的是這兩個不是一個東西,我們後邊也會説class常量池,所以這裏如果不明白也沒關係。

字符串常量池是用來存放已經創建過的字符串的。

由於後邊不會再專門詳解字符串常量池,所以本着負責任的態度,這裏把這玩意多説説。我們肯定是見過下面這段代碼的:

public static void main(String[] args){
        String str = "ABC";
        String str1 = "ABC";
        String str2 = new String("ABC");
        String str3 = null;

        System.out.println(str==str1);
        System.out.println(str==str2);
        str3 = str2.intern();
        System.out.println(str==str3);
   }
複製代碼

我也就不賣關子了,這裏的輸出結果見下圖:

我們來一點一點的解釋,第一句的str,在編譯的時候就在類的常量池中,代碼執行完這一句之後,“ABC”會被扔進字符串常量池,然後str指向常量池的“ABC”(你這裏關心執行完後會被扔進常量池就好了,類常量池後邊講),第二句這塊發現“ABC”在字符串常量池中已經有了,所以直接讓str1直接指向常量池中的“ABC”就行了,第三句由於用了new 指令,因此不會再去常量池中尋找,而是在堆內存上開闢一塊新的空間,去再次創建個“ABC”,第四句跳過。五六句就非常簡單了,第五句輸出true,第六句輸出false,第七句這裏你需要知道intern這個方法會把調用的字符串複製進常量池,並返回常量池的引用,但是如果常量池中已經有了同一個字符串,那麼就是直接返回字符串引用,因此第八句輸出了true。注意是複製,為了證明是複製進去而不是直接扔進去,我們這裏再看一段代碼:

public static void main(String[] args){
        String str2 = new String("ABC");
        String str3 = null;

        str3 = str2.intern();
        System.out.println(str2==str3);
}
複製代碼

輸出結果如下,符合預期,不明白的可以再理解理解上面的話。

6、直接內存

這個區域並非是jvm的一部分,但是其實也是很重要的一部分。在java有了NIO之後,可以直接操作堆外內存,這樣做的好處是避免了在java堆中和計算機native內存中來回複製數據,顯著提高了性能,但是副作用是使用不當的話也會導致OOM(因為它也是受物理內存的限制的)。