首頁  >  文章  >  Java  >  Java中關於類別載入器的程式碼詳解

Java中關於類別載入器的程式碼詳解

黄舟
黄舟原創
2017-06-18 09:44:271584瀏覽

這篇文章主要為大家詳細介紹了Java中類別載入器的相關資料,具有一定的參考價值,有興趣的小夥伴們可以參考一下

從java的動態性到類別載入機制 

Java是一種動態語言。那麼要怎麼理解這個「動態」呢?或者說一門語言具備了什麼特性,才能稱之為動態語言呢?對於java,我是這樣理解的。 

JVM(java虛擬機器)執行的不是本機機器碼指令,而是執行一種稱為字節碼的指令(存在於class檔案中)。這就要求虛擬機器在真正執行字節碼之前,先把相關的class檔案載入到記憶體中。虛擬機器不是一次載入所有需要的class文件,因為它在執行的時候根本不會知道以後會用到哪些class文件。它是每用到一個類,就會在運行時「動態地」載入和這個類相關的class檔。這就是java被稱為動態性語言的根本原因。除了動態載入類別之外,還會動態的初始化類,對類別進行動態連結。動態初始化和動態連結放在其他文章中進行介紹。本文只關心類別的載入。 

在JVM中負責載入類別的正是本文要介紹的類別載入器(ClassLoader),所以,類別載入器是JVM不可或缺的重要元件。 

java中的類別載入器及類別載入器運作原理 

java中(指的是javase)有三種載入器。每個類別載入器在創建的時候已經指定他們對應的目錄, 也就是說每個類別載入器去哪裡載入類別是確定的,我認為在ClassLoader類別中應該會有getTargetPath()之類的方法, 得到他們對應的路徑,找了找jdk的文檔,發現是沒有的。以下是這三種類載入器和他們對應的路徑: 

## * AppClassLoader  --   載入classpath指定的路徑中的類別 

 * ExtClassLoader   --   載入jre/lib/ext目錄下或java.ext .dirs系統屬性定義的目錄下的類別 
 *
BootStrap           --   載入JRE/lib/rt.jar中的類別 

#那麼類別載入器是如何運作的載入呢?可以參考jdk中ClassLoader類別的原始碼。這個類別的實作使用了模板方法模式,首先是loadClass方法來加載類,loadClass方法又調用了findClass方法,該方法讀取並返回類文件的數據,findClass方法返回後,loadClass方法繼續調用defineClass方法,將傳回的資料加工成虛擬機器運作時可識別的類型資訊。所以,我們如果開發自己的類別載入器,只需要繼承jdk中的ClassLoader類,並覆寫findClass方法就可以了,剩下的而工作,父類別會完成。其他java平台有的根據自己的需求,實作了自己特定的類別載入器,例如javaee平台中的tomcat伺服器,Android平台中的dalvik虛擬機器也定義了自己的類別載入器。

虛擬機器載入類別有兩種方式,一種方式就是上面提到的ClassLoader.loadClass()方法,另一個是使用反射API,Class.forName()方法,其實Class.forName( )方法內部也是使用的ClassLoader。 Class類別中for

Name方法的實作如下: 


public static Class<?> forName(String name, boolean initialize,
  ClassLoader loader)
  throws ClassNotFoundException
 {
 if (loader == null) {
  SecurityManager sm = System.getSecurityManager();
  if (sm != null) {
 ClassLoader ccl = ClassLoader.getCallerClassLoader();
 if (ccl != null) {
  sm.checkPermission(
 SecurityConstants.GET_CLASSLOADER_PERMISSION);
 }
  }
 }
 return forName0(name, initialize, loader);
 }

 /** Called after security checks have been made. */
 private static native Class forName0(String name, boolean initialize,
   ClassLoader loader)
 throws ClassNotFoundException;

類別載入器的三個特性 

#類別載入器有三個特性,分別為委派,可見性和單一性,其他文章上對這三個特性的介紹如下: 

  * 委託機制是指將載入一個類別的請求交給父類別載入器,如果這個父類別載入器不能夠找到或載入這個類,那就再載入它。

  * 可見性的原理是子類別的載入器可以看見所有的父類別載入器載入的類,而父類別載入器看不到子類別載入器載入的類別。
  * 單一性原理是指只載入一個類別一次,這是由委託機制確保子類別載入器不會再次載入父類別載入器載入過的類別。 

其中,委派機制是基礎,在其他資料中也把這種機制叫做類別載入器的雙親委派模型,其實說的是同一個意思。可加性和單一性是依賴委派機制的。


以下程式碼測試類別載入器的委派機制: 


#

 ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 
 
 ClassLoader extClassLoader = appClassLoader.getParent();
 System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@addbf1
 //AppClassLoader的父加载器是ExtClassLoader
 
 System.out.println(extClassLoader.getParent()); //null
 //ExtClassLoader的父加载器是null, 也就是BootStrap,这是由c语言实现的

由列印結果可知,載入我們自己寫的類別的載入器是AppClassLoader,AppClassLoader的父載入器是ExtClassLoader,在而ExtClassLoader的父載入器回傳結果為null,這表示他的附加載器是BootStrap,這個載入器是和虛擬機緊密聯繫在一起的,在虛擬機啟動時,就會載入jdk中的類別。它是由C實現的,沒有對應的java對象,所以回傳null。但在邏輯上,BootStrap仍是ExtClassLoader的父載入器。也就是說每當ExtClassLoader載入一個類別時,總是會委託給BootStrap載入。 

系统类加载器和线程上下文类加载器 

在java中,还存在两个概念,分别是系统类加载器和线程上下文类加载器。 

其实系统类加载器就是AppClassLoader应用程序类加载器,它两个值得是同一个加载器,以下代码可以验证: 


 ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 
 ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
 System.out.println(sysClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 //由上面的验证可知, 应用程序类加载器和系统类加载器是相同的, 因为地址是一样的

 这两个类加载器对应的输出,不仅类名相同,连对象的哈希值都是一样的,这充分说明系统类加载器和应用程序类加载器不仅是同一个类,更是同一个类的同一个对象。

每个线程都会有一个上下文类加载器,由于在线程执行时加载用到的类,默认情况下是父线程的上下文类加载器, 也就是AppClassLoader。 


 new Thread(new Runnable() {
 
 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //sun.misc.Launcher$AppClassLoader@19821f
 }
 }).start();

这个子线程在执行时打印的信息为sun.misc.Launcher$AppClassLoader@19821f,可以看到和主线程中的AppClassLoader是同一个对象(哈希值相同)。 

也可以为线程设置特定的类加载器,这样的话,线程在执行时就会使用这个特定的类加载器来加载使用到的类。如下代码: 


 Thread th = new Thread(new Runnable() {
 
 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74
 }
 });
 
 th.setContextClassLoader(new ClassLoader() {});
 
 th.start();

在线程运行之前,为它设置了一个匿名内部类的类加载器对象,线程运行时,输出的信息为:jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74,也就是我们设置的那个类加载器对象。 

类加载器的可见性 

下面验证类加载器的可见性,也就是 子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。 

以下代码使用父加载器ExtClassLoader加载子加载器AppClassLoader路径下的类,由输出可知,是不可能实现的。 


 try {
 Class.forName("jg.zhang.java.testConcurrent.Person", true, 
  ClassLoaderTest.class.getClassLoader().getParent());
 System.out.println("1 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("1 -- 未找到类");
 }

输出为 :1 -- 未找到类 。说明抛出了ClassNotFoundException异常。原因是让ExtClassLoader加载 jg.zhang.java.testConcurrent.Person这个类因为这个类不在jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下,所以抛出ClassNotFoundException。所以父加载器不能加载应该被子加载器加载的类。也就是说这个类在父加载器中不可见。这种机制依赖于委派机制。 

下面代码使用子加载器AppClassLoader 加载父加载器BootStrap中的类,这是可以实现的。


 try {
 Class.forName("java.lang.String", true, 
  ClassLoaderTest.class.getClassLoader());
 System.out.println("2 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("2 -- 未找到类");
 }

 输出为:2 -- 类被加载。说明成功加载了String类。是因为在指定由AppClassLoader加载String类时,由AppClassLoader一直委派到BootStrap加载。虽然是由子加载器的父加载器加载的,但是也可以说,父加载器加载的类对于子加载器来说是可见的。这同样依赖于委派机制。其实在虚拟机启动初期,java.lang.String已经被BootStrap预加载了,这时再次加载,虚拟机发现已经加载,不会再重复加载。这同时也证明了类加载器的单一性。 

测试代码 

到此为止,类加载器的知识就全部讲完了。以下是整个测试代码: 


package jg.zhang.java.testclassloader;


/**
 * 参考ImportNew上的一篇文章<<类加载器的工作原理>>,
 * 文章地址:http://www.importnew.com/6581.html
 * 
 * Java类加载器基于三个机制:委托、可见性和单一性。
 * 委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。
 * 可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
 * 单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。
 * 
 * 三种类加载器: 每个类加载器在创建的时候已经指定他们对应的目录, 也就是说每个类加载器去哪里加载类是确定的
 * 我认为在ClassLoader类中应该会有getTargetPath()之类的方法, 得到他们对应的路径,找了找jdk的文档,发现是没有的.
 * AppClassLoader -- 加载classpath指定的路径中的类
 * ExtClassLoader -- 加载jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下的类
 * BootStrap  -- 加载JRE/lib/rt.jar中的类
 * 
 * 
 * 
 * @author zhangjg
 *
 */
public class ClassLoaderTest {

 
 public static void main(String[] args) {
 test1();
 test2();
 test3();
 }

 /**
 * 验证线程上下文类加载器
 */
 private static void test3() {
 /**
 * 1 每个线程都会有一个上下文类加载器,由于在线程执行时加载用到的类,默认情况下是父线程
 * 的上下文类加载器, 也就是AppClassLoader
 */
 new Thread(new Runnable() {
 
 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //sun.misc.Launcher$AppClassLoader@19821f
 }
 }).start();
 
 /**
 * 2 也可以给创建的线程设定特定的上下文类加载器
 */
 Thread th = new Thread(new Runnable() {
 
 @Override
 public void run() {
 ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
 System.out.println(threadcontextClassLosder); //jg.zhang.java.testclassloader.ClassLoaderTest$3@1b67f74
 }
 });
 
 th.setContextClassLoader(new ClassLoader() {});
 
 th.start();
 }

 /**
 * 测试可见性,可见性依赖于委托机制
 */
 private static void test2() {
 
 /**
 * 1 让ExtClassLoader加载 jg.zhang.java.testConcurrent.Person这个类
 * 因为这个类不在jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下
 * 所以抛出ClassNotFoundException
 * 
 * 所以父加载器不能加载应该被子加载器加载的类,这个类在父加载器中不可见
 * 这种机制依赖于委派机制
 */
 
 try {
 Class.forName("jg.zhang.java.testConcurrent.Person", true, 
  ClassLoaderTest.class.getClassLoader().getParent());
 System.out.println("1 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("1 -- 未找到类");
 }
 
 
 /**
 * 2 让AppClassLoader加载java.lang.String类
 * 没有抛出异常,说明类被正常加载了
 * 虽然是由AppClassLoader一直委派到BootStrap而加载的
 * 所以可以说,父加载器加载的类对于子加载器来说是可见的,这同样依赖于委派机制
 * 
 * 其实在虚拟机启动初期,java.lang.String已经被BootStrap预加载了
 * 这时再次加载,虚拟机发现已经加载,不会再重复加载
 */
 try {
 Class.forName("java.lang.String", true, 
  ClassLoaderTest.class.getClassLoader());
 System.out.println("2 -- 类被加载");
 } catch (ClassNotFoundException e) {
 //e.printStackTrace();
 System.out.println("2 -- 未找到类");
 }
 
 }

 /**
 * 验证三种类加载器的父子关系
 */
 private static void test1() {
 ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
 System.out.println(appClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 
 ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
 System.out.println(sysClassLoader); //sun.misc.Launcher$AppClassLoader@19821f
 //由上面的验证可知, 应用程序类加载器和系统类加载器是相同的, 因为地址是一样的
 
 ClassLoader extClassLoader = appClassLoader.getParent();
 System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@addbf1
 //AppClassLoader的父加载器是ExtClassLoader
 
 System.out.println(extClassLoader.getParent()); //null
 //ExtClassLoader的父加载器是null, 也就是BootStrap,这是由c语言实现的

 }

}

以上是Java中關於類別載入器的程式碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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