細說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(因為它也是受實體記憶體的限制的)。