爲了弄懂Retrofit源碼,我把Java從底層擼了一遍

事情是這樣的:

最近在研究Retrofit,相信讀過它源碼的朋友都知道,裏面涉及了大量的反射和註解的調用,尤其是在請求建立的時候 ,使用了Java的動態代理方法,Proxy.nexInstance,由於之前在反射應用這塊比較少,就本着打破砂鍋問到底的態度查了一下反射的工作原理,爲什麼進去的時候是一個類,出來的時候就可以運行了?然後我又查到了,要了解反射的工作原理,及需要知道虛擬機類的加載機制,而類的加載機制當中,雙親委派模型和類加載機制的工作過程又是繞不過去的一環。

所以,我一咬牙,一跺腳,決定翻出藍寶書——《深入理解Java虛擬機》,花了整個週末的時間,把Java的類加載機制進行了深入的研究,同時我還發現,類加載機制還被大量的應用在了Android的熱修復領域,所以也就有了今天的這篇文章,希望能夠通過我的分享,傳播一些Java虛擬機類加載機制方面的知識,如果能幫助到大家,那真是我的榮幸了。

Retrofit源碼解析部分我打算從這一篇文章開始分爲幾個部分,分別進行分享,來達到從表面看本質的效果,接下來,大家就期待我更多的作品問世吧

Java的類加載機制:

Java類加載機制流程:

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

初始化階段,虛擬機規範嚴格限定了只有5種情況必須立即對類進行“初始化”(加載、驗證、準備要在此之前開始)。初始化一個類時,其靜態代碼區的代碼只會被執行一次(包括創建的對象被回收後,再次創建的情形)

  1. 遇到new、getstatic、putstatic、invokestatic這4條字節碼指令時候,如果類沒有初始化,需要先對其進行初始化。典型場景是使用new創建對象時,讀取或者設置類的靜態字段、調用一個類的靜態方法時。
  2. 使用java.lang.reflect包的時候對類進行反射調用時候,如果沒有進行過初始化,則需要先觸發其進行初始化,也就是說,我們調用了Proxy.newInstance的時候,其實對應的類已經初始化了。
  3. 當初始化一個類的時候,用戶需要指定一個要執行的主類(即包含main方法的那個類),虛擬機會先初始化這個主類
  4. 當初始化一個類時,發現其父類還沒有被初始化,那麼就會先初始化其父類。
  5. 在使用JDK1.7時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果是REF_getStatic ,REF_putStatic,REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其進行初始化。

 

Java中類的加載過程:

在類的加載過程當中,JVM大致需要完成以下3步動作:

  1. 通過類的全限定名獲取此類的二進制字節流
  2. 將這個類的二進制字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生成一個代表這個類的Class對象,作爲方法區這個類的各種數據存取的入口

 

而java最牛逼的地方就是在第一點,他沒有規定這個類到底應該來自哪裏,只要能夠通過全限定名能定位到就可以,並且這個過程是放到虛擬機之外去進行的,也就是說,用戶可以自由地實現類的加載方式,這就給我們留下了很多的想象空間。

舉個例子:這個類就像一個滿世界逛悠的人一樣,只要我能定位到他,不管是通過手機GSM,4G,5G,哪怕是衛星通信,GPS。。。。。。只要能定位到他個人就可以!!!

然後,高潮來了!來自全世界各地腦洞大開的程序員可是把這一條玩出了花樣,像在android領域的熱修復技術,就是利用了這點,在類的加載階段(可能是本地獲取,也可能是通過網絡獲取)去改寫類的加載方式,實現了熱啓動下的bug修復,可謂強悍!

在實際的應用過程當中,就是通過override一個類加載器的loadClass()方法。


連接階段之1——驗證階段

驗證階段是連接階段的第一步,也是非常重要的一步。其目的就是爲了確保運行在虛擬機上的程序是符合虛擬機要求的,並且不會威脅到虛擬機安全。

驗證階段大致會依次完成以下四個動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證

對比來說,使用純粹的JAVA語言來編寫的程序是安全的,因爲Java本身是不允許數組跨邊界訪問、強制轉換一個未定義的類型等等異常情況的。但我們知道,虛擬機上跑的源程序是可以由任何一個程序轉化成的字節碼文件轉化而來,如果不對字節碼文件進行檢查的話,就可能對虛擬機造成嚴重的影響,甚至導致虛擬機的崩潰。

關於虛擬機的驗證問題,由於檢查規則太多,大家可以去參考《Java虛擬機規範(Java SE7)》。

對於虛擬機的類加載機制來說,驗證階段是一個非常重要,但非必須的過程,注意這句話!因爲我們可以利用這點來優化虛擬機的類加載效率。

如果所運行的全部代碼都已經被反覆使用和驗證過,那麼在線上運行階段或者實施階段我們就可以考慮使用 -Xverify:none參數來關閉大部分的類驗證措施,來縮短虛擬機的驗證過程,進而提高類加載時間。

 

連接階段之2——準備階段

準備階段是正式爲變量分配內存並設置變量的初始值階段,這些變量的內存分配是在方法區中進行的。

這塊需要有兩個比較重要的概念區分一下:

這時候進行內存分配的只包括static類型的變量,而不包括實例變量,實例變量將會在初始化的時候進行值分配。舉個例子:

public static int a = 123;

這個a值在準備階段過後的初始值爲0而不是123.

但凡是都有例外,例如下面這句:

public static final int a = 123;

這句就是因爲在類字段的字段屬性中存在constant value屬性,所以在準備階段就會被賦值爲123。

簡單的說,如果有非final修飾的變量,賦虛擬機中的默認值。有final修飾的變量,則在準備階段直接賦值。

 

連接階段之3——解析階段

類的解析過程簡單的說就是將常量池內的符號引用替換爲直接引用的過程。那麼問題來了,什麼是符號引用,什麼是直接引用?

符號引用,簡單的說就是用符號代表的引用目標,例如a = 50;其實是用a這個符號指向了存有50值的內存空間。那什麼是直接引用呢?例如0xabc = 50;其中,0xabc指代的就是內存中的地址,只不過是在符號引用過程當中,使用a字符對內存地址進行了替換。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符7類符號引用進行

 

初始化:

初始化過程是類加載過程的最後一個階段,在前面的加載及初始化過程中,除了加載過程用戶可以干預以外,其他的步驟都是由虛擬機來主導和控制的。

到了初始化階段,纔開始真正的執行類中定義的Java代碼或者說開始執行程序員在類中定義的變量賦值。

這個過程是通過<clinit>()方法來進行處理的。

<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在程序當中出現的順序所決定的,在靜態語句塊處理的過程當中,靜態語句塊只能訪問定義在它之前的變量,定義在它之後的變量,可以進行賦值,但不能訪問。

<clinit>()方法與構造函數不同,由於虛擬機的特性,虛擬機會保證子類的<clinit>()執行,之前,父類的<clinit>()已經被執行,所以通過這個規定我們可以猜到,第一個被執行<clinit>()語句的肯定是Object。

結合剛剛說到的兩點,我們是不是可以得出,父類的靜態語句執行肯定要先於子類的靜態語句塊執行這樣的結論?

<clinit>()方法對於類或者接口來說不是必須的,因爲如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼虛擬機也不會爲這個類生成<clinit>()。但有一點需要注意的是,如果是接口有賦值的情況下,虛擬機仍然會正常生成<clinit>(),但不會執行其父類的<clinit>()。

最後,虛擬機內部會保證<clinit>()方法在多線程情景下的線程安全。

 

好了,經歷了前面的加載、鏈接、初始化階段之後,讓我們回過頭來再看看類的加載過程,我們知道,類的加載過程如果想要實現一些個性化操作的話是需要我們去override一個類加載器的loadClass方法的,那麼我們如何能保證自己load出來的類和虛擬機正常加載出來的類一致呢?

可以說,類的一致性需要類加載器和類本身來一同確定,每一個類加載器都有一個獨立的類名稱空間。也就是說,兩個類是否“相等”的前提是兩個類由同一個類加載器進行加載的。

 

雙親委派模型:

從Java虛擬機的角度來看,類加載器主要分爲兩類:一種是啓動 類加載器:Bootstrap Classloader,這個類加載器用c++語言實現,另一種就是所有除啓動類加載器之外的類加載器,這些類加載器都由Java來實現,並且都繼承自ClassLoader。

從Java開發者的角度來看,後一種類加載器又可以分爲兩種:

擴展類加載器(Extension ClassLoader):它由sun.misc.Launcher$ExtClassLoader實現,負責加載<JAVA_HOME>\lib\ext目錄中的,或者是被java.ext.dirs系統變量指定的路徑中的所有類庫,並且由開發者可以直接擴展使用

應用程序類加載器(Application ClassLoader):它由sun.misc.Launcher$AppClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般也稱它爲系統類加載器,來負責加載用戶類路徑classpath上所指定的類庫,也是可以由開發者來使用的,並且在默認情況下,它也是默認的系統類加載器

我們的app在運行的時候都是這三種類加載器配合運行的,並且在必要的時候還可以自定義類加載器來加入到類加載過程當中,是時候祭出雙親委派模型的經典畫面了

 

 

 

這個模型的工作過程是,如果一個子類的類加載器收到類加載請求,他首先會請求其父類的類加載器進行類加載,直到到了最頂層的類加載器不能處理的時候,他纔會通知其子類進行類加載的處理,這樣做的優勢就是,Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如:Object這個類,根據雙親委派工作模型的運行原理,Objcet類的類加載器在各種類加載器環境中都是同一個類。反之,如果沒有采用雙親委派模型來進行,處理,而是由用戶來自行決定Object類的加載規則,那麼就可能會導致一些未知異常的情況發生。

 

從上面博主的敘述中相信大家一定對類的加載機制和雙親委派機制有了一定的瞭解,那麼我們應該如何去自己實現一個類加載器呢?

 

現在一般的做法是去主動override一個findClass()方法。等等,findClass()?不是應該去override loadClass方法嗎?

 

是的,採用loadClass方法沒有錯,但是他破壞了雙親委派機制的工作鏈條,而採用findClass的優勢就在於,他是工作於一種類似的兜底策略,如果在loadClass裏面沒有找到目標類的話,就會調用findClass去尋找它,這點,我們通過源碼也可以看出來:

 

可以看到,通過loadClass方法傳入了我們需要找到的類名,如果在loadClass方法所在的類及其父類中沒有找到的話,就會去調用子類的findClass方法,這樣既可以保留雙親委派機制,又可以一定程度的擴展,例如熱部署、熱修復等等。

 

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