小白也能看懂的JVM內存區域

  • 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堆

  對於大多數Javac程序來說,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 開始使用元空間,以前永久代的所有字符串常量由堆內存進行管理,其他內容被放到了元空間,元空間直接在本地內存分配。

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

參考資料:

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

//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

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