天天看點

朝花夕拾——初探Java虛拟機及其加載過程

這一周的顔如玉系列裡,大部分都在研究Android的熱更新檔技術。這裡不做深入,原理無非是在Android在加載Dex時,先記載新的class檔案,以此替換舊的class檔案。附上張鴻洋的的Blog連結,有興趣的朋友可以了解。

為了徹底了解這一技術原理,是以打算從Java 虛拟機(Java Virtual Machine:以下簡稱 JVM)的加載開始學習。

本篇會以系統的方式為大家帶來以下内容:

  1. JVM及其生命周期介紹
  2. JVM體系結構介紹
  3. JVM加載Class過程

JVM生命周期介紹

什麼是JVM?

在《深入JAVA虛拟機第二版》中是這樣定義的:

  • 抽象規範
  • 一個具體的實作
  • 一個運作中的虛拟機執行個體

以上内容有點奇怪。一個虛拟機怎麼有三種截然不同的定義?

因為在不同的環境下,這JVM有三種含義:

假如你是要實作某某平台下的JVM,那麼一開始,JVM對你來說就是一個規範或者一個概念。然後,你可以采用軟體,或軟硬結合的方式來具體實作JVM的規範(即具體實作)。當你運作一個Java程式的時候,JVM就是一個運作中的虛拟機執行個體。

好吧,的确有點奇怪。不過對我們開發者來說,最後一個定義更符合我們的了解。

JVM的生命周期

對于一個JVM而言,它的生命周期與Java程式的運作情況息息相關,當你想開啟一個Java程式時,會先生成一個JVM的運作執行個體,這個執行個體通過調用程式中的 main(String[] args)開啟Java程式。我們的Java程式就在這個JVM執行個體中運作的。當你退出程式,目前的JVM執行個體也被銷毀。

請注意:這是 1 <–>1 的關系,一個JVM隻負責運作一個Java程式。

JVM體系結構介紹

從上面,我們知道一個Java程式是在一個JVM中運作的。那JVM裡到底有什麼東西呢?

下圖是JVM的體系結構圖,包含着我們的JVM裡所有的東西。

朝花夕拾——初探Java虛拟機及其加載過程
  • 類裝載子系統:涉及了Java虛拟機的其他幾個部分,以及幾個來自java.lang庫的類(如 ClassLoader:為程式提供了通路類裝載器機制的接口),詳細内容将在下文JVM加載過程部分中講訴。
  • 運作時資料區(記憶體):分為5個子產品,這個比較重要,稍後會有詳細說明。
  • 本地方法接口:通過這個接口可使得JVM内的Java程式與本地方法進行互動,咦,怎麼那麼眼熟?其實就是我們的JNI啦
  • 執行引擎:JVM運作我們Java代碼的核心實作。通過PC寄存器中的指令執行Java方法,也可以執行本地方法接口。需要注意的是一個線程對應一個執行引擎

作為一個Coder,OutOfMemryError,StackOverFlowError這些錯誤應該是經常遇到的。那為什麼會産生這些錯誤呢,就與我們的運作時資料區(記憶體區)有關系。

下面,我們按是否線程私有來區分這五個子產品:

(我說的細的掉渣,如果你隻想粗淺的知道 點這)

所有線程均可通路:方法區,堆

  • 方法區:簡單的說,儲存着類資料,即被裝載的類,其基本資訊,與一些額外資訊都在方法區中被儲存(當某類沒有被引用時,不同的JVM下,不一定實作垃圾回收,無記憶體可配置設定時候,會出現OutOfMemoryError)

    有人問基本資訊是什麼?

  • 這個類型的全限定名(就是完整類名,如Java.lang.Object等)
  • 這個類型的父類的全限定名(除非這個類的是Object類,沒有父類了)
  • 這個類型是類類型,還是接口類型
  • 這個類型的通路修飾符(如public,private等 )
  • 這個類型的所有接口的全限定名的有序清單

    額外資訊又是什麼?

  • 這個類型的運作時常量池:所有的常量,與該類中所使用的所有類在方法區内的符号引用 (也就是說,所有在該類中使用的類,都會保留它的一份在方法區内的符号引用,該類容易第一時間找到需要使用的類相關資訊。)
  • 字段資訊 (字段的名字,類型,修飾符)
  • 方法資訊 (方法名,傳回類型,入參的數量與類型,修飾符,方法中局部變量大小,方法的位元組碼,異常表)
  • 除了常量以外的所有 類(靜态)變量(所有類執行個體共享,即時沒有類執行個體也可以被通路,注意這類變量些變量隻和類有關,而非類的執行個體。他們隻作為一部分類型資訊儲存在方法區而已。隻有在被使用的過程中才會被配置設定空間)
  • 一個到ClassLoader的引用 (如果是由啟動類裝載器裝載,不記載。由使用者自定義的類裝載器裝載的,保留引用)
  • 一個到Class類的引用 (使得可以通過Class類可以輕易的擷取目前内的類在方法區中的資訊,與引用。如:可以用Class.forNmae(*)來得到目前的Class的對象引用)
  • 堆:簡單的說,所有的執行個體化出的對象,數組都在這個區域,不過,所有線程都可以通路的時候,需要考慮線程安全問題。這個區域,不一定要連續的空間,支援動态的擴張和縮小。記憶體洩露一般就發生在這。(所有的JVM都在這實作了垃圾回收,記憶體無法支援建立對象時,會出現OutOfMemoryError)

單一線程私有: Java棧,PC寄存器,本地方法棧

  • Java棧:以幀為機關保持着Java的運作狀态,一個方法為一個棧幀,調用時,入棧,結束傳回時出棧。
    • 棧幀:包括局部變量區,操作數棧,幀資料區;其中局部變量内儲存着入參,局部變量;操作數棧:執行指令時,以棧的形式作為資料的臨時儲存與操作區(建議看Blog);幀資料區包含着一些資料來支援常量池資料的解析,正常傳回,與異常委派機制。當棧幀記憶體大小固定時,超過了大小,會發生StackOverflowError 錯誤,當棧幀為動态增長時,申請的記憶體大小不足時,會發生OutOfMemoryError錯誤
  • PC寄存器:或者說是程式計數器更為恰當,隻有一個字長大小。記錄着下一條的将要被執行的指令位址,除本地方法除外。
  • 本地方法棧:一般為調用C/C++方法,實作和Java棧類似。不過當進入本地方法棧時,pc寄存器的值為“undefined”。

以上是按邏輯來區分,不過按實體上區分應該是以下這樣的(這點上衆說紛纭,不要問我….)

朝花夕拾——初探Java虛拟機及其加載過程

JVM加載Class過程

我們都知道編譯好的class是一個二進制的檔案,那我們的JVM是如何加載他們的呢?就是我們上面介紹的類裝載子系統

加載一個class檔案的在這裡分為3步

1.裝載:查找相應的class檔案并裝載二進制資料。

2.連接配接:執行驗證,準備,以及解析(可選)

    驗證:確定被導入類型的正确性

    準備:為類變量配置設定記憶體,并将其初始化為預設值

    解析:把類型中的符号引用轉化為直接引用

3.初始化:把類變量初始化為正确初始值。

更加詳細點這裡

而在代碼裡完成這些的,是我們的ClassLoader類。

以下給出其他學習連結

ClassLoader雙親加載

ClassL:oader類 中文文檔

我想我寫的絕對不會比他們更好。是以恬不知恥的引用了他們的文章。

謝謝各位。本篇的很多理論來自《深入JAVA虛拟機第二版》,有興趣的朋友可以去看看。