java(Android)類加載機制

1、Java的類加載過程

加載、鏈接(驗證、準備、解析)、初始化

加載就是把class文件字節碼加載進jvm內存,變成Class對象。驗證class字節流中包含的信息是jvm需要且有效的。準備是給類變量分配內存並設置初始化值。解析是把符號引用變成直接引用。初始化就是執行靜態初始化器(靜態代碼塊)和靜態變量初始化。

2、有幾種類加載器,它們有什麼不同?

啓動(Bootstrap)類加載器、擴展(Extension)類加載器和系統(System)加載器(也成應用加載器)

啓動加載器用於加載jvm需要用到的類,是C++實現的,加載<JAVA_HOME>/lib路徑下的核心類庫或者-Xbootclasspath參數指定的路徑下的jar包。

擴展類加載器,是java實現的,是Launcher的靜態內部類sun.misc.Launcher$ExtClassLoader ,它負責加載<JAVA_HOME>/lib/ext目錄下或由系統變量-Djava.ext.dir指定路徑中的庫。

系統類加載器,也是Launcher的靜態內部類sun.misc.Launcher$AppClassLoader,它負責加載系統路徑java -classpath或者-D java.class.path指定路徑下的類庫。一般情況下該類加載器是程序中默認的類加載器,通過ClassLoader.getSystemClassLoader()方法可以獲取該類加載器。

所以,我們知道,不同類加載器都有自己特定的加載路徑,而且只能加載自己特定路徑下的類。

3、有三種類加載器,jvm是怎麼保證一個類只加載一次的?

先明確一下概念,每個類加載器都有自己的類名稱空間,也就是說同一個class文件,不同的類加載器加載會生成不同的Class對象。也就是說,Class對象要相等,(1)類的全限定名是相同的(2)加載類是相同的

所以,爲了保證一個class文件只加載一次只生成一個Class對象,class文件只能讓一個類加載器加載,jvm使用了“雙親委派模式”來保證。

考慮到安全因素,我們試想一下,如果不使用這種委託模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委託的方式,就可以避免這種情況,因爲String 已經在啓動時就被引導類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變 JDK 中 ClassLoader 搜索類的默認算法。

首先,用類全限定名查詢類是否被加載過,沒有加載過,先嚐試用父加載器加載,遞歸加載,如果父加載器不能加載就自己加載。

這裏的父加載器不是繼承關係而是組合關係。自定義類加載器的父加載器是應用類加載器,應用類加載器的父加載器是擴展類加載器,擴展類加載器沒有父加載器。

看看java的類的繼承實現

4、雙親委派模型確實保證了了Class對象唯一性的問題,可是有問題是:目前只有啓動類加載器加載的接口,可是,需要用到這個接口實現類中的方法,實現類是第三方實現的,啓動類加載器是不能加載的,因爲不在啓動類加載器的加載目錄下,這個時候就得用到線程上下文類加載器了。

5、自定義類加載器

自定義類加載器是很重要的技能,自定義類加載器是爲了加載指定目錄下的類,而此時上面說的三個類加載器都失效了。最典型的使用就是Android中的類加載器 PathClassLoader和DexClassLoader,都是繼承自BaseDexClassLoader(extends ClassLoader)。PathClassLoader是加載系統類和安裝應用自己的類的,我們一般使用DexClassLoader,自定義類加載器也是需要滿足“雙親委派模式”的,不要重寫loadClass,而是要重寫findClass。具體看文章Android類加載機制的細枝末節

6、簡述一下android的類加載機制

android中有兩個類加載器,PathClassLoader和DexClassLoader,這兩個類都繼承自BaseDexClassLoader,Android中的類加載器屬於java中的自定義類加載器,所以得滿足java 類記載器的規則。PathClassLoader是用於加載系統類和應用安裝的類的,(應用安裝的時候會對類進行優化的,所以,安裝的類和網絡上下載的類是不一樣的),DexClassLoader用於加載jar、apk、zip和dex文件,執行未被應用安裝過的代碼。

dex文件被加載後進行優化,並且會存起來,BaseDexClassLoader構造函數的第二個參數optimizedDirectory指定了存放目錄。PathClassLoader是用於加載執行應用安裝過的代碼的,這些代碼在安裝的時候已經被優化過了,存放目錄是/data/dalvik-cache/,所以PathClassLoader調用父類BaseDexClassLoader構造函數的時候,optimizedDirectory是null。

PathClassLoader是系統使用的,我們不能用,我們可以使用DexClassLoader,主要工作還是在BaseDexClassLoader裏面,BaseDexClassLoader構造的時候,就會把dex進行優化放到optimizedDirectory目錄下(dexElements),並且會獲取到系統的so和包裏的so(nativeLibraryDirectories ),然後加載指定類的時候調用loadClass,也是雙親委派模型(DexClassLoader->PathClassLoader->BootClassLoader),首次加載的話,最終會調用DexClassLoader的findClass加載,也就是BaseDexClassLoader的findClass方法,它會從dexElements中加載。

7、

我們知道類的加載過程是分爲七步的,但是

(1)並不一定,只要加載類,七步都要一起做完,這是需要注意的。

什麼情況需要開始類加載過程的第一階段:加載?Java虛擬機規範中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。但是對於初始化階段,虛擬機規範則嚴格規定了以下幾種情況必須立即對類進行初始化,如果類沒有進行過初始化,則需要先觸發其初始化。

虛擬機規範嚴格規定了有且只有5中情況(jdk1.7)必須對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

  1. 遇到 new, getstatic, putstatic, invokestatic 這些字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用jdk1.7動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果REF_getstatic, REF_putstatic, REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。

 

有個概念是要求類需要初始化,我們可以把這個稱爲對類的“主動引用”,其它不觸發類的初始化,我稱爲對類的“被動引用”

從一道面試題來認識java類加載時機與過程

這篇文章中有被動引用的例子

 

(2)這七步,有些步驟順序可以調整,有些步驟是交叉在一起完成的。

看下面的紅色文字,理解一下,加載和驗證是怎麼交叉在一起的

 “加載”(Loading)階段是“類加載”(Class Loading)過程的第一個階段,在此階段,虛擬機需要完成以下三件事情:

  • 通過全限定類名獲取類的二進制字節流(不管文件的來源,網絡也好,磁盤文件或壓縮包也罷,只要能讀入二進制流就可以);
  • 將讀取的字節流代表的靜態存儲結構轉化爲方法區的運行時數據結構;
  • 在內存中(並沒有明確規定是在堆中,HotSpot中Class對象雖然也是對象,但是卻是存儲在方法區中的)生成一個代表此類的java.lang.Class對象,作爲方法區這個類的各種信息的入口。

加載階段即可以使用系統提供的類加載器在完成,也可以由用戶自定義的類加載器來完成。加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始。

1、文件格式驗證,是要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。如驗證魔數是否0xCAFEBABE;主、次版本號是否正在當前虛擬機處理範圍之內;常量池的常量中是否有不被支持的常量類型……該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區中,經過這個階段的驗證後,字節流纔會進入內存的方法區中存儲,所以後面的三個驗證階段都是基於方法區的存儲結構進行的。

(3)類初始化是類加載過程的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

 

8、一個問題,父類是什麼時候被加載的呢?

這個問題,本身問的就不是很專業,應該是父類是在什麼時候被初始化的。加載只是一個類加載過程中的第一步,我理解,由於虛擬機規範沒有對類加載的時機做定義,是由虛擬機的具體實現決定的,我覺得可能在加載子類的時候,也可能是在需要初始化父類的時候。

那父類是什麼時候被初始化的呢?虛擬機規範對初始化時機是有規定的,其中一個時機就是子類初始化的時候,父類一定要先初始化。

初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的。

 

有一個概念是類構造器<clinit>(),這個是虛擬機幫我們生成的,下面的描述參考Java類加載過程

<clinit>() <init>()

 

 

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的。


第6點,就是爲什麼可以使用靜態內部類實現單例的理論基礎。設計模式之單例模式

  1. <clinit>()方法是由編譯器自動收集類中靜態變量的賦值語句以及靜態代碼塊中定義的語句合併產生的,且內部的語句的順序是由定義的順序決定的,後面的語句可以訪問前面定義的變量,反過來可以賦值,但是不能訪問。
  2. <clinit>()<init>()是不同的,前者對應的是類,後者對應的是實例,即前一個是Class的構造器(是編譯器生成的),後者是實例對象的構造器(也就是我們定義或繼承的構造函數)。且虛擬機會保證子類的<clinit>()執行之前,其父類的<clinit>()一定執行完成,無需顯式指定。所以第一個執行<clinit>()的類是java.lang.Object;
  3. 因爲父類比子類先執行<clinit>(),所以父類的靜態變量和靜態代碼塊是先於子類執行的。
  4. 如果一個類或者接口中沒有靜態變量或靜態代碼塊,編譯器可以不生成<clinit>()
  5. 接口中沒有靜態代碼塊,但是可以有靜態變量。所以可以有<clinit>()的初始化動作,但是接口和類不同之處在於接口不需要先執行父接口的<clinit>()方法。
  6. 虛擬機會保證一個類的<clinit>()方法在多線程環境中能被正確的同步,利用這一點可以實現線程安全的單例模式。

9、被動引用

常量(static final)在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
 

 

 

 

 

深入理解Java類加載器(ClassLoader)

Android 類加載機制及熱修復原理

兩道面試題,帶你解析Java類加載機制

Java類加載機制的七個階段,加載、驗證、準備、解析、初始化、使用、卸載

從一道面試題來認識java類加載時機與過程

迴歸Java基礎:觸發類加載的六大時機,你知道幾個?

Java類加載過程

java類加載的時機和觸發類的初始化的條件

JVM常量池淺析

JVM中的直接引用和符號引用

淺析 JVM 中的符號引用與直接引用

雙親委派模型,類的加載機制,搞定大廠高頻面試題​​​​​​​

 

 

 

 

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