JVM加載class文件的原理機制

Java 語言是一種具有動態性的解釋型編程語言,當指定程序運行的時候, Java 虛擬機就將編譯生成的 . class 文件按照需求和一定的規則加載進內存,並組織成爲一個完整的 Java 應用程序。 Java 語言把每個單獨的類 Class 和接口 Implements 編譯成單獨的一個 . class 文件,這些文件對於 Java 運行環境來說就是一個個可以動態加載的單元。正是因爲 Java 的這種特性,我們可以在不重新編譯其它代碼的情況下,只編譯需要修改的單元,並把修改文件編譯後的 . class 文件放到 Java 的路徑當中, 等到下次該 Java 虛擬機器重新激活時,這個邏輯上的 Java 應用程序就會因爲加載了新修改的 .class 文件,自己的功能也做了更新,這就是 Java 的動態性。

下面用一個簡單的例子讓大家對 Java 的動態加載有一個基本的認識:

class TestClassA{
public void method(){
System.out.println("Loading ClassA");
}
}

public class ClassLoaderTest {
public static void main(String args[]){
TestClassA testClassA = new TestClassA();
testClassA.method();
}
}

編譯後輸入命令: java -verbose:class ClassLoaderTest ,執行文件。
輸出結構如圖 (1) 

從運行結果我們可以看到, JRE ( JavaRuntime Environment )首先加載 ClassLoaderTest 文件,然後再加載 TestClassA 文件,從而實現了動態加載。

1. 預先加載與依需求加載

Java 運行環境爲了優化系統,提高程序的執行速度,在 JRE 運行的開始會將 Java 運行所需要的基本類採用預先加載( pre-loading )的方法全部加載要內存當中,因爲這些單元在 Java 程序運行的過程當中經常要使用的,主要包括 JRE 的 rt.jar 文件裏面所有的 .class 文件。

當 java.exe 虛擬機開始運行以後,它會找到安裝在機器上的 JRE 環境,然後把控制權交給 JRE , JRE 的類加載器會將 lib 目錄下的 rt.jar 基礎類別文件庫加載進內存,這些文件是 Java 程序執行所必須的,所以系統在開始就將這些文件加載,避免以後的多次 IO 操作,從而提高程序執行效率。

圖( 2 )我們可以看到多個基礎類被加載, java.lang.Object,java.io.Serializable 等等。


圖( 2 )

相對於預先加載,我們在程序中需要使用自己定義的類的時候就要使用依需求加載方法( load-on-demand ),就是在 Java 程序需要用到的時候再加載,以減少內存的消耗,因爲 Java 語言的設計初衷就是面向嵌入式領域的。

在這裏還有一點需要說明的是, JRE 的依需求加載究竟是在什麼時候把類加載進入內部的呢?

我們在定義一個類實例的時候,比如 TestClassA testClassA ,這個時候 testClassA 的值爲 null ,也就是說還沒有初始化,沒有調用 TestClassA 的構造函數,只有當執行 testClassA = new TestClassA() 以後, JRE 才正真把 TestClassA 加載進來。

2. 隱式加載和顯示加載

Java 的加載方式分爲隱式加載( implicit )和顯示加載( explicit ),上面的例子中就是用的隱式加載的方式。所謂隱式加載就是我們在程序中用 new 關鍵字來定義一個實例變量, JRE 在執行到 new 關鍵字的時候就會把對應的實例類加載進入內存。隱式加載的方法很常見,用的也很多, JRE 系統在後臺自動的幫助用戶加載,減少了用戶的工作量,也增加了系統的安全性和程序的可讀性。

相對於隱式加載的就是我們不經常用到的顯示加載。所謂顯示加載就是有程序員自己寫程序把需要的類加載到內存當中,下面我們看一段程序:

class TestClass{
public void method(){
System.out.println("TestClass-method");

}
}

public class CLTest {
public static void main(String args[]) {
try{
Class c = Class.forName("TestClass");
TestClass object = (TestClass)c.newInstance();
object.method();
}catch(Exception e){
e.printStackTrace();
}
}
}

我們通過 Class 類的 forName (String s) 方法把自定義類 TestClass 加載進來,並通過 newInstance ()方法把實例初始化。事實上 Class 類還很多的功能,這裏就不細講了,有興趣的可以參考 JDK 文檔。

Class 的 forName() 方法還有另外一種形式: Class forName(String s, boolean flag, ClassLoader classloader) , s 表示需要加載類的名稱, flag 表示在調用該函數加載類的時候是否初始化靜態區, classloader 表示加載該類所需的加載器。

forName (String s) 是默認通過 ClassLoader.getCallerClassLoader() 調用類加載器的,但是該方法是私有方法,我們無法調用,如果我們想使用 Class forName(String s, boolean flag, ClassLoader classloader) 來加載類的話,就必須要指定類加載器,可以通過如下的方式來實現:

Test test = new Test();//Test 類爲自定義的一個測試類;
ClassLoader cl = test. getClass().getClassLoader();
// 獲取 test 的類裝載器;
Class c = Class.forName("TestClass", true, cl);

因爲一個類要加載就必需要有加載器,這裏我們是通過獲取加載 Test 類的加載器 cl 當作加載 TestClass 的類加載器來實現加載的。



3. 自定義類加載機制

之前我們都是調用系統的類加載器來實現加載的,其實我們是可以自己定義類加載器的。利用 Java 提供的 java.net.URLClassLoader 類就可以實現。下面我們看一段範例:
try{
URL url = new URL("file:/d:/test/lib/");
URLClassLoader urlCL = new URLClassLoader(new URL[]{url});
Class c = urlCL.loadClass("TestClassA");
TestClassA object = (TestClassA)c.newInstance();
object.method();
}catch(Exception e){
e.printStackTrace();
}

我們通過自定義的類加載器實現了 TestClassA 類的加載並調用 method ()方法。分析一下這個程序:首先定義 URL 指定類加載器從何處加載類, URL 可以指向網際網絡上的任何位置,也可以指向我們計算機裏的文件系統 ( 包含 JAR 文件 ) 。上述範例當中我們從 file:/d:/test/lib/ 處尋找類;然後定義 URLClassLoader 來加載所需的類,最後即可使用該實例了。



4. 類加載器的階層體系

討論了這麼多以後,接下來我們仔細研究一下 Java 的類加載器的工作原理:

當執行 java ***.class 的時候, java.exe 會幫助我們找到 JRE ,接着找到位於 JRE 內部的 jvm.dll ,這纔是真正的 Java 虛擬機器 , 最後加載動態庫,激活 Java 虛擬機器。虛擬機器激活以後,會先做一些初始化的動作,比如說讀取系統參數等。一旦初始化動作完成之後,就會產生第一個類加載器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰寫而成,這個 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化動作之外,最重要的就是加載 Launcher.java 之中的 ExtClassLoader ,並設定其 Parent 爲 null ,代表其父加載器爲 BootstrapLoader 。然後 Bootstrap Loader 再要求加載 Launcher.java 之中的 AppClassLoader ,並設定其 Parent 爲之前產生的 ExtClassLoader 實體。這兩個加載器都是以靜態類的形式存在的。這裏要請大家注意的是, Launcher$ExtClassLoader.class 與 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加載,所以 Parent 和由哪個類加載器加載沒有關係。

下面的圖形可以表示三者之間的關係:

父類

父類

載入

載入

BootstrapLoader

PARENT

AppClassLoader

PARENT

ExtClassLoader

這三個加載器就構成我們的 Java 類加載體系。他們分別從以下的路徑尋找程序所需要的類:

BootstrapLoader : sun.boot.class.path

ExtClassLoader: java.ext.dirs

AppClassLoader: java.class.path

這三個系統參量可以通過 System.getProperty() 函數得到具體對應的路徑。大家可以自己編程實現查看具體的路徑。



5. 總結

瞭解 Java 的類加載機制對我們熟練靈活運用 Java 語言,提高程序的運行效率有着非常重要的作用,知其然也要知其所以然,這樣才能從整體提高程序的質量。


以上是個人爲了畢業要發表的一篇論文,沒有什麼深度,下面再繼續討論一點關於ClassLoader的一定東東:

public class ClassLoaderTest1{
private ClassLoaderTest2 test = null;
public ClassLoaderTest1(){
test = new ClassLoaderTest2();
}
public void method(){
System.out.println("Loading ClassA");
}
}


class ClassLoaderTest2{
public ClassLoaderTest2(){

}
public void method(){
System.out.println("Loading ClassA");
}
}

測試程序:
URL url = null;
try {
url = new URL("file:/E:/JAVA/MyProject/string/");
} catch (MalformedURLException e) {
e.printStackTrace();
}
URLClassLoader cl = new URLClassLoader(new URL[]{url});
URLClassLoader cl1 = new URLClassLoader(new URL[]{url});
try {
Class tempClass = cl.loadClass("ClassLoaderTest1");
Class tempClass2 = cl.loadClass("ClassLoaderTest2");
Object test = tempClass.newInstance();
System.out.println(tempClass.getClassLoader());
System.out.println(tempClass2.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}

當ClassLoaderTest1,ClassLoaderTest2在當前目錄和E:/JAVA/MyProject/string/都存在的時候輸出爲sun.misc.Launcher$AppClassLoader@1050169
sun.misc.Launcher$AppClassLoader@1050169
即都是被AppClassLoader加載的, 即使在E:/JAVA/MyProject/string/下面也存在.

當ClassLoaderTest1,ClassLoaderTest2只在E:/JAVA/MyProject/string/下存在的時候輸出爲
java.net.URLClassLoader@480457
java.net.URLClassLoader@1a7bf11
即都是被自定義的加載器加載的,並且也可以Object test = tempClass.newInstance();

下面一的是最關鍵的,因爲ClassLoaderTest1需要用到ClassLoaderTest2,如果ClassLoaderTest2被AppClassLoader加載,而ClassLoaderTest1是被自定義的類加載器加載,就會出現如下錯誤:

java.lang.IllegalAccessError: tried to access class ClassLoaderTest2 from class ClassLoaderTest1
at ClassLoaderTest1. <init>(ClassLoaderTest1.java:6)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:274)
at java.lang.Class.newInstance0(Class.java:308)
at java.lang.Class.newInstance(Class.java:261)
at ClassLoaderTest.main(ClassLoaderTest.java:43)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章