天天看點

類加載過程

一、類的生命周期

類被加載到jvm虛拟機記憶體開始,到解除安裝出記憶體為止,他的生命周期可以分為:加載->驗證->準備->解析->初始化->使用->解除安裝。

其中驗證、準備、解析統一稱為連結階段

1、加載

  将類的位元組碼載入方法區中,内部采用 c++ 的 instanceklass 描述 java 類,它的重要 field 有:

    _java_mirror 即 java 的類鏡像,例如對 string 來說,就是 string.class,作用是把 klass 暴露給 java 使用

    _super 即父類

    _fields 即成員變量

    _methods 即方法

    _constants 即常量池

    _class_loader 即類加載器

    _vtable 虛方法表

    _itable 接口方法表

  如果這個類還有父類沒有加載,先加載父類 加載和連結可能是交替運作的

2、連結

2.1驗證

  驗證是連接配接階段的第一步,這一階段的目的是為了確定class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。驗證階段大緻會完成4個階段的檢驗動作:

  1)檔案格式驗證:驗證位元組流是否符合class檔案格式的規範;例如:是否以0xcafebabe開頭、主次版本号是否在目前虛拟機的處理範圍之内、常量池中的常量是否有不被支援的類型。

  2)中繼資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合java語言規範的要求;例如:這個類是否有父類,除了java.lang.object之外。

  3)位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。

  4)符号引用驗證:確定解析動作能正确執行。

驗證階段是非常重要的,但不是必須的,它對程式運作期沒有影響,如果所引用的類經過反複驗證,那麼可以考慮采用<code>-xverifynone</code>參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間。

2.2準備

  當完成位元組碼檔案的校驗之後,jvm 便會開始為類變量配置設定記憶體并初始化。這裡需要注意兩個關鍵點,即記憶體配置設定的對象以及初始化的類型。

記憶體配置設定的對象。java 中的變量有「類變量」和「類成員變量」兩種類型,「類變量」指的是被 static 修飾的變量,而其他所有類型的變量都屬于「類成員變量」。

在準備階段,jvm 隻會為「類變量」配置設定記憶體,而不會為「類成員變量」配置設定記憶體。「類成員變量」的記憶體配置設定需要等到初始化階段才開始。

例如下面的代碼在準備階段,隻會為 factor 屬性配置設定記憶體,而不會為 website 屬性配置設定記憶體。

初始化的類型。在準備階段,jvm 會為類變量配置設定記憶體,并為其初始化。但是這裡的初始化指的是為變量賦予 java 語言中該資料類型的零值,而不是使用者代碼裡初始化的值。

例如下面的代碼在準備階段之後,sector 的值将是 0,而不是 3。

但如果一個變量是常量(被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予使用者希望的值。例如下面的代碼在準備階段之後,number 的值将是 3,而不是 0。

2.3解析

  解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。

符号引用:簡單的了解就是字元串,比如引用一個類,java.util.arraylist 這就是一個符号引用,字元串引用的對象不一定被加載。

直接引用:指針或者位址偏移量。引用對象一定在記憶體(已經加載)。

3、初始化

  初始化,這個階段就是執行類構造器&lt; clinit &gt;()方法的過程,為類的靜态變量賦予正确的初始值,jvm負責對類進行初始化,主要對類變量進行初始化。

在java中對類變量進行初始值設定有兩種方式:

  聲明類變量是指定初始值

  使用靜态代碼塊為類變量指定初始值

jvm初始化步驟

  1)假如這個類還沒有被加載和連接配接,則程式先加載并連接配接該類

  2)假如該類的直接父類還沒有被初始化,則先初始化其直接父類

  3)假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:隻有當對類的主動使用的時候才會導緻類的初始化,類的主動使用包括以下六種:

  1)建立類的執行個體,也就是new的方式

  2)通路某個類或接口的靜态變量,或者對該靜态變量指派

  3)調用類的靜态方法

  4)反射(如class.forname(“com.shengsiyuan.test”))

  5)初始化某個類的子類,則其父類也會被初始化

  6)java虛拟機啟動時被标明為啟動類的類(java test),直接使用java.exe指令來運作某個主類

不會導緻類初始化的情況

  1)通路類的 static final 靜态常量(基本類型和字元串)不會觸發初始化

  2)類對象.class 不會觸發初始化

  3)建立該類的數組不會觸發初始化

4、使用

  當 jvm 完成初始化階段之後,jvm 便開始從入口方法開始執行使用者的程式代碼。

5、解除安裝

  當使用者程式代碼執行完畢後,jvm 便開始銷毀建立的 class 對象,最後負責運作的 jvm 也退出記憶體。

二、類加載器

1、類加載器分類

  1)啟動類加載器:bootstrap classloader,負責加載存放在jdk\jre\lib(jdk代表jdk的安裝目錄,下同)下,或被-xbootclasspath參數指定的路徑中的,并且能被虛拟機識别的類庫(如rt.jar,所有的java.開頭的類均被bootstrap classloader加載)。啟動類加載器是無法被java程式直接引用的。

  2)擴充類加載器:extension classloader,該加載器由sun.misc.launcher$extclassloader實作,它負責加載jdk\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴充類加載器。

  3)應用程式類加載器:application classloader,該類加載器由sun.misc.launcher$appclassloader來實作,它負責加載使用者類路徑(classpath)所指定的類,開發者可以直接使用該類加載器,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。

應用程式都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。

2、雙親委派

類加載過程

 類加載器加載類的源碼

 從圖中我們發現除啟動類加載器外,每個加載器都有父的類加載器。

雙親委派機制:如果一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務,就成功傳回;

隻有父類加載器無法完成此加載任務時,才自己去加載。

類加載過程

從類的繼承關系來看,extclassloader和appclassloader都是繼承urlclassloader,都是classloader的子類。而bootstrapclassloader是有c寫的,不再java的classloader子類中。

從圖中可以看到類加載器間的父子關系不是以繼承的方式實作的,而是以組合關系的方式來複用父加載器的代碼。

如果一個類加載器收到了類加載的請求,它首先會把這個請求委派給父加載器去完成,每一個層次的類加載器都是如此。

雙親委派模型的好處

  java類随着加載它的類加載器一起具備了一種帶有優先級的層次關系。比如,java中的object類,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,是以object在各種類加載環境中都是同一個類。如果不采用雙親委派模型,那麼由各個類加載器自己取加載的話,那麼系統中會存在多種不同的object類。

3、打破雙親委派

  3.1 自定義加載器重寫loadclass()方法,具體可以參考https://www.cnblogs.com/itpower/p/13211490.html

  3.2線程上下文類加載器(利用了java的spi機制)

這裡以jdbc為例來講解,我們在使用 jdbc 時,都需要加載 driver 驅動,不知道你注意到沒有,不寫class.forname("com.mysql.jdbc.driver"),也是可以讓 com.mysql.jdbc.driver 正确加載,那是怎麼做到的呢,我們看一下源碼

我們手動輸出一下drivermanager的類加載器

system.out.println(drivermanager.class.getclassloader());

列印 null,表示它的類加載器是 bootstrap classloader,會到 java_home/jre/lib 下搜尋類,但 java_home/jre/lib 下顯然沒有 mysql-connector-java-5.1.47.jar 包,

這樣問題來了,在 drivermanager 的靜态代碼塊中,怎麼能正确加載 com.mysql.jdbc.driver 呢

繼續看 loadinitialdrivers() 方法:

先看 2)發現它最後是使用 class.forname 完成類的加載和初始化,關聯的是應用程式類加載器,是以可以順利完成類加載

再看 1)它就是大名鼎鼎的 service provider interface (spi)約定如下,在 jar 包的 meta-inf/services 包下,以接口全限定名名為檔案,檔案内容是實作類名稱

類加載過程

這樣就可以使用serviceloader來得到實作類,展現的是【面向接口程式設計+解耦】的思想,在下面一些架構中都運用了此思想:

  jdbc

  servlet 初始化器

  spring 容器

  dubbo(對 spi 進行了擴充)

接着看 serviceloader.load 方法:

線程上下文類加載器是目前線程使用的類加載器,預設就是應用程式類加載器,它内部又是由 class.forname 調用了線程上下文類加載器完成類加載,具體代碼在 serviceloader 的内部類 lazyiterator 中:

 4、自定義加載器

問問自己,什麼時候需要自定義類加載器

  1)想加載非 classpath 随意路徑中的類檔案

  2)都是通過接口來使用實作,希望解耦時,常用在架構設計

  3)這些類希望予以隔離,不同應用的同名類都可以加載,不沖突,常見于 tomcat 容器

步驟:1) 繼承 classloader 父類

   2)要遵從雙親委派機制,重寫 findclass 方法,注意不是重寫 loadclass 方法,否則不會走雙親委派機制

   3)讀取類檔案的位元組碼

   4)調用父類的 defineclass 方法來加載類

   5)使用者調用該類加載器的 loadclass 方法