JVM 學習筆記(三)- 從虛擬機的啓動-類加載開始

1. 虛擬機的啓動

Java虛擬機的啓動是經過引導類加載器(Bootstrap Class Loader)建立一個初始類(Initial Class)來完成的,這個是由虛擬機的具體實現指定的。html

2. 虛擬機的執行

  1. 一個運行中的Java虛擬機有着一個清晰的任務:執行Java程序
  2. 程序開始執行時他才運行,程序結束時,他就中止運行。
  3. 執行一個所謂的Java程序時,電腦是真真正正地在執行一個Java虛擬機進程

咱們在執行一個簡單的程序Main方法程序的時候,實際上會加載很是多的類。當咱們在代碼中加入掛起代碼,而後在掛起期間採用JPS指令查看JVM當前所在運行的進程號(pid)以下: Main類獨立地佔用了一個進程號,而其餘的輔助運行類也在運行;當程序執行完成後,天然而然地Main方法消失了 java

3. 線程

在HotSpot虛擬機中,每一個線程都與操做系統的本地線程直接映射。當一個Java線程準備好執行之後,此時一個操做系統的本地線程也同時建立,Java線程執行終止後,本地線程也會回收。操做系統負責全部線程的安排調度到任何一個可用的CPU上,一旦本地線程初始化成功以後,他就會調用Java線程中的run()方法。線程又區分爲守護線程非守護線程若是程序中剩下的只有守護線程,那麼虛擬機也會退出數據庫

一個Java程序背後其實有不少的線程:安全

  • 虛擬機線程:這種線程的操做時須要JVM到達安全點纔會出現,這些操做必須在不一樣的線程中發生的緣由是:他們都須要JVM達到安全點,這樣堆纔不會變化。這種線程的執行的任務包括「STOP-THE-WORLD」,即讓全部線程都終止的垃圾收集、線程棧收集、線程掛起、偏向鎖撤銷。
  • 週期任務線程:這種線程是時間週期的體現,好比中斷等等,他們通常用於週期性操做的調度執行。
  • GC線程:這種線程對在JVM裏不一樣種類的垃圾收集行爲提供了支持。
  • 編譯線程:這種線程在運行時會將字節碼編譯成本地代碼。
  • 信號調度線程:這種線程接受信號發送給JVM,在它內部經過調用適當的的方法進行處理。
  1. JVM安全點:在虛擬機在進行可達性分析時,HotSpot虛擬機會在特定的位置記錄在哪有引用,這些特定的位置就叫作安全點。這是GC方面的知識,以後會作解析。

4. 類加載過程

咱們知道,JVM讀入Class文件進行加載。通常的讀入方式都是存在於本地的磁盤中,可是實際上,類加載支持:本地、網絡、JAR包、甚至是運行時計算生成獲得的Class文件。markdown

4.1 類加載的宏觀過程

  1. .Class文件存放在本地磁盤上,能夠理解爲設計師在紙上畫的模板,最終這個模板在執行的時候是要加載到JVM當中來的,根據該模板,JVM能夠實例化出N個如出一轍的實例。
  2. .Class文件加載到JVM中,被稱爲DNA元數據模板,放在方法區。
  3. 在.Class文件 -> JVM -> 最終成爲元數據模板,這個過程就須要一個運輸工具(Class Loader),扮演一個快遞員的操做。而這個運輸工具就是咱們今天的主角:類加載子系統。

4.2 類加載的詳細過程

4.2.1. 加載階段(狹義上的加載)

加載階段主要的任務就是,讀入Class文件,構建靜態存儲結構,在內存中生成元數據模板。詳細過程以下:網絡

  1. 經過一個類的全限定名獲取定義此類的二進制字節流。
  2. 將字節流 所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

加載的幾個來源:數據結構

  • 本地系統
  • 網絡
  • JAR、壓縮包等等
  • 運行時計算生成,例如:動態代理技術
  • 由其餘文件生成
  • 從專有的數據庫中提取.Class文件
  • 從加密文件中獲取,典型的防Class文件被反編譯的保護措施

4.2.2. 連接階段

連接階段主要是驗證Class文件的有效性、準確性和爲類中的信息初始化變量值、執行靜態方法。連接階段又劃分爲三個小的階段:多線程

4.2.2.1. 驗證階段

驗證的目的在於確保Class文件的字節流中包含信息符合當前的虛擬機要求,保證被加載類的正確性,保證虛擬機自身的安全。包括:函數

  • 文件格式驗證:可以被Java虛擬機識別的文件二進制頭的十六進制表示均是CA FE BA BE(Cafe babe)。
  • 元數據驗證
  • 字節碼驗證
  • 符號引用驗證

何爲元數據?【百度百科】工具

  1. 元數據(Metadata),爲描述數據的數據,主要是描述數據屬性的信息,用來支持如指示存儲位置、歷史數據、資源查找、文件記錄等功能。
  2. 元數據最大的好處是,它使信息的描述和分類能夠實現格式化,從而爲機器處理創造了可能。

其餘的內容能夠看看這篇博客:元數據(MetaData)

4.2.2.2. 準備階段

類變量分配內存而且設置該類變量的默認初始值。即零值。有以下代碼:

class Test{
		public static int a = 1;
	}
複製代碼

在準備階段,此時的類變量a的值僅僅是0,而不是1。

  1. 這裏不包括被final修飾的static變量,由於Final在編譯階段就會分配一個固定的值,編譯期即把結果放入了常量池。在運行時被初始化,能夠直接將這個固定死的值賦值給它。賦值後不可修改,可是常量池中只能引用到基本數據類型+String
  2. 這裏也不會爲實例變量分配初始化。由於類變量會分配在方法區中,而實例變量則是對象,會和其餘對象同樣分配到Java區中。(這裏不是說類變量不是對象,而是說類變量自己是一種特殊的對象。他被分配在方法區中,而不是和其餘new出來的對象同樣分配在堆中。)

其實,準備階段的操做能夠簡單地理解爲:只構建一個最爲簡單的類,除非咱們定下了寫死的final staitc且是(8+1)的基本數據類型之外,都是賦默認零值,引用類型爲null。

4.2.2.3. 解析階段
  1. 將常量池內的符號引用轉換爲直接引用。
  2. 符號引用就是一組符號來描述所引用的目標,符號引用的字面量形式明肯定義在《Java虛擬機規範》的Class文件格式中,直接引用就是直接指向目標的指針相對偏移量或一個間接定位到目標的句柄
  3. 解析動做主要針對類或者是接口、字段、類方法、接口類型、方法類型等等,對應常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等.

4.2.3. 初始化階段

做爲第二個大的階段連接階段,僅僅是完成了類的載入和數據類型的初始化,除了部分常量值,其餘的類對象、實例對象都沒有被正確賦值。而第三個階段初始化階段則是完成遺留下的問題的。

  1. 初始化階段執行的是:類構造器方法<Clinit>()方法,注意,構造器方法和構造方法不一樣。
  2. 構造器方法中的指令按照語句在源文件中出現的順序執行。
  3. <Clinit>()方法不一樣於類的構造器:構造器是虛擬機視角下的<init>()
  4. 若該類已有父類,那麼會保證子類的<Clinit>()方法執行前,父類的<Clinit>()方法語句執行完成。
  5. 虛擬機必須保證一個類的<Clinit>()方法在多線程的狀況下被同步加鎖,一個類的靜態代碼塊的初始化只能執行一次。這也是咱們靜態內部類實現線程安全的懶漢式單例的重要理論基礎。
  1. 單例模式

分爲餓漢式和懶漢式,餓漢式天生線程安全,可是作不到懶加載。而懶漢式可以實現線程安全,可是須要加鎖處理,不然多線程可能會建立不一樣的實例。目前的幾種實現線程安全的懶漢式單例方式有常見的加鎖處理。而靜態內部類的特性可使得它自然地實現懶漢式的單例模式。 首先是線程安全,在上文的3.5中,咱們知道,一個類的靜態代碼塊的初始化會被JVM加鎖,這樣一來,咱們就不須要手動加鎖了。 其次是懶加載,只有咱們調用到的時候,類加載器纔會爲咱們加載靜態內部類,不然是不會加載的,這樣一來,咱們就實現了線程安全的懶漢式單例模式。

2.構造器、構造函數、<init> 實例構造器<Clinit>()類構造器

  1. 構造函數:也叫構造方法,就是咱們寫代碼裏面new一個類的構造方法。
  2. 構造器:Javac編譯,生成的一個函數,是在字節碼層面存在的「函數」。它其實對一些代碼的整合後生成的函數。
  3. <init> 實例構造器:針對的是實例構造。
  4. ·<Clinit>()類構造器:cinit針對是類。

數量上來來說<init> 實例構造器至少存在一個, <Clinit>()類構造器構造器只存在一個. 由於類對象在JVM內存中只會存在一個(同一個類加載器)。 3. 枚舉類。枚舉類是一種比較特殊的類,它的底層實質上仍是Class,只不過是成員變量被public static final修飾的成員變量(經過類名調用),因此它是在static靜態代碼塊中一塊兒初始化的。因爲java類的加載和初始化過程都是線程安全的,因此建立一個enum類型是線程安全的,因此用枚舉類實現一個線程安全的單例是可行的。

4.3 靜態實例變量的賦值變化過程:

####代碼1:

pivate static int number = 1;
複製代碼
序號 階段
1 加載階段 -
2 連接-驗證 -
3 連接-準備 0
4 連接-解析 0
5 初始化階段 10

代碼2:

private final static int number = 10;
複製代碼
序號 階段
1 加載階段 -
2 連接-驗證 -
3 連接-準備 10
4 連接-解析 10
5 初始化階段 10

注意,這裏的各類虛擬機的實現各有不一樣,例如HotSpot虛擬機在驗證-準備階段就已經賦初值了,可是JVM規範是要在初始化階段才賦初值的。詳細可見參考來源1

代碼3:

private int number = 10;
複製代碼

在具體的實例建立的時候纔會賦初值。

##5 類加載器 前面,咱們介紹了一個類被加載進入JVM的不一樣的階段,然而具體執行加載過程的是咱們的類加載器(Class Loader)

類加載器劃分紅三種:

  1. BootStrap ClassLoader:這是由C/C++編寫的類架子啊器,嵌套在JVM內部。用來加載Java的核心類庫,用於加載:JAVA_HOME/jre/lib/rt.jar、resources.jar或者是sun.boot.class.path目錄下的文件
  1. BootStrapClassLoader沒有父加載器,可是他是擴展類加載器的父加載器。
  2. BootStrapClassLoader只能加載包名爲java、javax、sun開頭的類。
  1. Extension Classloader
    • i. 擴展類加載器:由Java語言編寫,派生於BootStrapClassLoader。從java.ext.dirs系統屬性所制定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(或者擴展目錄)下加載類庫。若是用戶建立的JAR放在此目錄下,也會由擴展類加載器進行加載。

    • ii. 應用程序類加載器(系統類加載器):由Java語言編寫,派生於BootStrapClassLoader,負責加載環境變量classpath或者系統屬性java.class.path指定路徑下的類庫。該類是默認的類加載器。通常來講,Java應用的類都是由它來完成加載。

    • iii. 用戶自定義的類加載器。

6. 雙親委派機制

網路上關於雙親委派機制的講解不少,通俗地說,受權委派機制就是當類加載的時候,咱們將任務"承包"給了類加載器,然而類加載器並不會本身去加載類,而是優先提交給其父類去加載,若是父類能加載,那麼我本身就不加載,使用父類加載獲得的類數據。

這樣作的目的是爲了保護類加載的來源,防止類的重複加載和核心API被惡意篡改致使的代碼泄漏或者是功能缺失。

7. 主動使用和被動使用

JVM必須知道一個類型是由BootStrap ClassLoader仍是其餘的ClassLoader加載的。

若是一個類型是用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中,當解析一個類型到另外一個類型的引用時,JVM須要保證兩個類型的類加載器是相同的。

JVM對類的使用分紅主動使用和被動使用。其中主動使用又細分爲七種狀況:

  1. 建立類的實例

  2. 訪問某個類或者接口的靜態變量,或者對該靜態變量賦值。

  3. 調用類的靜態方法

  4. 反射

  5. 初始化一個類的子類

  6. Java虛擬機啓動時被標明爲啓動類的類

  7. JDK7開始提供的動態語言支持Java.lang.invoke.MethodHandle實例的解析結果REF_getStaticREF_putStaticREF_invokeStatic句柄對應的類沒有初始化則初始化。

除了以上的七種狀況,其餘的調用都被視做是對類的被動調用,都不會致使類的初始化。

若是同一個類被不一樣的類加載器所加載,那麼這兩個類是不一樣的類。

8. 總結

.Class文件會被類加載子系統讀入JVM中的方法區當中,在方法區中,存放了最基本的類信息,而類加載子系統運行的各個階段會爲咱們類賦值、調用靜態方法等等。如圖中的紅線,就是類加載的主要過程。

參考來源
  1. 《深刻理解Java虛擬機-JVM高級特性與最佳實踐》 - 周志明著
  2. 你知道Java中final和static修飾的變量是在何時賦值的嗎?
  3. java枚舉類是怎麼初始化的,爲何說枚舉類是線程安全的
擴展閱讀
  1. Java 類的熱替換 —— 概念、設計與實現
相關文章
相關標籤/搜索