在完成了程式設計師工作中最困難的部分並編寫了“Hello World 2.0”應用程式後,剩下的就是組裝分發工具包並將其傳輸給客戶,或至少傳輸給測試服務。在發行版中,一切都按其應有的方式進行,當我們啟動程式時,Java 虛擬機器就出現了。眾所周知,虛擬機器以字節碼的形式讀取類別檔案中提供的命令,並將它們轉換為處理器的指令。我建議了解一下字節碼進入虛擬機器的方案。
類別載入器
它用於向 JVM 提供編譯後的字節碼,該字節碼通常儲存在擴展名為 的檔案中.class
,但也可以從其他來源獲取,例如透過網路下載或由應用程式本身產生。 根據Java SE規範,為了讓程式碼在JVM中運行,需要完成三個步驟:
-
從資源載入字節碼並建立類別的實例
Class
這包括在先前載入的類別中搜尋請求的類,取得用於載入的字節碼並檢查其正確性,建立該類別的實例(以便在
Class
運行時使用它)以及載入父類。如果父類別和介面尚未加載,則認為該類別未加載。 -
綁定(或連結)
根據規範,這個階段又分為三個階段:
- 驗證,檢查接收到的字節碼的正確性。
- 準備工作,為靜態欄位分配 RAM 並使用預設值初始化它們(在這種情況下,明確初始化(如果有)已經在初始化階段發生)。
- 解析,類型、欄位和方法的符號連結解析。
-
初始化接收到的對象
在這裡,與前面的段落不同,一切似乎都清楚應該發生什麼。當然,準確地理解這是如何發生的會很有趣。
- 在連結之前必須完全載入該類別。
- 一個類別在初始化之前必須經過充分的測試和準備。
- 連結解析錯誤在程式執行期間發生,即使它們是在連結階段偵測到的。
Java 載入器的類型
Java 中有三個標準載入器,每個載入器從特定位置載入一個類別:-
Bootstrap是一個基本的載入器,也稱為Primordial ClassLoader。
從 rt.jar 檔案載入標準 JDK 類
-
擴充類別載入器——擴充載入器。
載入擴充類,預設位於 jre/lib/ext 目錄中,但可以透過 java.ext.dirs 系統屬性設定
-
系統類別載入器——系統載入器。
載入 CLASSPATH 環境變數中定義的應用程式類
抽象類別類別載入器
除了基底類別之外,每個載入器都是抽象類別的後代java.lang.ClassLoader
。例如,擴充載入器的實作是 類sun.misc.Launcher$ExtClassLoader
,系統載入器是sun.misc.Launcher$AppClassLoader
。基本載入器是本機的,其實作包含在 JVM 中。任何擴展的類別都java.lang.ClassLoader
可以提供自己的方式來載入二十一點和這些相同的類別。為此,需要重新定義相應的方法,目前我只能粗淺地考慮一下,因為 我沒有詳細了解這個問題。他們來了:
package java.lang;
public abstract class ClassLoader {
public Class<?> loadClass(String name);
protected Class<?> loadClass(String name, boolean resolve);
protected final Class<?> findLoadedClass(String name);
public final ClassLoader getParent();
protected Class<?> findClass(String name);
protected final void resolveClass(Class<?> c);
}
loadClass(String name)
少數公共方法之一,它是載入類別的入口點。它的實作歸結為呼叫另一個受保護的方法loadClass(String name, boolean resolve)
,該方法需要被重寫。如果您查看此受保護方法的 Javadoc,您可以理解如下內容:提供兩個參數作為輸入。一種是需要載入的類別的二進位名稱(或完全限定的類別名稱)。類別名稱是用所有包的列表指定的。第二個參數是一個標誌,決定是否需要符號連結解析。預設情況下它是false,這意味著使用延遲類別載入。此外,根據文檔,在該方法的預設實現中,進行了一個調用findLoadedClass(String name)
,該調用檢查先前是否已加載該類,如果是,則返回對該類的引用。否則,將呼叫父載入器的類別載入方法。如果沒有一個載入器可以找到已載入的類,則每個載入器都會按照相反的順序嘗試查找並載入該類,從而覆蓋findClass(String name)
. 這將在「類別載入方案」一章中更詳細地討論。最後,最後但並非最不重要的一點是,在類別載入後,根據解析標誌,將決定是否透過符號連結載入類別。一個明顯的例子是,可以在類別載入階段呼叫Resolution階段。因此,透過擴展類別ClassLoader
並重寫其方法,自訂載入器可以實現自己的邏輯,以將字節碼傳遞到虛擬機器。Java 也支援「目前」類別載入器的概念。目前載入器是載入目前正在執行的類別的載入器。每個類別都知道它是用哪個載入器載入的,您可以透過呼叫其String.class.getClassLoader()
. 對於所有應用程式類,「目前」載入器通常是系統載入器。
類別載入的三個原則
-
代表團
載入類別的請求將傳遞給父載入器,並且僅當父載入器無法找到並載入該類別時,才會嘗試載入該類別本身。這種方法允許您使用盡可能接近基類的載入器來載入類別。這實現了最大的班級可見性。每個載入器都會保留其載入的類別的記錄,並將它們放置在其快取中。這些類別的集合稱為範圍。
-
能見度
載入器只能看到「它的」類別和「父類」的類,並且不知道其「子類」載入的類別。
-
獨特性
一個類別只能載入一次。委託機制確保啟動類別載入的載入器不會重載先前載入到 JVM 中的類別。
類別載入方案
當發生載入類別的呼叫時,會在目前載入器已載入的類別的快取中搜尋該類別。如果先前尚未載入所需的類,則委託原則會將控制權轉移給位於層級結構中更高一級的父載入器。父載入器也會嘗試在其快取中尋找所需的類別。如果該類別已載入且載入器知道其位置,則將Class
傳回該類別的物件。如果沒有,搜尋將繼續,直到到達基本引導程式。如果基底載入器沒有所需類別的資訊(即尚未載入),則將在給定載入器知道的類別的位置中搜尋該類別的字節碼,如果該類別無法載入後,控制權將返回到子載入器,子載入器將嘗試從已知的來源載入。如上所述,基本載入器的類別位置是 rt.jar 函式庫,擴充載入器的類別位置是 jre/lib/ext 擴充的目錄,系統類別的位置是 CLASSPATH,使用者類別的位置可以是不同的。因此,載入類別的進度是相反的方向——從根加載器到當前載入器。當找到類別的字節碼時,該類別將載入到 JVM 中並獲得該類型的實例Class
。正如您所看到的,所描述的載入方案與上述方法的實作類似loadClass(String name)
。下面你可以在圖中看到這個圖。
作為結論
在學習語言的第一步中,沒有特別需要了解Java中類別是如何載入的,但是了解這些基本原理將有助於您在遇到諸如 或 之類的錯誤時避免ClassNotFoundException
絕望NoClassDefFoundError
。好吧,或至少大致了解問題的根源是什麼。ClassNotFoundException
因此,當在程式執行期間動態載入類別時,如果載入器無法在快取中或沿著類別路徑找到所需的類,則會發生異常。但該錯誤NoClassDefFoundError
更為嚴重,當所需的類別在編譯期間可用但在程式執行期間不可見時,就會發生該錯誤。如果程式忘記包含它使用的庫,就會發生這種情況。嗯,了解您在工作中使用的工具的結構原理(不一定是清楚而詳細地深入了解)這一事實本身就可以讓您更清楚地理解該機制內部發生的過程,這在轉向,導致自信地使用這個工具。
GO TO FULL VERSION