Java虛擬機從入門到入土之JVM的類加載機制

轉載於:https://juejin.im/post/5e1aaf626fb9a0301d11ac8e

JVM總體概述

JVM總體上是由

  • 類裝載子系統(ClassLoader)
  • 運行時數據區
  • 執行引擎
  • 內存回收
  • 類文件結構

以上5個部分組成,每一個都是非常重要的,如果你要了解JVM,要學習JVM調優,那麼只能是一個個去把他們啃了

什麼是類加載機制

書上的原話:

虛擬機把描述類的數據從Class文件加載到內存,並對這些數據進行校驗,轉換 解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制

類加載的時機

類從被加載到虛擬機內存中開始,到卸載出內存為止,它的生命週期包括

  • 加載
  • 驗證
  • 準備
  • 解析
  • 初始化
  • 使用
  • 卸載

總共是7個階段

Java虛擬機從入門到入土之JVM的類加載機制

理解類加載三個字

首先 類 是指的.Class文件類,那麼怎麼生成這個文件呢?

  • Java代碼編譯
  • 原本就是.Class 文件
  • 動態代理生成

等等 還有很多

那麼 加載 這2個字應該怎麼理解呢 大家可以看下圖

Java虛擬機從入門到入土之JVM的類加載機制


本地的.Class文件通過類加載器加載到JVM內存中的方法區裡面,然後通過這個對象來訪問數據區的數據

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流代表的靜態存儲結構轉化成方法區的二進制字節流
  3. 再內存的方法區生成這個類的Java.lang.Class對象,作為這個類各個數據訪問的入口

五種必須初始化的情況

Java並沒用規定生命時候進行類加載的第一階段,但是對於初始化階段,虛擬機有嚴格的規範

  • 遇到new 關鍵字的時候
  • 使用reflect包的方法的時候
  • 當初始化一個類的時候發現父類還沒初始化,必須先初始化父類
  • 當虛擬機啟動的時候,加載main方法的類
  • 當使用1.7的動態語言支持的時候(這塊沒有接觸過,有沒有大佬懂的)

驗證階段

分為以下幾種樣裝情況

  • 文件格式的驗證,驗證當前字節流是否能被JVM識別
  • 元數據的驗證,驗證它的父類,它的繼承,是否是抽象類等
  • 字節碼驗證,驗證邏輯是否合理
  • 符合引用的驗證 驗證是否能通過生成的Class對象找到對應的數據

準備階段

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:

1、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。

2、這裡所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

假設一個類變量的定義為:

public static int value = 3;

那麼變量value在準備階段過後的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯後,存放於類構造器()方法之中的,所以把value賦值為3的動作將在初始化階段才會執行。

下表列出了Java中所有基本數據類型以及reference類型的默認零值:

Java虛擬機從入門到入土之JVM的類加載機制

這裡還需要注意如下幾點:

  • 對基本數據類型來說,對於類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值,而- 對於局部變量來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
  • 對於同時被static和final修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予默認零值。
  • 對於引用數據類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予默認的零值,即null。
  • 如果在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。

如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化為ConstValue屬性所指定的值。

解析階段

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程 ##初始化階段 到了初始化階段,才真正開始執行類中定義的Java代碼

類加載器

站在Java虛擬機的角度來講,只存在兩種不同的類加載器:

  • 啟動類加載器:它使用C++實現(這裡僅限於Hotspot,也就是JDK1.5之後默認的虛擬機,有很多其他的虛擬機是用Java語言實現的),是虛擬機自身的一部分。
  • 所有其他的類加載器:這些類加載器都由Java語言實現,獨立於虛擬機之外,並且全部繼承自抽象類java.lang.ClassLoader,這些類加載器需要由啟動類加載器加載到內存中之後才能去加載其他的類。

站在Java開發人員的角度來看,類加載器可以大致劃分為以下四類:

  • 啟動類加載器 (C實現)
  • 擴展類加載器 (ClassLoader)
  • 應用程序加載器 (ClassLoader)
  • 自定義加載器 (ClassLoader)

這幾種類加載器的層次關係如下圖所示:

Java虛擬機從入門到入土之JVM的類加載機制

雙親委派模型

類加載器之間的這種層次關係叫做雙親委派模型。 雙親委派模型要求除了頂層的啟動類加載器(Bootstrap ClassLoader)外,其餘的類加載器都應當有自己的父類加載器。這裡的類加載器之間的父子關係一般不是以繼承關係實現的,而是用組合實現的。

雙親委派模型的工作過程

由我來概況就是 八個字 向上檢查,從下加載 如果一個類接受到類加載請求,他自己不會去加載這個請求,而是將這個類加載請求委派給父類加載器,這樣一層一層傳送,直到到達啟動類加載器(Bootstrap ClassLoader)。 只有當父類加載器無法加載這個請求時,子加載器才會嘗試自己去加載。

雙親委派模型的代碼實現

雙親委派模型的代碼實現集中在java.lang.ClassLoader的loadClass()方法當中。

  • 首先檢查類是否被加載,沒有則調用父類加載器的loadClass()方法;
  • 若父類加載器為空,則默認使用啟動類加載器作為父加載器;
  • 若父類加載失敗,拋出ClassNotFoundException 異常後,再調用自己的findClass() 方法。 loadClass源代碼如下:
<code>protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//1 首先檢查類是否被加載
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {

//2 沒有則調用父類加載器的loadClass()方法;
c = parent.loadClass(name, false);
} else {
//3 若父類加載器為空,則默認使用啟動類加載器作為父加載器;
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//4 若父類加載失敗,拋出ClassNotFoundException 異常後,這個方法就是加載的核心代碼
c = findClass(name);
}
}
if (resolve) {
//5 再調用自己的findClass() 方法。
resolveClass(c);
}
return c;
}/<code>

自定義類加載器

<code>    class NetworkClassLoader extends ClassLoader {
* String host;
* int port;
*
* public Class findClass(String name) {
* byte[] b = loadClassData(name);
* return defineClass(name, b, 0, b.length);
* }
*
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}/<code>

這個就是官方的例子


Java虛擬機從入門到入土之JVM的類加載機制


破環雙親委派

雙親委派模型很好的解決了各個類加載器加載基礎類的統一性問題。即越基礎的類由越上層的加載器進行加載。 若加載的基礎類中需要回調用戶代碼,而這時頂層的類加載器無法識別這些用戶代碼,怎麼辦呢?這時就需要破壞雙親委派模型了。

java默認的線程上下文類加載器是系統類加載器(AppClassLoader).

<code>// Now create the class loader to use to launch the application    
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader" );
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader); /<code>

以上代碼摘自sun.misc.Launch的無參構造函數Launch()。

使用線程上下文類加載器,可以在執行線程中,拋棄雙親委派加載鏈模式,使用線程上下文裡的類加載器加載類.

典型的例子有,通過線程上下文來加載第三方庫jndi實現,而不依賴於雙親委派.

大部分java app服務器(jboss, tomcat..)也是採用contextClassLoader來處理web服務。

Java虛擬機從入門到入土之JVM的類加載機制

結尾

今天把類加載機制好好講了一下,這樣大家就更加的熟悉了內的加載過程,對於Java開發是有好處的


分享到:


相關文章: