阿里P7帶你深刻淺出JVM的運行機制

概述html

1. 基本概念:

JVM 是可運行 Java 代碼的假想計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收,堆 和一個存儲方法域。JVM 是運行在操做系統之上的,它與硬件沒有直接的交互。java

2. 運行過程:

咱們都知道 Java 源文件,經過編譯器,可以生產相應的.Class 文件,也就是字節碼文件,而字節碼文件又經過 Java 虛擬機中的解釋器,編譯成特定機器上的機器碼 。 也就是以下:mysql

Java 源文件—->編譯器—->字節碼文件
  
字節碼文件—->JVM—->機器碼

面試

每一種平臺的解釋器是不一樣的,可是實現的虛擬機是相同的,這也就是 Java 爲何可以跨平臺的緣由了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啓動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。算法

在這裏插入圖片描述
3. 線程:
spring

  • 這裏所說的線程指程序執行過程當中的一個線程實體。JVM 容許一個應用併發執行多個線程。
  • Hotspot JVM 中的 Java 線程與原生操做系統線程有直接的映射關係。當線程本地存儲、緩衝區分配、同步對象、棧、程序計數器等準備好之後,就會建立一個操做系統原生線程。
  • Java 線程結束,原生線程隨之被回收。操做系統負責調度全部線程,並把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的全部資源。

Hotspot JVM 後臺運行的系統線程主要有下面幾個:
在這裏插入圖片描述
sql

1、JVM啓動流程

在這裏插入圖片描述

  • 經過java +xxx(或javaw)啓動java虛擬機
  • 裝載配置,會在當前路徑中尋找jvm的config配置文件。
  • 根據查找jvm.dll文件。這個文件就是java虛擬機的主要實現。
  • 使用這個dll,初始化jvm虛擬機。得到相關的接口。
  • 找到main方法開始運行。
    上面這個過程的描述雖然比較簡單,可是jvm的啓動流程基本都已經涵蓋在裏面了。

2、JVM內存區域

JVM 內存區域主要分爲線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區域【JAVA 堆、方法區】、直接內存。數組

  • 線程私有數據區域生命週期與線程相同, 依賴用戶線程的啓動/結束 而 建立/銷燬(在 Hotspot VM 內, 每一個線程都與操做系統的本地線程直接映射, 所以這部份內存區域的存/否跟隨本地線程的生/死對應)。
  • 線程共享區域隨虛擬機的啓動/關閉而建立/銷燬。
  • 直接內存並非 JVM 運行時數據區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提供了基於 Channel 與 Buffer 的 IO 方式, 它可使用 Native 函數庫直接分配堆外內存, 而後使用DirectByteBuffer 對象做爲這塊內存的引用進行操做(詳見: Java I/O 擴展), 這樣就避免了在 Java堆和 Native 堆中來回複製數據, 所以在一些場景中能夠顯著提升性能。
    在這裏插入圖片描述

程序計數器(線程私有)

  • 一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每一個線程都要有一個獨立的程序計數器,這類內存也稱爲「線程私有」的內存。
  • 執行 java 方法時,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址);而執行Native 方法時,則爲空(undefined)。
  • 這個內存區域是惟一一個在虛擬機中沒有規定任何 OutOfMemoryError 狀況的區域。因此程序計數器不會發生內存溢出,由於其存儲的數據和所佔空間大小不會隨程序的執行而發生改變。

虛擬機棧(線程私有)

是描述 java 方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame),用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。由此可知,線程當前執行的方法所對應的棧幀一定位於Java棧的頂部,以及使用遞歸方法的時候容易致使棧內存溢出的現象。數據結構

  • 棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態連接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨着方法調用而建立,隨着方法結束而銷燬——不管方法是正常完成仍是異常完成(拋出了在方法內未被捕獲的異常)都算做方法結束。
  • 棧上分配:對於小對象(通常幾十個bytes),在沒有逃逸的狀況下,能夠直接分配在棧上(直接分配在棧上,能夠自動回收,減輕GC壓力);大對象或者逃逸對象沒法棧上分配

棧幀中數據的說明:併發

在這裏插入圖片描述
局部變量表:

  • 存儲方法中的局部變量(包括方法中的非靜態變量以及函數形參)

  • 對於基本數據類型的變量,直接存儲它的值;對於引用類型的變量,則存儲對象的地址。

  • 局部變量表的大小在編譯時就肯定了,所以在程序執行期間其大小是不會改變的。
    操做數棧:

  • 程序中的全部計算過程都是在藉助於操做數棧來完成的。

  • 棧最典型的一個應用就是用來對錶達式求值。
    指向運行時常量池的引用:

指向在方法執行的過程當中使用的常量。
方法返回地址:

當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。

本地方法棧(線程私有)
  本地方法區和 Java Stack 做用相似, 區別是虛擬機棧爲執行 Java 方法服務, 而本地方法棧則爲Native 方法服務, 若是一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那麼該棧將會是一個C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二爲一。

java堆(線程共享)

  • 是被線程共享的一塊內存區域,
  • 建立的對象和數組都保存在 Java 堆內存中,
    也是垃圾收集器進行垃圾收集的最重要的內存區域。
    因爲現代 VM 採用分代收集算法, 所以 Java 堆從 GC 的角度還能夠細分爲: 新生代(Eden區、From Survivor區和To Survivor區)和老年代。
    方法區/永久代(線程共享)


  • 即咱們常說的永久代(Permanent Generation), 用於存儲被 JVM 加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。HotSpot VM 把GC分代收集擴展至方法區, 即便用Java堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就能夠像管理 Java 堆同樣管理這部份內存, 而沒必要爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 所以收益通常很小)。
    運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池 (Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加
    載後存放到方法區的運行時常量池中。 Java 虛擬機對 Class 文件的每一部分(天然也包括常量池)的格式都有嚴格的規定,每個字節用於存儲哪一種數據都必須符合規範上的要求,這樣纔會被虛擬機承認、裝載和執行。

3、JVM運行時內存

Java 堆從 GC 的角度還能夠細分爲: 新生代(Eden 區、From Survivor區和To Survivor區)和老年代。

在這裏插入圖片描述
1. 新生代:

Eden區:Java 新對象的出生地(若是新建立的對象佔用內存很大,則直接分配到老年代)。當 Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。
Servivor From:上一次 GC 的倖存者,做爲這一次 GC 的被掃描者。

Servivor To:保留了一次 MinorGC 過程當中的倖存者。
MinorGC 的過程(複製->清空->互換):MinorGC 採用複製算法。

1:eden、servicorFrom 複製到 ServicorTo,年齡+1

首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(若是有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(若是 ServicorTo 不夠位置了就放到老年區);
  
2:清空eden、servicorFrom

而後,清空 Eden 和 ServicorFrom 中的對象;
  
3:ServicorTo和ServicorFrom互換

最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom區。
  
2. 老年代:

  • 主要存放應用程序中生命週期長的內存對象。
  • 老年代的對象比較穩定,因此 MajorGC 不會頻繁執行。在進行 MajorGC 前通常都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,致使空間不夠用時才觸發。當沒法找到足夠大的連續空間分配給新建立的較大對象時也會提早觸發一次 MajorGC 進行垃圾回收騰出空間。
  • MajorGC 採用標記清除算法:首先掃描一次全部老年代,標記出存活的對象,而後回收沒有標記的對象。MajorGC 的耗時比較長,由於要掃描再回收。MajorGC 會產生內存碎片,爲了減小內存損耗,咱們通常須要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。
    3. 永久代:
  • 指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被放入永久區域,它和和存放實例的區域不一樣,GC 不會在主程序運行期對永久區域進行清理。因此這也致使了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常。
  • 在 Java8 中,永久代已經被移除,被一個稱爲「元數據區」(元空間)的區域所取代。元空間的本質和永久代相似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入 java 堆中,這樣能夠加載多少類的元數據就再也不由MaxPermSize 控制, 而由系統的實際可用空間來控制。

4、內存模型

在這裏插入圖片描述

下圖爲多個線程執行的過程:

在這裏插入圖片描述
https://www.cnblogs.com/wslook/p/9126569.html

最新2020整理收集的一些高頻面試題(都整理成文檔),有不少乾貨,包含mysql,netty,spring,線程,spring cloud、jvm、源碼、算法等詳細講解,也有詳細的學習規劃圖,面試題整理等, 須要獲取這些內容的朋友請加Q君樣: 756584822

相關文章
相關標籤/搜索