`
weigang.gao
  • 浏览: 466378 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

Java类加载原理解析

    博客分类:
  • java
 
阅读更多

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

1       基本信息

摘要:

每个java开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载。Java的类加载机制是java技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

由于关于java类加载的内容较多,所以打算分三篇文章简述一下:

第一篇:java类加载原理解析

第二篇:插件环境下类加载原理解析

第三篇:线程上下文类加载器

 

2       Java虚拟机类加载器结构简述

2.1    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)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在后面单独介绍。

2.2    类加载双亲委派机制介绍和分析

       在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机 制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返 回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和标准扩展类加载器为例作简单分析。

                 
                 图一 标准扩展类加载器继承层次图

                   
        图二 系统类加载器继承层次图

       通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

//加载指定名称(包括包名)的二进制类型,供用户调用的接口

public Class<?> loadClass(String name) throws ClassNotFoundException{//…}

//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是,这里的resolve参数不一定真正能达到解析的效果~_~),供继承用

protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}

//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用

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

//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

throws ClassFormatError{//…}

       通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

public Class<?> loadClass(String name)throws ClassNotFoundException {
        return loadClass(name, false);
}
protectedsynchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // 首先判断该类型是否已经被加载
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
            try {
                if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
                    c = parent.loadClass(name, false);
                } else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
        // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;

 

    通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:


                    
                             图三 类加载器默认委派关系图

上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

示例代码:

public static void main(String[] args) {
   try {
     System.out.println(ClassLoader.getSystemClassLoader());
     System.out.println(ClassLoader.getSystemClassLoader().getParent();
     System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
   } catch (Exception e) {
       e.printStackTrace();
   }
}

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

代码输出如下:

sun.misc.Launcher$AppClassLoader@197d257

sun.misc.Launcher$ExtClassLoader@7259da

null

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

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

protected ClassLoader() {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
		security.checkCreateClassLoader();
	}
	//默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
	this.parent = getSystemClassLoader();
	initialized = true;
}
protected ClassLoader(ClassLoader parent) {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
		security.checkCreateClassLoader();
	}
	//强制设置父类加载器
	this.parent = parent;
	initialized = true;
}

 

 

    我们再看一下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.URLClassLoader和java.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)内容如下:

测试一:

public class ClassLoaderTest {
	public static void main(String[] args)  {
		try {
			//系统类加载器加载路径
			System.out.println(System.getProperty("java.class.path"));
			
			//标准扩展类加载路径
			System.out.println(System.getProperty("java.ext.dirs"));
			
			//启动类加载器加载路径
			System.out.println(System.getProperty("sun.boot.class.path"));
			
			//调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean。但最终加载TestBean不一定是系统类加载器
			Class typeLoaded = Class.forName("com.hsp.bean.TestBean");
			
			//查看被加载的TestBean类型是被那个类加载器加载的
			System.out.println(typeLoaded.getClassLoader());
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} 
	}

}

 

对应的输出如下:

 D:\workspace4.4\ClassLoaderDemo01\bin

D:\software\jdk1.6\jre\lib\ext;C:\windows\Sun\Java\lib\ext

D:\software\jdk1.6\jre\lib\resources.jar;D:\software\jdk1.6\jre\lib\rt.jar;D:\software\jdk1.6\jre\lib\sunrsasign.jar;D:\software\jdk1.6\jre\lib\jsse.jar;D:\software\jdk1.6\jre\lib\jce.jar;D:\software\jdk1.6\jre\lib\charsets.jar;D:\software\jdk1.6\jre\classes

sun.misc.Launcher$AppClassLoader@197d257

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

 

测试二:

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

D:\workspace4.4\ClassLoaderDemo01\bin

D:\software\jdk1.6\jre\lib\ext;C:\windows\Sun\Java\lib\ext

D:\software\jdk1.6\jre\lib\resources.jar;D:\software\jdk1.6\jre\lib\rt.jar;D:\software\jdk1.6\jre\lib\sunrsasign.jar;D:\software\jdk1.6\jre\lib\jsse.jar;D:\software\jdk1.6\jre\lib\jce.jar;D:\software\jdk1.6\jre\lib\charsets.jar;D:\software\jdk1.6\jre\classes

sun.misc.Launcher$ExtClassLoader@7259da

 

 

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

 

    测试三:

我们知道Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。因此我们

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

D:\workspace4.4\ClassLoaderDemo01\bin

D:\software\jdk1.6\jre\lib\ext;C:\windows\Sun\Java\lib\ext

D:\software\jdk1.6\jre\lib\resources.jar;D:\software\jdk1.6\jre\lib\rt.jar;D:\software\jdk1.6\jre\lib\sunrsasign.jar;D:\software\jdk1.6\jre\lib\jsse.jar;D:\software\jdk1.6\jre\lib\jce.jar;D:\software\jdk1.6\jre\lib\charsets.jar;D:\software\jdk1.6\jre\classes

sun.misc.Launcher$ExtClassLoader@7259da

 

    测试四:

在jdk1.6中执行System.getProperty("sun.boot.class.path"),输出如下内容:

D:\software\jdk1.6\jre\lib\resources.jar;  

D:\software\jdk1.6\jre\lib\rt.jar;         

D:\software\jdk1.6\jre\lib\sunrsasign.jar;

D:\software\jdk1.6\jre\lib\jsse.jar;        

D:\software\jdk1.6\jre\lib\jce.jar;         

D:\software\jdk1.6\jre\lib\charsets.jar;    

D:\software\jdk1.6\jre\classes

红色字体部分是能够找到在jdk1.6中找到的部分,黑色的是没有的部分。在默认情况下,启动类加载器会自动加载D:\software\jdk1.6\jre\lib\sunrsasign.jar;路径下的jar包。因此,我们将test.jar改成名成sunrsasign.jar,这样启动类加载器就能够加载TestBean.class字节码文件了。运行测试代码,输出如下:

D:\workspace4.4\ClassLoaderDemo01\bin

D:\software\jdk1.6\jre\lib\ext;C:\windows\Sun\Java\lib\ext

D:\software\jdk1.6\jre\lib\resources.jar;D:\software\jdk1.6\jre\lib\rt.jar;D:\software\jdk1.6\jre\lib\sunrsasign.jar;D:\software\jdk1.6\jre\lib\jsse.jar;D:\software\jdk1.6\jre\lib\jce.jar;D:\software\jdk1.6\jre\lib\charsets.jar;D:\software\jdk1.6\jre\classes

null

 

   测试三和测试二输出结果一致。那就是说,放置到< 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中变量视图查看验证。

 

结论:①启动类加载器只会加载sun.boot.class.path(启动类加载器加载路径)路径下面特定的名称的jar, 扩展类加载器只会加载java.ext.dirs(标准扩展类加载路径)下面的类,系统类加载器只会加载java.class.path(系统类加载路径)下面的类

②自定类加载器的父加载器是系统类加载器,系统类加载器的父加载器是扩展类加载器,扩展类加载器的父类是null

 

分享到:
评论

相关推荐

    Java类加载原理解析文档

    Java类加载原理解析.docx 注意:这是一篇文档。。。

    Java类加载原理解析.rar

    Java的类加载机制是java技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解java虚拟机的连接模型和java语言的动态性...

    深入理解java类加载机制

    在类加载方面,我们将深入探讨Java程序的类加载原理和流程,包括加载、验证、准备、解析和初始化等五个环节的详细解析,并对其强调点进行详细讲解。我们将详细介绍Java虚拟机中类的生命周期并探讨类加载时的各种问题...

    Java类加载器层次结构原理解析

    主要介绍了Java类加载器层次结构原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    Java类加载连接和初始化原理解析

    主要介绍了Java类加载连接和初始化原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    移动开发+Android组件化+APT原理解析+ARouter基础技术之APT原理解析,源码分析

    在javac编译期间,先加载APT实现类(即 AbstractProcessor实现类),并查找解析java源文件注解信息,并于APT声明支持处理的注解信息做匹配,如果java源文件存在APT声明的注解,则调起APT实现类的 process方法 ...

    java源码包---java 源码 大量 实例

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    JAVA上百实例源码以及开源项目源代码

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    JAVA上百实例源码以及开源项目

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码,...

    java源码包4

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...

    深入理解java7

    第二部分是7-13章,对JVM、Java源代码和字节代码操作、类加载器、对象生命周期、多线程、并发编程、泛型、安全等Java平台的核心技术进行了深入解析,掌握这部分内容有助于深入理解Java的底层原理;第三部分为第14章...

    java源码包3

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...

    java源码包2

     Java二进制IO类与文件复制操作实例,好像是一本书的例子,源代码有的是独立运行的,与同目录下的其它代码文件互不联系,这些代码面向初级、中级Java程序员。 Java访问权限控制源代码 1个目标文件 摘要:Java源码...

    JAVA高并发高性能高可用高扩展架构视频教程

    公司级框架原理解析 解密公司内部框架开发(打造属于自己的专属框架) 手写Tomca之深度解析动态资源请求原理 深度解析springMVC实现原理(手写springMVC框架) Java验证码 正则黑名单爬虫系统 深入数据库连接池内部运转...

    成百上千个Java 源码DEMO 4(1-4是独立压缩包)

    日历表格面板 [ConfigLine.java] 控制条类 [RoundBox.java] 限定选择控件 [MonthMaker.java] 月份表算法类 [Pallet.java] 调色板,统一配色类 Java扫雷源码 Java生成自定义控件源代码 2个目标文件 Java实现HTTP连接...

    java调用.net写的返回值为dataset的webservice(2)实例项目

    包内容太大,无法用一个压缩文件上传。放在另一个。 网上的调用例子太多了,有的要用cmd来解析webserive,有的没有包,有的代码报错。...GetWebService2 类实现原理:从webservice取值后保存为xml,然后读取,

Global site tag (gtag.js) - Google Analytics