Java类加载原理

来源:互联网 发布:linux下载迅雷资源 编辑:程序博客网 时间:2024/04/28 00:27

深入探讨 Java 类加载器

http://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html

 

 

 

当我们运行这个类的时候,java   MyGreeting.首先操作系统将启动JVM进程 。JVM的初始类加载器(bootstrap classloader)将jdk下常用的类(rt.jar)加载到内存 (方法区,如果是HotSpot 就是持久代)中。并且由系统类加载器(Application Classloader)加载我们的MyGreeting (一个自定义类)。JVM生成一个Main线程来处理我们的逻辑 。其中new MyGreeting对象。在Java heap创建了一个MyGreeting对象。heap中MyGreeting对象的持有一个该类元数据的引用。当我们对该对象进行操作是,线程与 heap中对象发生交互。JVM为每一个线程分配一个工作内存,用来存储我们线程栈上所要用到的数据 。其中工作内存与主存直接交互数据共有8种动作

以上就是我的理解,有些地方可能不对,希望大家轻拍。

推荐一本有关JVM的书,个人感觉挺适合初学者的。《深入理解Java虚拟机:JVM高级特性与最佳实践》

这段话来源:http://www.iteye.com/topic/1112298?page=2

 

 

JVM三种预定义类型类加载器

我们首先看一下 JVM预定义的三种类型类加载器,当一个 JVM 启动的时候, Java 缺省开始使用如下三种类型类装入器:

 

启动( Bootstrap )类加载器 :引导类装入器是用本地代码实现的类装入器,它负责将<Java_Runtime_Home>/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

 

标准扩展( Extension )类加载器 :扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

 

系统( System )类加载器 :系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径( CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

 

除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器


 

JVM在加载类时默认采用的是双亲委派 机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载 

 

LoadClass中loadClass方法:

Java代码  收藏代码
  1. public Class<?> loadClass(String name)throws ClassNotFoundException {  
  2.   
  3.         return loadClass(name, false);  
  4.   
  5. }  
  6.   
  7. protected synchronized Class<?> loadClass(String name, boolean resolve)  
  8.   
  9.             throws ClassNotFoundException {  
  10.   
  11.         // 首先判断该类型是否已经被加载  
  12.   
  13.         Class c = findLoadedClass(name);  
  14.   
  15.         if (c == null) {  
  16.   
  17.             //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  
  18.   
  19.             try {  
  20.   
  21.                 if (parent != null) {  
  22.   
  23. //如果存在父类加载器,就委派给父类加载器加载  
  24.   
  25.                     c = parent.loadClass(name, false);  
  26.   
  27.                 } else {  
  28.   
  29. //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)  
  30.   
  31.                     c = findBootstrapClass0(name);  
  32.   
  33.                 }  
  34.   
  35.             } catch (ClassNotFoundException e) {  
  36.         // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能  
  37.                 c = findClass(name);  
  38.             }  
  39.         }  
  40.         if (resolve) {  
  41.             resolveClass(c);  
  42.         }  
  43.         return c;  
  44.   
  45.     }   

 

 

 

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.    try {  
  3.      System.out.println(ClassLoader.getSystemClassLoader());  
  4.      System.out.println(ClassLoader.getSystemClassLoader().getParent();  
  5.      System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
  6.    } catch (Exception e) {  
  7.        e.printStackTrace();  
  8.    }  
  9. }  

 说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。

代码输出如下:
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$ExtClassLoader@7259da
null

 

 

我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了 null ,就是说标准扩展类加载器本身强制设定父类加载器为 null 。我们还是借助于代码分析一下:

      我们首先看一下 java.lang.ClassLoader 抽象类中默认实现的两个构造函数:

Java代码  收藏代码
  1. protected ClassLoader() {  
  2.   
  3.         SecurityManager security = System.getSecurityManager();  
  4.   
  5.         if (security != null) {  
  6.   
  7.             security.checkCreateClassLoader();  
  8.   
  9.         }  
  10.   
  11.         //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器  
  12.   
  13.         this.parent = getSystemClassLoader();  
  14.   
  15.         initialized = true;  
  16.   
  17.     }  
  18.   
  19.     protected ClassLoader(ClassLoader parent) {  
  20.   
  21.         SecurityManager security = System.getSecurityManager();  
  22.   
  23.         if (security != null) {  
  24.   
  25.             security.checkCreateClassLoader();  
  26.   
  27.         }  
  28.   
  29.         //强制设置父类加载器  
  30.   
  31.         this.parent = parent;  
  32.   
  33.         initialized = true;  
  34.   
  35.     }  

 

我们再看一下ClassLoader抽象类中parent成员的声明:

       // The parent class loader for delegation

private ClassLoader parent;

声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

1.              系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

2.               扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

     现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?

 

标准扩展类加载器和系统类加载器及其父类java.net.URLClassLoaderjava.security.SecureClassLoader都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass)方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试所以,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

 

 

 

2.3    类加载双亲委派示例

以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在eclipse中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

package classloader.test.bean;

    publicclass TestBean {

        public TestBean() {}

}

在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

测试一:

publicclass ClassLoaderTest {

    publicstaticvoid main(String[] args) {

        try {

            //查看当前系统类路径中包含的路径条目

            System.out.println(System.getProperty("java.class.path"));

//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean

Class typeLoaded = Class.forName("classloader.test.bean.TestBean");

//查看被加载的TestBean类型是被那个类加载器加载的

            System.out.println(typeLoaded.getClassLoader());

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

对应的输出如下:D:"DEMO"dev"Study"ClassLoaderTest"bin sun.misc.Launcher$AppClassLoader@197d257

(说明:当前类路径默认的含有的一个条目就是工程的输出目录)

测试二:

将当前工程输出目录下的…/classloader/test/bean/TestBean.class打包进test.jar剪贴到< Java_Runtime_Home >/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin

sun.misc.Launcher$ExtClassLoader@7259da

对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。

    测试三:

test.jar拷贝一份到< Java_Runtime_Home >/lib下,运行测试代码,输出如下:

D:"DEMO"dev"Study"ClassLoaderTest"bin

sun.misc.Launcher$ExtClassLoader@7259da

   测试三和测试二输出结果一致。那就是说,放置到< Java_Runtime_Home >/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除< Java_Runtime_Home >/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

3       java程序动态扩展方式

Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。

       运行时动态扩展java应用程序有如下两个途径:

3.1    调用java.lang.Class.forName(…)

这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发那个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

这里的initialize参数是很重要的,可以觉得被加载同时是否完成初始化的工作(说明: 单参数版本的forName方法默认是不完成初始化的).有些场景下,需要将initialize设置为true来强制加载同时完成初始化,例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题,因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加载.

3.2    用户自定义类加载器

通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):

1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2

2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真个虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3

3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。

       (说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下)

4       常见问题分析:

4.1    由不同的类加载器加载的指定类型还是相同的类型吗?

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

4.2    在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

//java.lang.Class.java

       publicstatic Class<?> forName(String className)throws ClassNotFoundException {

return forName0(className, trueClassLoader.getCallerClassLoader());

}

//java.lang.ClassLoader.java

// Returns the invoker'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

//虚拟机本地实现,获取当前类的类加载器,前面介绍的ClassgetClassLoader()也使用此方法

native ClassLoader getClassLoader0();

4.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.    <Java_Runtime_Home>/lib下的类

2.    < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类

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

4.4    在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:

即时用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。

    说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5

 

4.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种的结论就不成立了。大家可以简  单测试一下,现<Java_Runtime_Home>/lib< Java_Runtime_Home >/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逻辑)

publicclass WrongClassLoader extends ClassLoader {

        protected Class<?> findClass(String name) throws ClassNotFoundException {

            //假设此处只是到工程以外的特定目录D:/library下去加载类

            具体实现代码省略

        }

}

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

beans.Account

WrongClassLoader@1c78e57

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

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

2.      2、正确设置父类加载器

通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

3.      3、保证findClassString )方法的逻辑正确性

事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

4.6    如何在运行时判断系统类加载器能加载哪些路径下的类?

一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到;

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

4.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类加载原理解析

http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

 

 

 

二、

ClassLoader应该是每一个Java程序员都必须了解的,但是我整整工作了四年才发现原来在这方面全是空白,现在在做模块化,必须得了解这方面的知识,模块间必须做隔离. 
以下是我这段时候的学习和总结,很多东西都是借网上的资料,JDK的ClassLoader的API,做如下总结: 
1.类加载器概述 
类加载器是一个对象,是负责加载类.在JVM是通过类加载器的调用LoadClass方法加载类对象. 
类加载器结构: 
1. 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的[null] 
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类[ExtClassLoader] 
3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它[AppClassLoader] 
2.类加载器的工作流程 
 
当classLoader有类需要载入时,先让其parent查找载入,如果parent找不到,再由自己搜索路径进行载入。ClassLoader在运行期会以父/子的层次结构存在,每个classLoader 实例都有其父ClassLoader的引用,而父ClassLoader并没有持有子ClassLoader的引用,从而形成一条单向链,当一个类装载请求提交到某个ClassLoader时,默认的类装载过程如下: 
1. 检查这个类有没有被装载过,如果已经装载过,返回 
2. 调用父ClassLoader去装载类,如果装载成功返回. 
3. 调用自身的装载类方法,如果装载成功则返回 
4. 如查都没有成功,抛出ClassNotFoundException. 
简单说,当ClassLoader链上的某一ClassLoader收到类装载请求时,会按顺序向上询问其所有父节点,直到boot classLoader.任何一个节点成功受理了此请求,则返回,如果所有父节点都不能受理,这个时候才由请求的ClassLoader自身来装载这个类,如果仍不能装载,则抛出异常
3.Class的类加载器到底是哪个. 
类加载器在其应用场景的不同又可以分为如下类加载器: 
 1. 系统 ClassLoader 
 2. 调用者 ClassLoader 
 3. 线程上下文ClassLoader 
这些类加载器主要是用于动态加载资源,也可以解决架包的重复问题. 
调用者类加载器是指当前所在的类装载时所使用的ClassLoader,它可能是SystemClassLoader, 也可能是一个自定义的ClassLoader.可以通过getClass().getClassLoader()来得到Caller ClassLoader.例如,存在类A,是被AClassLoader所加载,A.class.getClassloader()为AClassLoader的实例,它就是A.class的Caller Classloader.  
如果在A类中new一个B类,那么B类的类加载器就一定是AClassLoader吗。答案是错的。因为new一个对象,loadClass(B.class)可能在其父ClassLoader中就已经完成. 
决定一个类的类加载器是defineClass,而判断两个类是否为同一对象的标准里面有一条是类加载器必须为相同. 
现在有一个问题,如何使用指定的ClassLoader去完成类和资源的加载呢,或者说,当需要去实例化一个调用者ClassLoader和它的父ClassLoader都不能加载的类时,怎么办. 
一个典型的一例子是Jaxp,当使用xerces的Sax实现时,我们首先需要通过rt.jar中的java.xml.parsers.SaxparserFactory.getinstance()得到xeceImpl.jar中的org.apache.xerces.jaxp.SAXParserFactory.Impl的实例,由于Jaxp的框架接口的类位于Java_hom/lib/rt.jar中,由bootStrap ClassLoader装载,处于ClassLoader层次结构中的最顶层,而xecesImpl.jar由低怪的ClassLoader装载,也就是说SaxParserFactoryImpl是在SaxParserFactory中实例化的,如前所述,使用SaxParserFactory的CallerClassLoader(boot)是完成不了这个任务的.这里我们需要理解下线程上下文ClassLoader. 

线程上下文ClassLoader
. 每一个线程都有一个关联的上下文ClassLoader.如果使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文ClassLoader.如果程序对线程上下文ClassLoader没有任何改动的话,程序的所有线程将都使用System ClassLoader作为上下文ClassLoader.当使用Thread.currentThread().setContextClassLoader(classLoader)时,线程上下文ClassLoader就变成了指定的ClassLoader了。此时,在本线程的任意一处地方,调用Thread.currentThread().getContextClassLoader().都可以得到前面设置的ClassLoader. 
一个线程来了,第一件事情便是设置ClassLoader来设置线程上下文类加载器,模块内部都使用线程上下文类加载器[比如某一接口处于AClassLoader,我设置AClassLoader为线程上下文类加载器,那么我通过getContextClassLoader就可以得到A类的类加载器,以后使用A类的类加载器,这样就可以统一调用了] 
(有人可能会问了,我总不能每加载一个类,都使用上下文线程去加载吧,我笑了,其实这大可不必,只要你的类是你私有的[在当前类加载器父级加载器未加载过],就不需要重新加载) 

4.JVM工作流程 
Class Loader 加载流程 
 Jvm 建立=>初始化工作=>产生第一个ClassLoader,即boot 
 Boot ClassLoader在sum.misc.Launcher类里面的ExtClassLoader,并设置其Parent为Boot. 
 Boot ClassLoader载入sun.misc.Launcher$AppClassLoader,设定其parent为ExtClassLoader(但是AppClassLoader也是boot所载入) 
 AppClassLoader载入各个xx.class,xx.class也有可能被ExtClassLoader或者boot载入. 
 自定义的ClassLoader的getparent()是AppClassLoader.parent和他的加载器没有关系. 
 ExtClassLoader和AppClassLoader都是URLClassLoader的子类。 
 AppClassLoader的URL是由系统参数java.class.path取出的字符串决定,而java.class.path由运行机制java.exe时的-cp或-classpath或CLASSPATH环境变量决定 
 ExtClassLoader查找的url是系统变量java.ext.dirs,java.ext.dirs默认为jdk\jre\lib\ext 
 Bootstrap loader的查找url是sun.boot.class.path 
5.独立应用的类加载器 
由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。 
因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。 
Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。 

通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。[如何覆盖父类的加载机制] 

有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效[双亲委托机制]。 
解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。 

顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前累加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。 

但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。 
这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器: 
 JNDI使用线程上下文类加载器 
 Class.getResource()和Class.forName()使用当前类加载器 
 JAXP使用上下文类加载器 
 Java.unit.ResourceBundle使用调用者的当前类加载器 
 URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器 
 Java序列化API缺省使用调用者当前的类加载器. 
6.Tomcate类加载器的代理模式 
下面是对每个类加载器的定义: 
1.Bootstrap加载器在这里是Java里的Bootstrap和ExtClassLoader的总称,负责加载Java核心包的类,和<Java_Home>/jre/lib/ext目录下的类.通常我们开发人员并不关心.我想只要是java程序这些肯定是必要的 
2.System就是系统加载器,一般是AppClassLoader,负责加载ClassPath环境变量设置目录下的值,这个我们开发人员会非常关注,但是在Tomcat里面,虽然用AppClassLoader类加载器,但我们设置的ClassPath对它没有影响(如果有影响,那就麻烦了,将会导致Tomcat运行不稳定),为什么呢,因为tomcat每次启动的时候都会在命令行窗口中都会重新设置Classpath值为:<catalina_Home>/bin/bootstrap.jar和<java_Home>/lib/tools.jar,所以这里面的类一般对应用程序不可见的.除非你设置了 
3.Common类加载器负责加载TomcatHOME/common/class下的.Class文件和common/lib中的jar包,这些类可以被Tomcat内核和每个Web应用程序都可以看见,一般放公用的一些重要的类,如servlet.jar等 
4.Catalina类加载器从server/classes和server/lib下加载类,Catalina加载的类只对Tomcat服务器内核可见,对Web应用程序不可见,对于运行Tomcat内核的线程,它的上下文类加载器就是Catalina类加载器 
5.Shared类加载器负责从share/classes和share/lib中加载类,它加载的类只对所有Web应用程序有效,对Tomcat不可见. 
6.WebappX类加载器负责加载Web应用程序的/web-INF/classes和lib目录下的类,只对当前Web应用程序有效,对其他Web应用程序无效,对于运行每个Web应用程序的线程,他们的上下文类加载器就是它们各自的WebappX类加载器 
7.自定义类加载器 
通过ClassLoader的子类动态加载class文件,体现java动态实时类装入特性: 
ClassLoader有两种载入方式: 
 Pre-loading 预先载入,载入的基础类 
 Load-on-demand 按需载入,只有实例化一个类才会被classloader载入,仅仅声有不会被载入. 
Static 何时执行: 
 当调用forName(String)载入class时执行,如果调用ClassLoader.loadClass不会执行,forName(String,false,ClassLoader)也不会执行. 
 如果在载入class时没有执行static块,则在第一次实例化时执行,比如new,Class.newInstance()操作. 
 Static块仅执行一次 

 

当使用java 去执行一个类的时候,JVM使用applicationClassLoader加载这个类,如果A类引用了B类,不管是直接引用,还是class.forName()引用,JVM会找到加载A类的classLoader,并使用这个ClassLoader加载B类. 
注意:JVM加载类A,并使用A的ClassLoader去加载B,但B的类加载器并不一定和A的类加载器一致. 
使用java –verbos:class Main运行一个程序,加载如下: 
隐式加载 
这里的B就是引用类,发生由于引用,实例化或继承导致需要装载类的时候,隐式类装载是在幕后启动的,JVM会解析必要的引用并装载类. 
显式加载 
1. Java.lang.Class.forName()方法加载 
a) Public static Class forName(String classname) 
b) Public static Class forName(String className,Boolean ini,ClassLoader loader) 
参数说明: 
className 所需类的完全限定名 
ini 是否必须初始化类(静态代理初始化) 
loader – 用于加载类的类加载器 
调用只有一个参数的相当于Class.forName(className,true,loader); 
这里的loader 为callerClassLoader,就是调用者类加载器. 

0 0
原创粉丝点击