您的位置:首页 > 编程语言 > Java开发

【Java】ClassLoader源码全面解析java类加载机制

2014-01-09 10:04 519 查看
对于java类加载机制,是Java的比较重要的基础知识,也是笔试面试中被经常提及的一个问题。这篇文章就从头到尾全面剖析java类加载的全部细节。

代码的生命周期

  在讲解java类加载机制的时候必须要首先知道的是,java类加载的时候加载的是什么。谈到这里又不得不谈论整个java文件从编写到运行整个的生命周期。其实所有的编程语言都是要经历下面这几个阶段,可能有的编译器会把几个解读合并成一个,而有的编程语言可能会跳过部分阶段,但是整个大的流程是没有差别的。一个程序文件从编写到运行需要经历,编译->加载->初始化->运行->收尾等几个阶段。

  先对这几个阶段进行简单介绍。

  编译:把源代码通过编译器编译成机器可以识别的语言。一般是把高级源代码翻译成汇编,而对于java则是编译成字节码。

  加载:把由编译器编译成的文件加载进内存。其实这一部分和初始化是不可分割的,这一部分不仅包括初始化而且还包括链接这个阶段。这里把它分成两个部分是为了介绍方便。对于java来说,这部分的工作是获取编译后的字节码文件,然后交给JVM对字节码进行解释执行。

  初始化:当文件被加载进内存的时候,则向操作系统申请内存,进行必要的变量或者内存的初始化工作,为程序执行做准备。

  运行:则是程序进行正常的业务执行阶段。

  收尾:当程序执行结束之后,由操作系统或者其他中间部分(JVM)进行的垃圾收集与资源释放等工作。

  通过上面可以看出,对于java来说类加载主要工作就是获取被编译好的字节码文件。那加载时机,从哪里加载,如何加载,我们就在下面讲解。

类加载

加载与初始化时机

  对于JVM把字节码加载进JVM是一个比较耗费时间的一个操作,它要经历获取字节码文件地址,建立输入输出流,验证字节码正确性等很多操作。所以,JVM是不会轻易加载一个字节码文件的,这个时候就需要一个时机来规定,当某个时机到来JVM必须加载该字节码文件,否则就出错。

    那什么时候JVM会加载一个类那,java规范规定,当出现如下情况的时候,则必须主动加载并初始化一个类。

  1:当使用new关键字,和读取一个类的静态属性的时候(静态变量与静态方法,但是静态final的除外)。这里对访问静态变量需要注意,加载是仅加载定义该静态变量的类。在C类中通过A的子类B访问A中静态变量的时候,B是不会被加载与初始化的,只会加载并初始化A。因为我们只访问了A中定义的静态变量,这一点需要注意。

  2:当使用反射获取一个类的时候;

  3:当初始化一个子类,必须首先加载并初始化其父类;

  4:当执行一个含有main方法的类的时候,必须首先加载含有那个main方法的类。

  除了上述四条之外则不能保证会加载并初始化一个类。

 类加载器介绍

  对java来说,类加载是通过类加载器来完成的。类加载实质要完成的工作是获取字节码,并把字节码生成JVM可以识别的Class对象。这个时候JVM是如何识别某个字节码被加载进JVM了那?通过该字节码的全限定名与加载该类的加载器。即如果同一个类被不同的类加载器加载生成Class对象,那JVM也认为这些对象是不同的。

  对类加载器按大类可以分成两类,一类归属于JVM范畴,用户无法直接调用;一类属于用户自定义的类加载器。其中JVM范畴的加载器包括有

  Bootstrap class loader(
load <JAVA_HOME>/jre/lib
), 主要包括resources.jar、rt.jar、sunrsasign.jar、jsse.jar、charsets.jar等

  Extensions class loader(load
<JAVA_HOME>/jre/lib/ext
), 这个要看每个人的配置不同,一般包括dnsns.jar、localdata.jar等

  System class loader(load加载你指定的classpath下的文件)。
  对于这三种类加载器用户是无法操纵的,也就是说你无法直接通过new操作符获取对象引用。想要获取这三种加载器可以通过ClassLoader类中提供的接口而获取,另一种办法是通过代码获取当前类的加载器,然后获取相应的父加载器,这样也可以获取三种JVM的类加载器。除了上述三种类加载器之外则是用户定义的类加载器,一般是通过实现抽象类ClassLoader来定制化自己的类加载器。三种虚拟机控制的类加载器与用户自定义的加载器可以归一化到某个点用来完成一个类的加载,这个点就是系统加载器。具体可以看下图:



   三种JVM加载器从下到上分别为sun.misc.Launcher$AppClassLoader、sun.misc.Launcher$ExtClassLoader(具体代码在底部)、BootstrapClassLoader。除了引导类加载器(BootstrapClassLoader)之外,所有的类加载器都有一个父类加载器。请注意,这里的父类加载器和类继承中的父类是不一样的,和父子继承没有任何关系。这里的父类加载器你可以理解成更高一层的类加载器,因为在JVM类加载中是采用的父类加载器委托的机制,即当使用一个类加载器加载某个类的时候,该类加载器不会自己直接去加载而是让比它等级更高的父加载器加载,但是也有特殊的场景(这个在下面细谈)。

  其加载过程就是,当调用一个类加载器请求一个类进行加载,那么该类加载器会首先把该请求提交到其父加载器,让后让父加载器进行加载。当父加载器寻找不到该类的时候(因为每个加载器加载路径被限制),这时候才采用第一个被调用的类加载器进行加载。采用此做法的好处是避免重复加载一个类,导致安全问题。上面我们已经说明对于同一个类,不同类加载器进行加载,生成的class对象也会不相同,所以如果任何一个类加载器都进行加载而不是上交给父类,那么同一个类就可能在内存中存在多个对象。

  比如对于所有类的父类Object它理应是由JVM自己加载器进行加载,如果不实用委托父加载器加载机制,那么我们就同样可以使用自己编写的类加载器进行加载,那么这时候内存中就会存在两个Object对象。但是如果使用委托父加载器,就不会出现这情况。因为,当我们要加载Object类的时候,当前类加载器就会把该加载任务移交给父类,有可能一直到顶层父类。在父类中发现Object类已经加在成功了,那么下面就什么都不做。这样就保证了内存中只有一个Object对象,而不会出现重复加载的情况。

  对与每个类加载器都会提供一个getParent()方法,用于获取父类加载器。这里需要注意的是当你获取到ExtClassLoader继续调用getParent方法获取它的父类的时候,程序返回的是NULL。但是按照上面所说它应该返回的是它的父类BootstrapClassLoader,在这里返回NULL是因为BootstrapClassLoader是所有类加载器中唯一一个使用C++语言开发的,当通过getParent获取到BootstrapClassLoader之后因为在java中没有一个对象可以表示它,所以就返回了NULL。

  一般用户自定义的类加载的父类是系统类加载器,而且程序运行中的所有类的加载一般都是由系统类加载器完成的。系统类加载器也可以通过ClassLoader.getSystemClassLoader()获取,而且如果用户不自定义类加载器,jvm中所使用的加载器也默认为此加载器。具体加载过程会在下面进行详述。

类加载过程

  上面说了这么多但是还没有说到加载一个类的过程到底是如何的。下面我们就通过对ClassLoader这个用户可以直接操作实现的类加载器的源码进行分析,来一步一步阐述是如何加载一个类的。

  在上面我们已经提起两点,一个就是如果用户不定义自己的类加载器,那么系统就会默认使用系统类加载器;另外一点是加载类的时候是通过委托给父类加载器加载的。这两点都可以在ClassLoader代码中被验证。对于加载一个类,是通过ClassLoader中的loadClass方法完成,该方法是直接调用另外一个收保护方法,把加载任务全部交给该收保护方法,该方法代码如下:

[java]
view plaincopy





protected synchronized Class<?> loadClass(String name, boolean resolve)  
    throws ClassNotFoundException  
    {  
    // First, check if the class has already been loaded  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        try {  
        if (parent != null) {  
            c = parent.loadClass(name, false);  
        } else {  
            c = findBootstrapClass0(name);  
        }  
        } catch (ClassNotFoundException e) {  
            // If still not found, then invoke findClass in order  
            // to find the class.  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
    }  

  上面代码还是很容易明白的,我在SO上看到一个很简介的介绍可以更直观的说出上述代码究竟做了什么,贴出如下:

A.loadClass()
|
(not-found?) (by findLoadedClass)
|
B.loadClass()
|
(not found?) (by findLoadedClass)
|
systemclassloader.loadClass  (
B is
parent, also can be
|                  called classpath classloader)
|
(not found?) (by findLoadedClass)
|
bootstrap classloader.loadClass (the bootstrap classloader,
|                   (this has no parent)
|
(not found?)
|
systemclassloader.findClass  (on system classloader,
|                   will try to "find" class in "classpath")
|
(not found?)
|
B.findClass
|
(not found?)
|
A.findClass
|
(not found?)
|
ClassNotFoundException

这就是整个加载类的过程,需要说明的两点是,一般情况下当不使用自定义的类的时候,findClass是什么都不错直接抛出类找不到异常的。另外一点是当实现自定义的类加载器的时候建议是覆盖findClass而不是直接覆盖loadClass。

示例代码

1)打印三种JVM的加载路径

[java]
view plaincopy







public static void main(String[] args){  
        System.out.println(LoaderTest.class.getClassLoader());  //打印该类的加载器

        System.out.println(LoaderTest.class.getClassLoader().getParent());//打印加载器的父加载器
        System.out.println(LoaderTest.class.getClassLoader().getParent().getParent());
//祖父类加载器

        String bootClassPath = System.getProperty("sun.boot.class.path");//boot class loader  
        String extClassPath = System.getProperty("java.ext.dirs");//ExtClassLoader  
        String appClassPath = System.getProperty("java.class.path");//AppClassLoader  
        System.out.println("bootClassPath:" + bootClassPath);  
        System.out.println("extClassPath:" + extClassPath);  
        System.out.println("appClassPath:" + appClassPath);  
    }  

sun.misc.Launcher$AppClassLoader@6b97fd

sun.misc.Launcher$ExtClassLoader@1c78e57
null

bootClassPath:D:\tool\eclipse\jdk1.6.0_12\jre\lib\resources.jar;D:\tool\eclipse\jdk1.6.0_12\jre\lib\rt.jar;D:\tool\eclipse\jdk1.6.0_12\jre\lib\sunrsasign.jar;D:\tool\eclipse\jdk1.6.0_12\jre\lib\jsse.jar;D:\tool\eclipse\jdk1.6.0_12\jre\lib\jce.jar;D:\tool\eclipse\jdk1.6.0_12\jre\lib\charsets.jar;D:\tool\eclipse\jdk1.6.0_12\jre\classes
extClassPath:D:\tool\eclipse\jdk1.6.0_12\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
appClassPath:因为使用了maven控制,该路径非常多,在这里就不贴出结果。

从上述的输出打印可以看出,默认的系统类加载器是AppClassLoader,该加载器的父加载器为ExtClassLoader,祖父加载器为null,是因为祖父加载器是bootclassloader是使用C++编写的。

对于类加载器的加载路径都是不同的,所以上面有个地方提到父类加载器无法加载某个类的时候就会把该加载任务再返回给加载器就是因为如此。

2)打印不同类的加载器

public static void main(String[] args){
System.out.println(LoaderTest.class.getClassLoader());//自定义类
System.out.println(DNSNameService.class.getClassLoader());//在jre\lib\ext下的一个类
System.out.println(ClassLoader.class.getClassLoader());//jre\lib
System.out.println(Object.class.getClassLoader());//jre\lib
}输出:

sun.misc.Launcher$AppClassLoader@6b97fd

sun.misc.Launcher$ExtClassLoader@1c78e57

null

null
可以看出,对于自定义类是使用的系统类加载器,对于在:D:\tool\eclipse\jdk1.6.0_12\jre\lib\ext;C:\Windows\Sun\Java\lib\ext下的类都是使用的ExtClassLoader加载器,所有系统类都是通过系统类加载器进行加载,打印出类加载器为null

  参考资料:

  http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
  http://www.ibm.com/developerworks/cn/java/j-dclp1/
  http://www.infoq.com/cn/articles/cf-Java-class-loader
  http://en.wikipedia.org/wiki/Java_Classloader
  http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html
  sun.misc.Launcher: http://srl.cs.berkeley.edu/~mhn/hedc/sun/misc/Launcher.java.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  类加载 ClassLoader