你瞭解JVM嗎,快來看看這篇文章

本文基於Jdk1.7版本,VM爲Hotspot

前言:

在講JVM之前,首先引入一個概念叫“跨平臺”,學JAVA的人都知道,Java就是一門跨平臺的語言,其實就是因爲Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。(面試點

 

概念:

JVM 即Java虛擬機, 由類加載子系統、運行時數據區、執行引擎以及本地方法接口組成。本文着重介紹類加載子系統 和 運行時數據區。

一、類加載子系統:

Java的類加載功能是通過類加載子系統來完成的。當JVM需要使用一個類的時候,類加載子系統能夠從class文件載入、鏈接和初始化這個類。整個類加載過程如下圖所示(面試點):

1、加載

加載是類加載過程中的第一個階段,這個階段會在內存中生成一個代表這個類的 java.lang.Class 對 象,作爲方法區這個類的各種數據的入口。注意這裏不一定非得要從一個 Class 文件獲取,這裏既 可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在運行時計算生成(動態代理), 也可以由其它文件生成(比如將 JSP 文件轉換成對應的 Class 類)。

2、校驗

這一階段的主要目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並 且不會危害虛擬機自身的安全。

3、準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間

例:

public static int  vPort= 80; (靜態變量)
實際上變量 vPort在準備階段過後的初始值爲 0 而不是 80,將 vPort賦值爲 80 是在初始化階段; 

public static final int vPort= 80; (常量)
在編譯階段會爲 vPort生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 vPort賦值爲 80。 

4、解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程

符號引用:
符號引用與虛擬機實現的佈局無關,引用的目標並不一定要已經加載到內存中。
各種虛擬 機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在 Java 虛擬機規範的 Class 文件格式中。  
直接引用:
直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。

例:

User user = new User();
在未加載時候,user僅僅是一個字符(符號引用)它指向了一個實例,但是這個實例還不存在;
準備過程之後,實例分配了內存那麼就會有內存地址或者物理地址(直接引用)

5、初始化

初始化階段是類加載最後一個階段,到了初始階段,纔開始真正執行類中定義的 Java 程序代碼。
在準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源(調用構造器)

二、類的雙親委派機制:

類加載器:

啓動類加載器(BootstrapClassLoader):負責加載 JAVA_HOME\jre\lib目錄中的類庫,或通過-Xbootclasspath 參數指定路徑中的,且被 虛擬機認可(按文件名識別,如 rt.jar)的類庫。
擴展類加載器(ExtensionClassLoader):負責加載JAVA_HOME\jre\lib\ext目錄中的類庫,或通過 java.ext.dirs 系統變量指定路徑中的類庫。
應用程序加載器(ApplicationClassLoader):負責加載用戶路徑(classpath)上的類庫。
自定義類加載器:負責加載用戶指定目錄下的類庫,可以通過繼承 java.lang.ClassLoader 實現自定義的類加載器

過程:

當類加載器接收到一個類的加載請求時,大致做了如下步驟:面試點

1)檢測自己是否有加載過目標類,如果有直接進入第6步,否則進入第2步。
2)委託給父類加載器去加載目標類,父類加載器同樣策略執行第1步,一直到啓動類加載器。
3)當所有的父類加載器都無法加載的時候,當前類加載器執行第4步
4)加載目標類,如果找不到類,執行第5步,否則執行第6步,並將其放入自己的緩存中
5)拋出ClassNotFountException異常。
6)返回對應的java.lang.Class對象。

好處:

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

三、運行時數據區:

組成部分:程序計數器(線程私有)、虛擬機棧(線程私有)、本地方法棧(線程私有)、方法區/永久代(線程共享)、堆(線程共享)

程序計數器:

一塊較小的內存空間,是當前線程所執行的字節碼的行號指示器,記錄着當前程序運行到哪了字節碼指令。分支,循環,跳轉,異常處理,線程回覆等都需要依賴這個計數器來完成 。

由於Java的多線程是通過線程輪流切換完成的,一個線程沒有執行完時就需要一個東西記錄它執行到哪了,下次搶佔到了CPU資源時再從這開始,這個東西就是程序計數器,正是因爲這樣,所以它也是“線程私有”的內存。

如果線程正在執行的是Java 方法,則這個計數器記錄的是正在執行的虛擬機字節碼指令地址;
如果正在執行的是Native 方法,則這個計數器值爲空(null);
這個內存區域是唯一一個在虛擬機中沒有規定任何 OutOfMemoryError 情況的區域;

虛擬機棧:

是描述java方法執行的內存模型,先進後出原則,每個方法在執行的同時都會創建一個棧幀(Stack Frame) 用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息每一個方法從調用直至執行完成 的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程

可能拋出的異常:
StackOverflowError:如果線程內請求的棧深度超過虛擬機允許的深度,會拋出該異常。(通常由遞歸不當造成) 
OutOfMemoryError:如果虛擬機允許動態擴展棧的大小,但是在擴展時無法申請到足夠的內存空間,會拋出該異常

棧幀( Frame):
用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接 (Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨着方法調用而創建,隨着方法結束而銷燬——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異 常)都算作方法結束。
在編譯程序代碼的時候,棧幀中需要多大的局部變量表,多深的操作數棧就都已經確定了。因此一個棧幀需要分配多大的內存不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

先進後出原則:
如果方法methodOne方法調用了methodTwo,那麼methodOne就會先入棧創建一個棧楨,接着methodTwo再入棧成爲棧頂(假設沒有其他的方法執行),methodTwo執行完先出棧,接着methodOne執行完出棧。

局部變量表:

存放了編譯期可知的各種基本數據類型,對象引用(僅限局部變量的,不包含成員變量的)。其中每個局部變量空間(Slot)有32位,所以long和double類型的數據會佔用兩個局部變量空間,其他類型包括對象引用佔用一個。對象引用調用的是存在堆中的對象,這個引用可以是對象的起始地址或者是指向對象的句柄。局部變量表所需的內存在編譯期就已經確定了也就是進入這個方法時就已經確定了,運行期間不會更改。

操作數棧:

存儲方法內一些進行了運算操作後的結果
當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,會有各種字節碼指向操作數棧中寫入和提取值,也就是入棧與出棧操作。例如,在做算術運算的時候就是通過操作數棧來進行的,又或者調用其它方法的時候是通過操作數棧來行參數傳遞的。

動態鏈接:

在方法內調用接口,通過字面量鏈接到具體的實現類,實現Java的動態特性
每個棧幀都包含一個指向運行時常量池中該棧幀所屬性方法的引用,持有這個引用是爲了支持方法調用過程中的動態鏈接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分將在每一次的運行期期間轉化爲直接引用,這部分稱爲動態鏈接。

方法出口:

當一個方法被執行後,有兩種方式退出這個方法。
執行引擎遇到方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者。這種退出方法方式稱爲正常完成出口。
在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理(沒有catch),就會導致方法退出。這種退出方式稱爲異常完成出口。一個方法使用異常完成出口的方式退出不會產生任何返回值。

本地方法棧:

本地方法棧與虛擬機棧非常類似,他們之間的區別只是虛擬機棧爲虛擬機執行Java服務,本地方法棧爲虛擬機執行Native方法服務

可能拋出的異常:StackOverflowError和OutOfMemoryError異常

方法區/永久代:

方法區也是一個線程共享的區域,存儲已被虛擬機加載的類信息,常量(final),靜態變量(static),JIT(即時編譯器)編譯後的代碼等數據。(常量也可以在運行時產生,如String的intern方法)

虛擬機對方法區規範非常寬鬆,除了和Java的堆一樣不需要連續的內存和可以選擇固定大小意外,還可以選擇不實現垃圾回收。垃圾回收行爲在這個區域比較少見但還是有必要的,主要是針對常量池回收和類型的卸載

1.對於每一個加載的類型,會在方法區中保存以下信息:
    類及其父類的全限定名(java.lang.Object沒有父類)、類的類型(Class or Interface)、訪問修飾符(public, abstract, final)、實現的接口的全限定名的列表、常量池、字段信息、方法信息、除常量外的靜態變量、ClassLoader引用、Class引用;

2.對於每一個字段,會在方法區中保存以下信息(字段聲明順序也會保存):
    字段名、字段的類型、字段的修飾符(public, private , protected, static, final, volatile, transient);

3.對於每一個方法,會在方法區中保存以下信息(方法聲明順序也會保存):
    方法名、方法返回類型(或void)、參數信息、方法修飾符(public, private, protected , static, final, synchronized, native, abstract);如果方法不是抽象方法並不是本地方法(Native Method),還會保存以下信息: 方法的字節碼、本地變量表及操作數棧的大小、異常表;

常量池:

它是方法區(Method Area)的一部分。用於存放編譯期間生成的各種字面量和符號引用。常量池中存儲的是對象的引用而不是對象的本身。

堆:

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

可能拋出的異常:
OutOfMemoryError:如果堆中沒有內存完成實例分配,並且堆也無法擴展時,將會拋出此異常。

 

新生代:
是用來存放新生的對象。一般佔據堆的 1/3 空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC(採用複製算法)進行垃圾回收。分爲 Eden 區、ServivorFrom、ServivorTo 三個區。

1)Eden區:Java 新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老年代,如:大的數組)。當Eden區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。
2)ServivorFrom:上一次 GC 的倖存者,作爲這一次 GC 的被掃描者。
3) ServivorTo:保留了一次 MinorGC 過程中的倖存者。

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

1).首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(如果有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);
2).然後清空 Eden 和 ServicorFrom 中的對象;
3).最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom區。

圖解JVM GC過程https://www.jianshu.com/p/314272e6d35b

老年代:
主要存放應用程序中生命週期長的內存對象。
老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
MajorGC 採用標記清除算法:

1).首先掃描一次所有老年代,標記出存活的對象;  2).然後回收沒有標記的對象。

MajorGC 的耗時比較長,因爲要掃描再回收。MajorGC 會產生內存碎片,爲了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。


結尾:

    本文是最近學習JVM的一些總結和記錄,如有不對的地方,歡迎評論吐槽。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章