首頁  >  文章  >  Java  >  深入解析Java中的Class Loader類別載入器

深入解析Java中的Class Loader類別載入器

高洛峰
高洛峰原創
2017-01-13 09:33:351597瀏覽

類別載入的過程
類別載入器的主要工作就是把類別檔案載入到JVM中。如下圖所示,其流程分為三步驟:

1.載入:定位要載入的類別文件,並將其位元組流裝載到JVM中;
2.連結:給要載入的類別分配最基本的記憶體結構保存其訊息,例如屬性,方法以及引用的類別。在該階段,該類別也處於不可用狀態;
(1)驗證:對載入的位元組流進行驗證,例如格式上的,安全方面的;
(2)記憶體分配:為該類別準備記憶體空間來表示其屬性,方法以及引用的類;
(3)解析:載入該類所引用的其它類,例如父類,實現的介面等。
3.初始化:對類別變數進行賦值。

深入解析Java中的Class Loader类加载器

類別載入器的層級
下圖虛線以上是JDK提供的幾個重要的類別載入器,詳細說明如下:

(1)Bootstrap Class Loader:當啟動包含主函數的類別時,載入至JAVA_HOME/lib目錄下或-Xbootclasspath指定目錄的jar包;
(2)Extention Class Loader:載入JAVA_HOME/lib/ext目錄下的或-Djava.ext.dirs指定目錄下的jar包。
(3)System Class Loader:載入classpath或-Djava.class.path指定目錄下的類別或jar套件。

深入解析Java中的Class Loader类加载器

要了解的是:

1.除了Bootstrap Class Loader外,其它的類別載入器都是java.lang.ClassLoader類別的子類別;
2.BaderBootClass Class Loader不是類別的子類別;實現,如果你沒有使用個人化類別載入器,那麼java.lang.String.class.getClassLoader()就為null,Extension Class Loader的父載入器也為null;
3.獲得類別載入器的幾種方式:
(1)獲得Bootstrap Class Loader:試圖獲得Bootstrap Class Loader,得到的必然是null。可以用以下方式驗證下:使用rt.jar套件內的類別物件的getClassLoader方法,例如java.lang.String.class.getClassLoader()可以得到或取得Extention Class Loader,再呼叫getParent方法取得;
(2)取得Extention Class Loader:使用JAVA_HOME/lib/ext目錄下jar包內的類別物件的getClassLoader方法或先取得System Class Loader,再透過它的getParent方法取得;
(3)取得System Class Loader:呼叫包含主函數的類別物件的getClassLoader方法或在主函式內呼叫Thread.currentThread().getContextClassLoader()或呼叫ClassLoader.getSystemClassLoader();
(4)取得User-Defined Class Loader:呼叫類別物件的getClassLoader方法或稱為Thread. currentThread().getContextClassLoader();



類加載器的操作原則
1.代理原則
2.可見性原則
3.是一個類別加載器在加載一個類別時會請求它的父加載器代理加載,父加載器也會請求它的父加載器代理加載,如下圖所示。

深入解析Java中的Class Loader类加载器為什麼要使用代理模式呢?首先這樣可以減少重複的載入一個類別。 (還有其它原因嗎?)


容易誤解的地方:

一般會以為是類加載器的代理順序是Parent First的,也就是:

1.載入一個類別時,類別載入器首先檢查自己是否已經載入了該類,如果已加載,則返回;否則請父載入器代理;
2.父載入器重複1的操作一直到Bootstrap Class Loader ;
3.如果Bootstrap Class Loader也沒有加載該類,將嘗試進行加載,加載成功則返回;如果失敗,拋出ClassNotFoundException,則由子加載器進行加載;
4.子類加載器捕捉異常後嘗試加載,如果成功則傳回,如果失敗則拋出ClassNotFoundException,直到發起載入的子類別載入器。
這種理解對Bootstrap Class Loader,Extention Class Loader,System Class Loader這些載入器是正確的,但一些個人化的載入器則不然,比如,IBM Web Sphere Portal Server實作的一些類別載入器就是Parent Last的,是子加載器首先嘗試加載,如果加載失敗才會請父加載器,這樣做的原因是:假如你期望某個版本log4j被所有應用使用,就把它放在WAS_HOME的庫裡,WAS啟動時會加載它。如果某個應用程式想要使用另一個版本的log4j,如果使用Parent First,這是無法實現的,因為父載入器裡已經載入了log4j內的類別。但如果使用Parent Last,負責載入應用程式的類別載入器會優先載入另外一個版本的log4j。


可見性原則
每個類別對類別載入器的可見性是不一樣的,如下圖所示。

擴展知識,OSGi就是利用這個特點,每一個bundle由一個單獨的類加載器加載,因此每個類加載器都可以加載某個類的一個版本,因此整個系統就可以使用一個類的多個版本。

深入解析Java中的Class Loader类加载器

<br/>

唯一性原則
每一個類別在一個載入器裡最多載入一次。

擴充知識1:準確地講,Singleton模式所指的單例指的是一組類別載入器中某個類別的物件只有一份。

擴展知識2:當一個類別可以被多個類別載入器加載,每個類別物件在各自的namespace內,對類別物件進行比較或對實例進行類型轉換時,會同時比較各自的名字空間,例如:

Klass類別被ClassLoaderA加載,假設類別物件為KlassA;同時被ClassLoaderB加載,假設類別物件為KlassB,那麼KlassA不等於KlassB。同時ClassA的實例被cast成KlassB時會拋出ClassCastException異常。
為什麼要個性化類別載入器
個人化類別載入器為Java語言增加了很多彈性,主要的用途有:

1.可以從多個地方載入類別,例如網路上,資料庫中,甚至即時的編譯原始檔取得類別檔案;
2.個人化後類別載入器可以在執行時間原則性的載入某個版本的類別檔案;
3.個人化後類別載入器可以動態卸載一些類別;
4.個性化後類別載入器可以對類別進行解密解壓縮後再載入類別。


類別的隱式和明確載入
隱式載入:當一個類別被引用,被繼承或被實例化時會被隱式載入,如果載入失敗,是拋出NoClassDefFoundError。

明確載入:使用下列方法,如果載入失敗會拋出ClassNotFoundException。

cl.loadClass(),cl是類別載入器的實例;
Class.forName(),使用目前類別的類別載入器進行載入。
類別的靜態區塊的執行
假如有如下類:

package cn.fengd; 
  
public class Dummy { 
 static { 
 System.out.println("Hi"); 
 } 
}

另建一個測試類別:

package cn.fengd; 
  
public class ClassLoaderTest { 
  
 public static void main(String[] args) throws InstantiationException, Exception { 
  
 try { 
  /* 
  * Different ways of loading. 
  */
  Class c = ClassLoaderTest.class.getClassLoader().loadClass("cn.fengd.Dummy"); 
 } catch (Exception e) { 
  // TODO Auto-generated catch block 
  e.printStackTrace(); 
 } 
 } 
  
}

運作後效果如何呢?

不會輸出Hi。由此可見使用loadClass後Class類別物件並沒有初始化;

如果在Load語句後加上c.newInstance(); 就會有Hi輸出,對該類別進行實例化時才初始化類別物件。

如果換一種載入語句Class c = Class.forName("cn.fengd.Dummy", false, ClassLoader.getSystemClassLoader());

不會輸出Hi。因為參數false表示不需要初始化該類別物件;

如果在Load語句後面加上c.newInstance(); 就會有Hi輸出,而對該類別進行實例化時才初始化類別物件。

如果換成Class.forName("cn.fengd.Dummy");或new Dummy()呢?

都會輸出Hi。

常見問題分析:

1.由不同的类加载器加载的指定类型还是相同的类型吗?
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.我们可以用两个自定义类加载器去加载某自定义类型(注意,不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。

2.在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

//java.lang.Class.java
 
 publicstatic Class<?> forName(String className)throws ClassNotFoundException {
 
return forName0(className, true, ClassLoader.getCallerClassLoader());
 
}
 
//java.lang.ClassLoader.java
 
// Returns the invoker&#39;s class loader, or null if none.
 
static ClassLoader getCallerClassLoader() {
 
  // 获取调用类(caller)的类型
 
 Class caller = Reflection.getCallerClass(3);
 
  // This can be null if the VM is requesting it
 
 if (caller == null) {
 
  returnnull;
 
 }
 
 // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
 
 return caller.getClassLoader0();
 
}
 
//java.lang.Class.java
 
//虚拟机本地实现,获取当前类的类加载器
native ClassLoader getClassLoader0();

3.在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是?
在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

//摘自java.lang.ClassLoader.java
protected ClassLoader() {
 
  SecurityManager security = System.getSecurityManager();
 
  if (security != null) {
 
  security.checkCreateClassLoader();
 
  }
 
  this.parent = getSystemClassLoader();
 
  initialized = true;
 
}

我们再来看一下对应的getSystemClassLoader()方法的实现:

privatestaticsynchronizedvoid initSystemClassLoader() {
 
  //...
 
  sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
 
  scl = l.getClassLoader();
 
  //...
 
}

我们可以写简单的测试代码来测试一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

本机对应输出如下:

sun.misc.Launcher$AppClassLoader@197d257

所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:

即时用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

(1)6e5475ece90bdb5cedfc1a16b6632d24/lib下的类

(2)9da487fed862e2e75b98522999bd48b8/lib/ext下或者由系统变量java.ext.dir指定位置中的类

(3)当前工程类路径下或者由系统变量java.class.path指定位置中的类

4.在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?
JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:
即时用户自定义类加载器不指定父类加载器,那么,同样可以加载到6e5475ece90bdb5cedfc1a16b6632d24/lib下的类,但此时就不能够加载6e5475ece90bdb5cedfc1a16b6632d24/lib/ext目录下的类了。
    说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。

5.编写自定义类加载器时,一般有哪些注意点?
(1)一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑
一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)
publicclassWrongClassLoaderextends ClassLoader {
 
 public Class<?> loadClass(String name) throws ClassNotFoundException {
 
  returnthis.findClass(name);
 
 }
 
 protected Class<?> findClass(String name) throws ClassNotFoundException {
 
  //假设此处只是到工程以外的特定目录D:/library下去加载类
 
  具体实现代码省略
 
 }
 
}

    通过前面的分析我们已经知道,用户自定义类加载器(WrongClassLoader)的默

       认的类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简

       单测试一下,现在6e5475ece90bdb5cedfc1a16b6632d24/lib、9da487fed862e2e75b98522999bd48b8/lib/ext和工

       程类路径上的类都加载不上了。

问题5测试代码一

publicclass WrongClassLoaderTest {
 
 publicstaticvoid main(String[] args) {
 
  try {
 
  WrongClassLoader loader = new WrongClassLoader();
 
  Class classLoaded = loader.loadClass("beans.Account");
 
  System.out.println(classLoaded.getName());
 
  System.out.println(classLoaded.getClassLoader());
 
  } catch (Exception e) {
 
  e.printStackTrace();
 
  }
 
 }
 
}

(说明:D:"classes"beans"Account.class物理存在的)

输出结果:

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)
 
 at java.io.FileInputStream.open(Native Method)
 
 at java.io.FileInputStream.<init>(FileInputStream.java:106)
 
 at WrongClassLoader.findClass(WrongClassLoader.java:40)
 
 at WrongClassLoader.loadClass(WrongClassLoader.java:29)
 
 at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
 
 at java.lang.ClassLoader.defineClass1(Native Method)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
 
 at WrongClassLoader.findClass(WrongClassLoader.java:43)
 
 at WrongClassLoader.loadClass(WrongClassLoader.java:29)
 
 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)
 
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
 
 at java.lang.ClassLoader.defineClass1(Native Method)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
 
 at java.lang.ClassLoader.defineClass(ClassLoader.java:400)
 
 at WrongClassLoader.findClass(WrongClassLoader.java:43)
 
 at WrongClassLoader.loadClass(WrongClassLoader.java:29)
 
 at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

   

这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass(…)引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
问题5测试二

//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)
publicclassWrongClassLoaderextends ClassLoader {
 
 protected Class<?> findClass(String name) throws ClassNotFoundException {
 
  //假设此处只是到工程以外的特定目录D:/library下去加载类
 
  具体实现代码省略
 
 }
 
}

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

beans.Account
 
WrongClassLoader@1c78e57

这说明,beans.Account加载成功,且是由自定义类加载器WrongClassLoader加载。

这其中的原因分析,我想这里就不必解释了,大家应该可以分析的出来了。

(2)正确设置父类加载器
通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
(3)保证findClass(String )方法的逻辑正确性
事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

6.如何在运行时判断系统类加载器能加载哪些路径下的类?
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到;

二是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")

7.如何在运行时判断标准扩展类加载器能加载哪些路径下的类?
方法之一:

try {
  URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
 
  for (int i = 0; i < extURLs.length; i++) {
 
   System.out.println(extURLs[i]);
 
  }
 
 } catch (Exception e) {//…}

       本机对应输出如下:

file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar
 
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar
 
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar
 
file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar

更多深入解析Java中的Class Loader类加载器相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn