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)
圖(
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
和由哪個類加載器加載沒有關系。
下面的圖形可以表示三者之間的關系:
??????
?PARENT
AppClassLoader
|
這三個加載器就構成我們的
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)
所以JVM在尋找類的時候,不僅僅是根據類的名稱,而是根據類明和類的加載器一起來決定的!