概述
1. 基本概念:
JVM 是可運行 Java 代碼的假想計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收,堆 和一個存儲方法域。JVM 是運行在操作系統之上的,它與硬件沒有直接的交互。
2. 運行過程:
我們都知道 Java 源文件,通過編譯器,能夠生產相應的.Class 文件,也就是字節碼文件,而字節碼文件又通過 Java 虛擬機中的解釋器,編譯成特定機器上的機器碼 。 也就是如下:
-
- Java 源文件—->編譯器—->字節碼文件
- 字節碼文件—->JVM—->機器碼
每一種平台的解釋器是不同的,但是實現的虛擬機是相同的,這也就是 Java 為什么能夠跨平台的原因了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啟動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。
3. 線程:
- 這里所說的線程指程序執行過程中的一個線程實體。JVM 允許一個應用並發執行多個線程。
- Hotspot JVM 中的 Java 線程與原生操作系統線程有直接的映射關系。當線程本地存儲、緩沖區分配、同步對象、棧、程序計數器等准備好以后,就會創建一個操作系統原生線程。
- Java 線程結束,原生線程隨之被回收。操作系統負責調度所有線程,並把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的所有資源。
Hotspot JVM 后台運行的系統線程主要有下面幾個:
一、JVM啟動流程
- 通過java +xxx(或javaw)啟動java虛擬機
- 裝載配置,會在當前路徑中尋找jvm的config配置文件。
- 根據查找jvm.dll文件。這個文件就是java虛擬機的主要實現。
- 使用這個dll,初始化jvm虛擬機。獲得相關的接口。
- 找到main方法開始運行。
上面這個過程的描述雖然比較簡單,但是jvm的啟動流程基本都已經涵蓋在里面了。
二、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 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用於存儲哪種數據都必須符合規范上的要求,這樣才會被虛擬機認可、裝載和執行。
三、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區。
- 1:eden、servicorFrom 復制到 ServicorTo,年齡+1
2. 老年代:
- 主要存放應用程序中生命周期長的內存對象。
- 老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
- MajorGC 采用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生內存碎片,為了減少內存損耗,我們一般需要進行合並或者標記出來方便下次直接分配。當老年代也滿了裝不下的
時候,就會拋出 OOM(Out of Memory)異常。
3. 永久代:
- 指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被放入永久區域,它和和存放實例的區域不同,GC 不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常。
- 在 Java8 中,永久代已經被移除,被一個稱為“元數據區”(元空間)的區域所取代。元空間的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入 java 堆中,這樣可以加載多少類的元數據就不再由MaxPermSize 控制, 而由系統的實際可用空間來控制。
四、內存模型

