Java功底 — 類加載機制

學習一門編程語言的三個層次

我覺得對一門語言的理解不能僅僅停留在表面,如果把學習一門編程語言分爲三個層次的話。我認爲大概分爲下面三個方面:

  • 第一層 是認識瞭解這門編程語言的語法、用法
  • 第二層 是瞭解這門編程語言同其他語言的優劣勢和生態
  • 第三層 是瞭解這門編程語言底層運行的機制,能從這樣有利於解決罕見問題和程序調優

Java最核心的還是JVM就是上面說的第三層,其中最重要的就是Java類加載機制。下面我們先看一下Java運行的過程

 

Java運行過程

首先,我們要了解一件事情,那就是我們編寫的Java代碼是如何運行起來的呢?

這張圖的流程可能很多人都知道。

Java文件通過javac編譯成.class字節碼文件,然後由JVM加載字節碼文件,開始編譯執行,這個加載的過程就是類加載。

JVM主要分爲五塊區域:

  • 虛擬機棧

  • 本地方法棧

  • pc寄存器

  • 方法區

下面放一張更詳細的流程圖

Java類的生命週期

這張圖應該很多人已經看到過,但是我今天還是要拿出來講一下,因爲我覺得這張圖本身是不嚴謹的,稍後會說明。

關於這張圖,我覺得有3個方面需要說明一下:

  1. 這張圖表現了一個類完整的生命週期,而我之前提到的類加載,只包括加載連接初始化這三個過程。
  2. 我們要區分類加載加載,圖上的加載只是類加載的第一步。
  3. 這張圖上,連接裏解析環節的順序是靈活的,驗證是分爲很多個步驟,而其他環境的順序固定不可變。比如後面會講到的“後期綁定”,就是發生在初始化環節之後。

接下來先來看加載這個環節。

加載

加載是在硬盤上查找或通過IO讀取.class字節碼文件,並將其轉化爲靜態數據結構存儲在方法區,同時在堆中實例化Class對象,便於用戶調用的java.lang.Class類型的對象的過程。

像一些動態代理技術,就是用的即時計算出來的class,然後實例化代理對象。而上面提到的方法區和堆的概念,可以參照前一篇掌握JVM這一篇文章就夠了,去查看JVM內存模型的說明。

接下來是連接裏的驗證階段。

連接-驗證

首先我要先說明一點,之所以我前面說那張Java類生命週期的圖不嚴謹,其一就是因爲驗證這個動作有很多步驟,其實是分佈在各個不同環節的,並非像圖所示的固定在某個環節。

  1. 其中第一個步驟,對文件格式的驗證,其實是發生在上面加載環節的,只有驗證通過才能順利加載,這時候JVM方法區就有了Class對象的靜態數據結構,堆中也有了實例化的對象,但這時候需要使用這個類還需要連接。
  2. 連接的第一步就是對這個類的元數據和字節碼進一步進行驗證,看是否會對JVM造成危害,等驗證通過,JVM暫且認爲這個類是安全的。但這時候驗證並沒有完全結束,還有一步對符號引用的驗證。
  3. 對符號引用的驗證,是發生在解析環節內的,而解析環節是可以在初始化環節之前或之後進行的,所以驗證是包含了很多步驟,分佈在各個不同環節的。

在元數據和字節碼驗證通過之後,虛擬機就進入到了準備階段。

連接-準備

準備就是爲類靜態變量分配內存,並將其初始化爲默認值

例:static int b= 10,這裏只將b初始化爲0,10的值會在初始化階段賦值。

類變量如果被final修飾則會直接賦予定義的值(即final static修飾的變量 final static int c = 15;)

此時會出現一個被太多人混淆和誤解的概念:

我們來好好捋一捋。

“常常看到有人說,JDK8及以後採用“元空間”來替代“方法區”,這種說話時完全錯誤的,方法區是抽象概念,元空間是具體實現方式”

方法區是JVM規範中的定義,只是個概念,無論是元空間還是永久代,都只是方法區的一種實現

  •  jdk7是永久代,用於存放類信息,字符串常量池,靜態變量。

  • jdk8以及以後元空間存放類信息,字符串常量池和靜態變量被放到了堆中。

jrocket和hotspot虛擬機都有方法區的概念,只是jrocket沒有永久代這種實現方式。在jdk8中整合兩大虛擬機之後,由永久代換成了元空間。

在準備完成之後,就來到了之前一直提到的解析環節。

連接-解析

解析就是將符號引用轉化爲直接引用

如果我們調用的是具體的實現類,那就是靜態解析

像我們所瞭解的多態,就是通過“後期綁定”來實現的,其實就是這裏的動態解析。比如接口或抽象類,剛開始是不知道具體實現類的,所以在運行過程中,當虛擬機調用棧得到類型信息,這時候就可以用明確的直接引用替換符號引用了,這就是動態解析。這時候解析環節就是發生在初始化之後的。

底層對應的invokedynamic這條字節碼指令。

當解析完成,整個連接步驟就完成了,這時候類就引到了程序中。

接下來是初始化階段。

初始化

初始化階段比較好理解,簡單概括就是:爲類變量初始化賦值,爲成員變量賦零值,執行靜態代碼塊

下面進入源碼階段,講一下類加載底層的一個詳細流程。

類加載底層詳細流程

  • 先執行java文件
  • 創建Java虛擬機
  • 創建引導類加載器
  1. 由C++調用Java代碼創建JVM啓動器實例Launcher
  2. Launcher類由引導類加載器加載
  3. 同時創建其他類加載器(都是Launcher下的靜態內部類)
  4. 通過ClassLoader.loadClass()加載類

雙親委派機制

默認從Application ClassLoader開始加載類。

在每個ClassLoader開始的時候,先從jvm緩存查找該類。如果緩存沒找到,然後向上委託給父類加載器進行加載,一直到Bootstrap ClassLoader。

父加載器加載失敗則向下由子加載器繼續加載。

 
  • Bootstrap ClassLoader:負責加載jre/lib目錄下的核心jar包,比如rt.jar等

  • Extension ClassLoader:負責加載jre/lib/ext目錄下的jar包

  • Application ClassLoader:負責加載項目中自己寫的類,就是加載ClassPath/類路徑下的.class字節碼文件

  • 自定義加載器:負責加載自定義路徑下的.class字節碼文件

 

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