深刻淺出 JVM - JVM 內部結構總結

目錄git

  • 運行時數據區域
  • 垃圾回收算法
  • 垃圾收集器

運行時數據區域

  • 線程私有區域
    • 程序計數器
    • Java 虛擬機棧
    • 本地方法棧
  • 線程共享區域
    • Java 堆
    • 方法區
    • 運行時常量池(屬於方法區的一部分)

線程私有區域

程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計算器來完成。github

因爲 Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。故爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,因此這塊內存區域是"線程私有"的區域。算法

Java 虛擬機棧

Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是 Java 方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。數組

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機棧執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。多線程


線程共享區域

Java 堆

對於大多數應用來講,Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動是建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。這一點在 Java 虛擬機規範中的描述是:全部對象的實例以及數組都要在堆上分配,可是隨着 JIT 編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部對象都分配在堆上也漸漸變得沒那麼"絕對"了。優化

方法區(非堆)

方法區(Method Area)與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然 Java 虛擬機規範把 Java 方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non Heap(非堆),目的應該是與 Java 堆區分開來。線程

方法區也被開發者成爲"永久代"(Permanent Generation)。3d

運行時常量池(屬於方法區的一部分)

Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。指針


垃圾回收算法

標記-清除(Mark-Sweep)算法

如同名字同樣,算法分爲"標記"和"清除"兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,它的標記過程其實在前一節講述對象標記斷定時已經介紹過了。之因此說它是最基礎的算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。cdn

它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

複製算法

爲了解決效率問題,複製(Copying)算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣就是每次只對其中一塊內存進行回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。

這種算法的代價是將內存縮小爲了原來的一半,代價很大。這種算法也在特殊場景中會有很大用處,好比回收新生代的時候,IBM 公司的專門研究代表,新生代的對象 98% 是"朝生夕滅"的,因此不須要按照 1:1 的比例來劃份內存區域,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor 空間。當回收時,將 Eden 空間和 Survivor 空間中還存活着的對象一次性地複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔使用過的 Survivor 空間。這裏確定有一個具體空間分配比例,HotSpot 虛擬機默認 Eden:Survivor 爲 8:1,也就是每次新生代中可用內存爲整個新生代的 90%(80%+10%),只有 10% 的內存會被"浪費"。固然,98% 的對象可回收只是通常場景下的數據,JVM 沒有辦法保證每次回收都只有很少於 10% 的對象存活,當 Survivor 空間不夠用時,須要依賴其它內存(這裏指老年代)進行分配擔保(Handle Promotion)。

內存的分配擔保就比如咱們如今使用支付寶裏面的花唄,若是咱們信譽很好,在 98% 的狀況下都能按時償還,因而支付寶會默認咱們會在下一月也能按時按量的償還咱們的預支,只須要有一個擔保人能保證若是我下次不能還款時,能夠幫助你還錢,那支付寶就認爲咱們預支花唄是沒有風險的。內存的分配擔保也同樣,若是另一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制進入老年代。具體怎麼分配擔保會在後續分析。

標記-整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。更關鍵的時,若是不想浪費 50% 的空間,就須要有額外的空間進行分配擔保,以應對使用的內存中全部對象都 100% 存活的極端狀況,因此在老年代通常不能直接選用複製算法。

根據老年代存活時間較長的特色,有人提出了另外一種"標記-整理"(Mark-Compact)的算法,標記過程仍然與"標記-清除"算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活對象想一端移動,而後直接清理掉邊界之外的內存。

分代收集算法

這種算法沒有什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的算法。

  • 新生代:複製算法

    由於在新生代中,每次垃圾回收時都發現有大批對象死去,只有少許存活,那就選用複製算法,只需付出少許存活對象的複製成本就能夠完成收集。

  • 老年代:標記-清理/標記-整理

    由於老年代對象存活率高、沒有額外空間對它進行分配擔保。

更多精彩原創內容請關注:JavaInterview,歡迎 star,支持鼓勵如下做者,萬分感謝。

相關文章
相關標籤/搜索