小白也能看懂的JVM記憶體區域

愛程式設計DE文兄發表於2020-10-23

前言

  最近在準備面試題刷到了JVM這塊,作為一個小白,鞏固知識點最好的方式就是親手寫出來並分享;相信我的理解,同樣是小白的你,一定有很大的幫助。不信,請你往下看!

JVM記憶體區域簡介

  如果有人問Java的記憶體區域或者執行時資料區域,說的就是JVM記憶體區域

  Java程式在執行的時候,Java虛擬機器所管理的記憶體是被劃分為若干個資料區域,注意這些資料區域不是固定死的,抽象得可以分成為JDK1.8前後的JVM記憶體區域,但是總體上差別不大。

一.JDK1.8前的JVM記憶體區域

  JVM記憶體區域從執行緒的角度可以分成:執行緒共享和執行緒私有;

    》執行緒共享的區域有:堆,方法區(永久代),直接記憶體(非執行時資料區域)

    》執行緒私有的區域有:程式計數器,虛擬機器棧,本地方法棧

  如果有人問JVM記憶體區域,一般講這5個就行:程式計數器,虛擬機器棧,本地方法棧,堆,方法區;注意直接記憶體並不是執行時資料區域的一部分

 

1.程式計算器

  程式計算器是一塊比較小的記憶體區域。大家應該知道執行緒在輪流切換執行時,執行緒執行到哪,那下一次拿到CPU使用權執行任務就從哪開始繼續執行,簡言之:從哪跌倒從哪爬起來;那是不是我們得知道它從哪跌倒的啊?沒錯,程式計算器的作用就在這了,你可以把它理解成是當前執行緒所執行的指令(位元組碼)的行號指示器,儲存的是程式當前執行的指令的地址(也可以說儲存下一條指令的所在儲存單元的地址)。

  執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體,生命週期和執行緒一致。

  如果執行緒正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則為空(Undefined)。由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位現象(OutOfMemory)的。

2.虛擬機器棧(Java棧)
  虛擬機器棧描述的是 Java 方法執行的記憶體模型。什麼意思呢?大家看培訓班的視訊的時候,機構老師應該會有提過一個術語:方法進棧。沒錯,這裡的棧就是虛擬機器棧,在執行java的方法的同時,會對應建立一個棧幀,方法進棧的“方法”明確的講就是棧幀,虛擬機器棧的組成部分也是棧幀;
  當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,執行緒當前執行的方法所對應的棧幀必定位於Java棧的頂部。看下圖:

   棧幀可以理解為一種儲存資料的基本資料結構,你只需要關心它儲存的是什麼即可,在上面的圖說的很清楚了:區域性變數表,操作棧等等

    》區域性變數表:區域性變數表是存放方法引數和區域性變數的區域;對於基本資料型別的變數,則直接儲存它的值,對於引用型別的變數,則存的是指向物件的引用。區域性變數表的大小在編譯期就可以確定其大小了,因此在程式執行期間區域性變數表的大小是不會改變的。

    》操作棧:是個初始狀態為空的桶式結構棧。在方法執行過程中, 會有各種指令(語句)往棧中寫入和提取資訊。JVM 的執行引擎是基於棧的執行引擎, 其中的棧指的就是操作棧。位元組碼指令集的定義都是基於棧型別的,棧的深度在方法元資訊的 stack 屬性中。

    》動態連結:指向當前方法所屬的類的執行時常量池的引用;這個不是必須有的,如果方法中有使用到方法所屬類中的常量,那這個動態連結就是這個常量在執行時常量池中的引用

    》方法返回地址:當一個方法執行完畢之後,要返回到之前呼叫它的地方,因此在棧幀中必須儲存一個方法返回地址。

3.本地方法棧

  很多人對本地方法棧和java棧給搞混了,由於java棧是為java方法所服務的,因此也被叫做方法棧,那混淆就來了;

  方法棧是虛擬機器棧,不是本地方法棧!

  方法棧是虛擬機器棧,不是本地方法棧!

  方法棧是虛擬機器棧,不是本地方法棧!重要事情說三遍!!

  ok,回到本地方法棧。本地方法棧其實和Java棧的作用和原理是很相似的。最大的區別在於Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的。

4.Java堆

  對於大多數Java程式來說,Java 堆是 Java 虛擬機器所管理的記憶體中最大的一塊,也是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項以及陣列都在這裡分配記憶體。

  堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。從GC的角度來看, 堆中還可以細分為:新生代和老年代;新生代再細緻一點還可以分為: Eden 空間、From Survivor 空間、To Survivor 空間。

  Java 堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可,當前主流的虛擬機器都是按照可擴充套件來實現的(通過 -Xmx 和 -Xms 控制)。如果在堆中沒有足夠的記憶體區完成例項分配,並且堆也無法再擴充套件時,將會丟擲 OutOfMemoryError 異常。

  來!我們順便在擴充下新生代和老年代的內容,這可是面試的熱點,先上圖:

   我們說過從GC的角度,堆記憶體還分成老年代和新生代。

  新生代:是用來存放新生或者建立不久的物件。一般佔據堆的 1/3 空間。如果頻繁建立物件,那麼新生代會頻繁地觸發GC進行垃圾回收,因此新生代又分為 Eden 區、 ServivorFrom、 ServivorTo 三個區。

    1.Eden 區:Java 新物件的出生點(如果新建立的物件佔用記憶體很大,則直接分配到老年代),當 Eden 區記憶體不夠的時候就會觸發 GC,對新生代區進行一次垃圾回收。

    2.ServivorFrom:上一次 GC 的倖存物件,但是它會作為本次 GC 的掃描目標

    3.ServivorTo:保留了一次 GC 過程中的倖存物件,簡單說就是GC在一次回收過程中未清除物件會被放在這個區域。

  GC在回收新生代的過程有3個階段:複製-->清空-->互換

    複製:首先,eden、servicorFrom區域存活的物件會先被複制到 ServicorTo,同時這些物件的年齡+1(如果有物件的年齡以及達到了老年的標準,則賦值到老年代區),如果出現 複製的過程中ServicorTo 不夠記憶體了,物件就放到老年區。

    清空:然後,清空 Eden 和 ServicorFrom 中的物件;

    互換:最後, ServicorTo 和 ServicorFrom的物件進行互換,這樣的話,原 ServicorTo 將成為下一次 GC要清除的ServicorFrom區

  老年代:主要存放應用程式中生命週期長的記憶體物件,說簡單點,就是很多次逃過GC的回收或者是因為物件記憶體過大的物件會被放到這裡;老年代的物件比較穩定,所以GC 不會頻繁執行。在進行 GC 前一般都是先進行了一次GC,使得有一些新生代的物件晉身入老年代,導致空間不夠用時才觸發GC。當沒有足夠大的連續空間分配給新建立的較大物件時也會提前觸發一次 GC 進行垃圾回收騰出空間。

5.方法區

  方法區(Method Area)也是各個執行緒共享的記憶體區域,位元組碼檔案被類載入器載入到JVM的第一塊區域就是我們的方法區,它用於儲存類資訊、常量、靜態變數等。

  Java 虛擬機器規範對方法區的限制非常寬鬆,除了和 Java 堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充套件外,還可以選擇不實現垃圾收集。垃圾收集行為在這個區域是比較少出現的,其記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。當方法區無法滿足記憶體分配需求時,將丟擲 OutOfMemoryError 異常。

  方法區還有一個特別重要的區域就是執行時常量池,我們要知道Class 檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於儲存編譯期生成的各種字面量和符號引用;在類載入並進入方法區後,常量池的內容會被放到執行時常量池中存放。

  執行時常量池相對於 Class 檔案常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只有編譯期才能產生,也就是說並不是先放到Class 檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern() 方法。既然執行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會丟擲 OutOfMemoryError 異常。

  最後,相信大家經常聽到方法區和永久代,元空間等來掛鉤甚至等同起來,如果要比喻他們的關係,方法區相當介面,永久代和元空間相當實現類,在JDK1.8前,HotSpot虛擬機器(HotSpot是Java虛擬機器的實現)以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分割槽域,從而不需要專門為這部分設計垃圾回收機制。

6.直接記憶體

  直接記憶體(Direct Memory)並不是虛擬機器執行時資料區的一部分,也不是 Java 虛擬機器規範中定義的記憶體區域,但它會被頻繁得使用;可以簡單得理解為JVM的外設記憶體。

二.JDK1.8的JVM記憶體區域

    》執行緒共享的區域有:堆,直接記憶體(非執行時資料區域),元空間

    》執行緒私有的區域有:程式計數器,虛擬機器棧,本地方法棧

  到JDK1.8其實和之前差別不大,唯一的不同在於JDK1.8 前,Hotspot 中方法區的實現是永久代,使用的是堆記憶體來儲存物件例項;JDK1.8 開始使用元空間,以前永久代的所有字串常量由堆記憶體進行管理,其他內容被放到了元空間,元空間直接在本地記憶體分配。

  以上是個人的一些理解,如果大家覺得哪裡不妥,歡迎指正!

參考資料:

  https://www.cnblogs.com/dolphin0520/p/3613043.html

https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md

  https://www.cnblogs.com/czwbig/p/11127124.html

相關文章