天天看點

透過現象看本質:Java類動态加載和熱替換

摘要:本文主要介紹類加載器、自定義類加載器及類的加載和解除安裝等内容,并舉例介紹了Java類的熱替換。

最近,遇到了兩個和Java類的加載和解除安裝相關的問題:

1) 是一道關于Java的判斷題:一個類被首次加載後,會長期留駐JVM,直到JVM退出。這個說法,是不是正确的?

2) 在開發的一個內建平台中,需要內建類似接口的多種工具,并且工具可能會有新增,同時在不同的環境部署會有裁剪(例如對外提供服務的應用,不能提供特定的采購的工具),如何才能更好地實作?

針對上面的第2點,我們采用Java插件化開發實作。上面的兩個問題,都和Java的類加載和熱替換機制有關。

類加載器,顧名思義,就是用來實作類的加載操作。每個類加載器都有一個獨立的類名稱空間,就是說每個由該類加載器加載的類,都在自己的類名稱空間,如果要比較兩個類是否“相等”,首先這兩個類必須在相同的類命名空間,即由相同的類加載器加載(即對于任何一個類,都必須由該類本身和加載它的類加載器一起确定其在JVM中的唯一性),不是同一個類加載器加載的類,不會相等。

在Java中,主要有如下的類加載器:

透過現象看本質:Java類動态加載和熱替換

圖1.1 Java類加載器

下面,簡單介紹上面這幾種類加載器:

啟動類加載器(Bootstrap Class Loader):這個類使用C++開發(所有的類加載器中,唯一使用C++開發的類加載器),用來加載<JAVA_HOME>/lib目錄中jar和tools.jar或者使用 -Xbootclasspath 參數指定的類。

擴充類加載器(Extension Class Loader):定義為misc.Launcher$ExtClassLoader,用來加載<JAVA_HOME>/lib/ext目錄或者使用java.ext.dir指定的類。

應用程式類加載器(Application Class Loader):定義為misc.Launcher$AppClassLoader,用來加載使用者類路徑下面(classpath)下面所有的類,一般情況下,該類是應用程式預設的類加載器。

使用者自定義類加載器(User Class Loader):使用者自定義類加載器,一般沒有必要,後面我們會專門來一部分介紹該類型的類加載器。

雙親委派模型,是從 Java1.2 開始引入的一種類加載器模式,在Java中,類的加載操作通過java.lang.ClassLoader中的loadClass()方法完成,咱們首先看看該方法的實作(直接從Java源碼中撈出來的):

我們結合上面的注釋,來解釋下雙親委派模型的内容:

1) 接收到一個類加載請求後,首先判斷該類是否有加載,如果已經加載,則直接傳回;

2) 如果尚未加載,首先擷取父類加載器,如果可以擷取父類加載器,則調用父類的loadClass()方法來加載該類,如果無法擷取父類加載器,則調用啟動器加載器來加載該類;

3) 判斷該類是否被父類加載器或者啟動類加載器加載,如果已經加載完成則傳回,如果未成功加載,則自己嘗試來加載該類。

上面的描述,說明了loadClass()方法的實作,我們進一步對上面的步驟進行解釋:

因為類加載器首先調父類加載器來進行加載,從loadClass()方法的實作,我們知道父類加載器會嘗試調自己的父類加載器,直到啟動類加載器,是以,任何一個類的加載,都會最終委托到啟動類加載器來首先加載;

在前面有進行介紹,啟動類加載器、擴充類加載器、應用程式類加載器,都有自己加載的類的範圍,例如啟動類加載器隻加載JDK核心庫,是以并不是父類加載器就可以都加載成功,父類加載器無法加載(一般如上面代碼,抛出來ClassNotFoundException),此時會由自己加載。

最後啰嗦一下,再進行一下總結:

雙親委派模型:如果一個類加載器收到類加載請求,會首先把加載請求委派給父類加載器完成,每個層次的類加載器都是這樣,最終所有的加載請求都傳動到最根的啟動類加載器來完成,如果父類加載器無法完成該加載請求(即自己加載的範圍内找不到該類),子類加載器才會嘗試自己加載。

這樣的雙親委派模型有個好處:就是所有的類都盡可能由頂層的類加載器加載,保證了加載的類的唯一性,如果每個類都随機由不同的類加載器加載,則類的實作關系無法保證,對于保證Java程式的穩定運作意義重大。

在Java中,每個類都有相應的Class Loader,同樣的,每個執行個體對象也會有相應的類,當滿足如下三個條件時,JVM就會解除安裝這個類:

1) 該類所有執行個體對象不可達

2) 該類的Class對象不可達

3) 該類的Class Loader不可達

那麼,上面示例對象、Class對象和類的Class Loader直接是什麼關系呢?

在類加載器的内部實作中,用一個Java集合來存放所加載類的引用。而一個Class對象總是會引用它的類加載器,調用Class對象的getClassLoader()方法,就能獲得它的類加載器。是以,Class執行個體和加載它的加載器之間為雙向引用關系。

一個類的執行個體總是引用代表這個類的Class對象。在Object類中定義了getClass()方法,這個方法傳回代表對象所屬類的Class對象的引用。此外,所有的Java類都有一個靜态屬性class,它引用代表這個類的Class對象。

Java虛拟機自帶的類加載器(前面介紹的三種類加載器)在JVM運作過程中,會始終存在,而這些類加載器則會始終引用它們所加載的類的Class對象,是以這些Class對象始終是可觸及的。是以,由Java虛拟機自帶的類加載器所加載的類,在虛拟機的生命周期中,始終不會被解除安裝。

那麼,我們是不是就完全不能在Java程式運作過程中,動态修改我們使用的類了嗎?答案是否定的!根據上面的分析,通過Java虛拟機自帶的類加載器加載的類無法解除安裝,我們可以自定義類加載器來加載Java程式,通過自定義類加載器加載的Java類,是可以被解除安裝的。

前面介紹到,類加載的雙親委派模型,是推薦模型,在loadClass中實作的,并不是必須使用的模型。我們可以通過自定義類加載器,直接加載我們需要的Java類,而不委托給父類加載器。

透過現象看本質:Java類動态加載和熱替換

圖2.1 自定義類加載器

如上圖所示,我們有自定義的類加載器MyClassLoader,用來加載類MyClass,則在JVM中,會存在上面三類引用(上圖忽略這三種類型對象對其他的對象的引用)。如果我們将左邊的三個引用變量,均設定為null,那麼此時,已經加載的MyClass将會被解除安裝。

動态解除安裝需要借助于JVM的垃圾收集功能才可以做到,但是我們知道,JVM的垃圾回收,隻有在堆記憶體占用比較高的時候,才會觸發。即使我們調用了System.gc(),也不會立即執行垃圾回收操作,而隻是告訴JVM需要執行垃圾回收,至于什麼時候垃圾回收,則要看JVM自己的垃圾回收政策。

但是我們不需要悲觀,即使動态解除安裝不是那麼牢靠,但是實作動态的Java類的熱替換還是有希望的。

下面通過代碼來介紹Java類的熱替換方法(代碼簡陋,主要為了說明問題):

如下面的代碼:

首先定義一個自定義類加載器:

上面在loadClass時,先判斷類name(包含package的全限定名)是否以java開始,如果是java開始,則使用JVM自帶的類加載器加載。

然後定義一個簡單的動态加載類:

在執行過程中,會動态修改列印内容,測試類的熱加載。

然後定義一個調用類:

當我們運作上面Main程式過程中,我們動态修改執行内容(SayHello中,從 hello zmj... 更改為 hello ping...),最終展示的内容如下:

本文主要介紹類加載器、自定義類加載器及類的加載和解除安裝等内容,并舉例介紹了Java類的熱替換實作。

其實,最近在開發項目中,需要裁剪特性,就想用pf4j來做插件化開發,了解了一些類加載機制,整理一下。

主要參考《深入Java虛拟機:JVM進階特性與最佳實踐》。

本文分享自華為雲社群《Java類動态加載和熱替換》,原文作者:maijun 。

點選關注,第一時間了解華為雲新鮮技術~

上一篇: udp伺服器