一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

Java虛擬機器

1、JVM簡介

JVM 是可執行 Java 程式碼的假想計算機 ,包括一套位元組碼指令集、一組暫存器、一個棧、 一個垃圾回收,堆和 一個儲存方法域。JVM 是執行在作業系統之上的,它與硬體沒有直接的互動。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

我們都知道 Java 原始檔,透過編譯器,能夠生產相應的 。Class 檔案,也就是位元組碼檔案, 而位元組碼檔案又透過 Java 虛擬機器中的直譯器,編譯成特定機器上的機器碼 。 也就是如下:

① Java 原始檔 ——> 編譯器 ——> 位元組碼檔案

② 位元組碼檔案 ——> JVM ——> 機器碼

每一種平臺的直譯器是不同的,但是實現的虛擬機器是相同的,這也就是 Java 為什麼能夠跨平臺的原因了 ,當一個程式從開始執行,這時虛擬機器就開始例項化了,多個程式啟動就會存在多個虛擬機器例項。程式退出或者關閉,則虛擬機器例項消亡,多個虛擬機器例項之間資料不能共享。

2、JVM記憶體區域

按照功能劃分

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

按照記憶體是否共享劃分

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

JVM 記憶體區域主要分為執行緒私有區域【程式計數器、虛擬機器棧、本地方法區】、執行緒共享區域【JAVA 堆、方法區】、直接記憶體。

執行緒私有資料區域生命週期與執行緒相同,依賴使用者執行緒的 啟動/結束 而 建立/銷燬(在 Hotspot VM 內,每個執行緒都與作業系統的本地執行緒直接對映,因此這部分記憶體區域的 存/否 跟隨本地執行緒的 生/死 對應)。

執行緒共享區域隨虛擬機器的 啟動/關閉 而 建立/銷燬。

直接記憶體並不是 JVM 執行時資料區的一部分,但也會被頻繁的使用:在 JDK 1。4 引入的 NIO 題 供了基於 Channel 與 Buffer 的 IO 方式,它可以使用 Native 函式庫直接分配堆外記憶體,然後使用 DirectByteBuffer 物件作為這塊記憶體的引用進行操作(詳見: Java I/O 擴充套件), 這樣就避免了在 Java 堆和 Native 堆中來回複製資料,因此在一些場景中可以顯著提高效能。

2。1、程式計數器(執行緒私有)

一塊較小的記憶體空間,是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的程式計數器,這類記憶體也稱為

執行緒私有

的記憶體。 正在執行 java 方法的話,計數器記錄的是虛擬機器位元組碼指令的地址(當前指令的地址)。如果還是 Native 方法,則為空。 這個記憶體區域是唯一一個在虛擬機器中沒有規定任何 OutOfMemoryError 情況的區域。

2。2、虛擬機器棧(執行緒私有)

是描述 java 方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

棧幀(Frame)是用來儲存資料和部分過程結果的資料結構,同時也被用來處理動態連結(Dynamic Linking)、 方法返回值和異常分派(Dispatch Exception)。棧幀隨著方法呼叫而建立,隨著方法結束而銷燬——無論方法是正常完成還是異常完成(丟擲了在方法內未被捕獲的異常)都算作方法結束。

棧幀結構圖:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

2。3、本地方法區(執行緒私有)

本地方法區和 Java Stack 作用類似,區別是虛擬機器棧為執行 Java 方法服務,而本地方法棧則為Native方法服務, 如果一個 VM 實現使用 C-linkage 模型來支援 Native 呼叫,那麼該棧將會是一個 C 站,但 HotSpot VM 直接就把本地方法棧和虛擬機器棧合二為一。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

2。4、堆(Heap-執行緒共享)

執行時資料區,是被執行緒共享的一塊記憶體區域,建立的物件和陣列都儲存在 Java 堆記憶體中,也是垃圾收集器進行 垃圾收集的最重要的記憶體區域。

由於現代 VM 採用分代收集演算法,因此 Java 堆從 GC 的角度還可以細分為:新生代(Eden 區、SurvivorFrom 區和 SurvivorTo 區)和老年代。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

新生代

是用來存放新生的物件。一般佔據堆的 1/3 空間。由於頻繁建立物件,所以新生代會頻繁觸發 MinorGC 進行垃圾回收。新生代又分為 Eden 區、SurvivorFrom 、SurvivorTo三個區。

Eden 區(伊甸園區):

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

SurvivorFrom (倖存0區):

上一次 GC 的倖存者,作為這一次 GC 的被掃描者。

SurvivorTo(倖存1區):

保留了一次 MinorGC 過程中的倖存者。

MinorGC 的過程(複製 --> 清空 --> 互換)

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

首先,把 Eden 和 ServivorFrom 區域中存活的物件複製到 ServicorTo 區域(如果有物件的年 齡以及達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1(如果 ServicorTo 不 夠位置了就放到老年區)

清空 eden、servicorFrom:

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

ServicorTo 和 ServicorFrom 互換:

最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時的 ServicorFrom 區。

老年代

主要存放應用程式中生命週期長的記憶體物件。

老年代的物件比較穩定,所以 MajorGC(重GC) 不會頻繁執行。在進行 MajorGC 前一般都先進行 了一次 MinorGC,使得有新生代的物件晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新建立的較大物件時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。

MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的物件,然後回收沒有標記的物件。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生記憶體碎片,為了減少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會丟擲 OOM(Out of Memory)異常。

測試堆記憶體

public class TestJVM { public static void main(String[] args) {

//返回虛擬機器試圖使用的最大記憶體

long max = Runtime。getRuntime()。maxMemory();

//返回虛擬機器的初始化總記憶體

long total = Runtime。getRuntime()。totalMemory();

//預設情況下:分配的總記憶體是電腦執行時記憶體的1/4,而初始化記憶體是1/64

System。out。println(“max”+max+“位元組\t”+(max/(double)1024/1024)+“MB”);

System。out。println(“total”+total+“位元組\t”+(total/(double)1024/1024)+“MB”);

//新增:VM options

//-Xms1024m -Xmx1024m -XX:+PrintGCDetails

}}

max1873805312位元組 1787。0MBtotal126877696位元組 121。0MB

如下圖新增 VM options:-Xms1024m -Xmx1024 -XX:+PrintGCDetails,再次執行程式:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

2。5、方法區/永久代(執行緒共享)

方法區(method area)

只是

JVM 規範

中定義的一個概念,用於儲存被 JVM 載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。具體放在哪裡,不同的實現可以放在不同的地方。HotSpot VM把 GC 分代收集擴充套件至方法區,即使用 Java 堆的永久代來實現方法區,這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分記憶體,而不必為方法區開發專門的記憶體管理器(永久帶的記憶體回收的主要目標是針對常量池的回收和型別的解除安裝,因此收益一般很小)。

常量池

執行時常量池(Runtime Constant Pool)是方法區的一部分。Class 檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。 Java 虛擬機器對 Class 檔案的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個位元組用於儲存哪種資料都必須符合規範上的要求,這樣才會被虛擬機器認可、裝載和執行。

常量池是為了避免頻繁的建立和銷燬物件而影響系統性能,其實現了物件的共享。例如字串常量池,在編譯階段就把所有的字串文字放到一個常量池中。java 中基本型別的包裝類的大部分都實現了常量池技術,即Byte,Short,Integer,Long,Character,Boolean。前5種包裝類預設建立了數值[-128,127]的相應型別的快取資料,但是超出此範圍仍然會去建立新的物件。 兩種浮點數型別的包裝類Float,Double並沒有實現常量池技術。

永久代

永久代

Hotspot

虛擬機器特有的概念,指記憶體的永久儲存區域,主要存放 Class 和 Meta(元資料)的資訊,Class 在被載入的時候被放入永久區域,它和存放例項的區域不同,GC 不會在主程式執行期對永久區域進行清理。所以這也導致了永久代的區域會隨著載入的 Class 的增多而脹滿,最終丟擲 OOM 異常。是方法區的一種實現,別的 JVM 都沒有這個東西。雖然 Java 虛擬機器規範把方法區描述為堆的一個

邏輯部分

,但是它卻有一個別名叫做

Non-Heap(非堆)

,目的應該是與 Java 堆區分開來。

在 Java8 中,永久代已經被移除,被一個稱為“元資料區”(元空間)的區域所取代。元空間的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。類的元資料放入 native memory,字串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的元資料就不再由 MaxPermSize 控制, 而由系統的實際可用空間來控制。

3、JVM類載入器

虛擬機器設計團隊把載入動作放到 JVM 外部實現,以便讓應用程式決定如何獲取所需的類,JVM 提供了3種類載入器:

啟動類載入器(Bootstrap ClassLoader):

負責載入 JAVA_HOME\lib 目錄中的,或透過 -Xbootclasspath 引數指定路徑中的,且被虛擬機器認可(按檔名識別,如 rt。jar)的類。

擴充套件類載入器(Extension ClassLoader):

負責載入 JAVA_HOME\lib\ext 目錄中的,或透過 java。ext。dirs 系統變數指定路徑中的類庫。

應用程式類載入器(Application ClassLoader):

負責載入使用者路徑(classpath)上的類庫。JVM 透過雙親委派模型進行類的載入,當然我們也可以透過繼承 java。lang。ClassLoader 實現自定義的類載入器。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

4、雙親委派機制

當一個類收到了類載入請求,他首先不會嘗試自己去載入這個類,而是把這個請求委派給父類去完成,每一個層次類載入器都是如此,因此所有的載入請求都應該傳送到啟動類載入其中,只有當父類載入器反饋自己無法完成這個請求的時候(在它的載入路徑下沒有找到所需載入的 Class),子類載入器才會嘗試自己去載入。

採用雙親委派的一個好處是比如載入位於 rt。jar 包中的類 java。lang。Object,不管是哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入器最終得到的都是同樣一個 Object 物件。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

5、沙箱安全機制

Java 安全模型的核心就是 Java 沙箱(sandbox),什麼是沙箱?沙箱是一個限制程式執行的環境。沙箱機制就是將 Java 程式碼限定在虛擬機器(JVM)特定的執行範圍中,並且嚴格限制程式碼對本地系統資源訪問,透過這樣的措施來保證對程式碼的有效隔離,防止對本地系統造成破壞。沙箱主要限制系統資源訪問,那系統資源包括什麼?CPU、記憶體、檔案系統、網路。不同級別的沙箱對這些資源訪問的限制也可以不一樣。

所有的 Java 程式執行都可以指定沙箱,可以定製安全策略。

在 Java 中將執行程式分成原生代碼和遠端程式碼兩種,原生代碼預設視為可信任的,而遠端程式碼則被看作是不受信的。對於授信的原生代碼,可以訪問一切本地資源。而對於非授信的遠端程式碼在早期的 Java 實現中,安全依賴於沙箱(Sandbox)機制。如下圖所示 JDK1。0 安全模型:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

但如此嚴格的安全機制也給程式的功能擴充套件帶來障礙,比如當用戶希望遠端程式碼訪問本地系統的檔案時候,就無法實現。因此在後續的 Java1。1 版本中,針對安全機制做了改進,增加了安全策略,允許使用者指定程式碼對本地資源的訪問許可權。如下圖所示 JDK1。1 安全模型:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

在 Java1。2 版本中,再次改進了安全機制,增加了程式碼簽名。不論原生代碼或是遠端程式碼,都會按照使用者的安全策略設定,由類載入器載入到虛擬機器中許可權不同的執行空間,來實現差異化的程式碼執行許可權控制。如下圖所示JDK1。2 安全橫型:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

當前最新的安全機制實現,則引入了域(Domain)的概念。虛擬機器會把所有程式碼載入到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行互動,而各個應用域部分則透過系統域的部分代理來對各種需要的資源進行訪問。虛擬機器中不同的受保護域(Protected Domain),對應不一樣的許可權(Permission)。存在於不同域中的類檔案就具有了當前域的全部許可權,如下圖所示最新的安全模型(jdk 1。6):

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

組成沙箱的基本元件:

位元組碼校驗器(bytecode verifier):

確保 Java 類檔案遵循 Java 語言規範。這樣可以幫助 Java 程式實現記憶體保護。但並不是所有的類檔案都會經過位元組碼校驗,比如核心類。

類裝載器(class loader):

其中類裝載器在3個方面對 Java 沙箱起作用它防止惡意程式碼去幹涉善意的程式碼,雙親委派機制它守護了被信任的類庫邊界它將程式碼歸入保護域,確定了程式碼可以進行哪些操作

虛擬機器為不同的類載入器載入的類提供不同的名稱空間,名稱空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個名稱空間是由 Java 虛擬機器為每一個類裝載器維護的,它們互相之間甚至不可見。類裝載器採用的機制是雙親委派模式:

從最內層 JVM 自帶類載入器開始載入,外層惡意同名類得不到載入從而無法使用

由於嚴格透過包來區分了訪問域,外層惡意的類透過內建程式碼也無法獲得許可權訪問到內層類,破壞程式碼就自然無法生效。

存取控制器(access controller):

存取控制器可以控制核心 API 對作業系統的存取許可權,而這個控制的策略設定,可以由使用者指定。

安全管理器(security manager):

是核心API和作業系統之間的主要介面。實現許可權控制,比存取控制器優先順序高。

安全軟體包(security package):

java。security 下的類和擴充套件包下的類,允許使用者為自己的應用增加新的安全特性,包括:安全提供者訊息摘要數字簽名加密鑑別

6、GC垃圾回收演算法

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

6。1、如何確定垃圾

引用計數法

在 Java 中,引用和物件是有關聯的。如果要操作物件則必須用引用進行。因此,很顯然一個簡單 的辦法是透過引用計數來判斷一個物件是否可以回收。簡單說,即一個物件如果沒有任何與之關聯的引用,即他們的引用計數都不為 0,則說明物件不太可能再被用到,那麼這個物件就是可回收物件。

可達性分析

為了解決引用計數法的迴圈引用問題,Java 使用了可達性分析的方法。透過一系列的 GC roots 物件作為起點搜尋。如果在 GC roots 和一個物件之間沒有可達路徑,則稱該物件是不可達的。要注意的是,不可達物件不等價於可回收物件,不可達物件變為可回收物件至少要經過兩次標記過程。兩次標記後仍然是可回收物件,則將面臨回收。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

6。2、標記清除演算法(Mark-Sweep)

最基礎的垃圾回收演算法,分為兩個階段,標註和清除。標記階段標記出所有需要回收的物件,清除階段回收被標記的物件所佔用的空間。如圖:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

從圖中我們就可以發現,該演算法最大的問題是記憶體碎片化嚴重,後續可能發生大物件不能找到可利用空間的問題。

6。3、複製演算法(copying)

為了解決 Mark-Sweep 演算法記憶體碎片化的缺陷而被提出的演算法。按記憶體容量將記憶體劃分為等大小的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的物件複製到另一塊上去,把已使用 的記憶體清掉,如圖:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

這種演算法雖然實現簡單,記憶體效率高,不易產生碎片,但是最大的問題是可用記憶體被壓縮到了原 本的一半。且存活物件增多的話,Copying 演算法的效率會大大降低。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

6。4、標記整理演算法(Mark-Compact)

結合了以上兩個演算法,為了避免缺陷而提出。標記階段和 Mark-Sweep 演算法相同,標記後不是清理物件,而是將存活物件移向記憶體的一端。然後清除端邊界外的物件。如圖:

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

6。5、分代收集演算法

分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據物件存活的不同生命週期將記憶體劃分為不同的域,一般情況下將 GC 堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量物件需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的演算法。

新生代與複製演算法

目前大部分 JVM 的 GC 對於新生代都採取 Copying 演算法,因為新生代中每次垃圾回收都要回收大部分物件,即要複製的操作比較少,但通常並不是按照 1:1 來劃分新生代。一般將新生代劃分為一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用 Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的物件複製到另 一塊 Survivor 空間中。

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!

老年代與標記複製演算法

而老年代因為每次只回收少量物件,因而採用 Mark-Compact 演算法。

JAVA 虛擬機器提到過的處於方法區的永生代(Permanet Generation),它用來儲存 class 類, 常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。

物件的記憶體分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放物件的那一塊),少數情況會直接分配到老生代。

當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後,Eden Space 和 From Space 區的存活物件會被挪到 To Space,然後將 Eden Space 和 From Space 進行清理。

如果 To Space 無法足夠儲存某個物件,則將這個物件儲存到老生代。

在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆迴圈。

當物件在 Survivor 區躲過一次 GC 後,其年齡就會+1。預設情況下年齡到達 15 的物件會被移到老生代中。

ps:私信小編有驚喜喔~

分類: [JavaSE]

一篇JVM詳細圖解,堅持看完!帶你真正搞懂Java虛擬機器!