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

Java类加载原理解析及怎样自定义Java类实现特殊的功能

2014-09-21 21:08 781 查看


1 基本信息

摘要:

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

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

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

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

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

分类:开发技术->J2EE

标签: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)内容如下:

测试一:

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, true, ClassLoader.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

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

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逻辑)

publicclassWrongClassLoaderextends 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、保证findClass(String )方法的逻辑正确性

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

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


5 总结:

写这篇文章的初衷是通过分析JDK相关代码来验证一些加载规则,核心就是借助双亲委派机制来分析一个加载请求处理的主要过程,所列举的几个简单的例子实际意义不大,因为遇到的情况往往比例子情况复杂的多。

下一篇文章,我会重点分析一下Eclipse的插件类加载器,并分析一下插件环境下的类加载和普通java应用场景中的类加载有什么不同,并会提供一个比较完整的类加载器。

插件类加载器就是一个由eclipse开发的一个用户自定义类加载器,所以分析时候用到的一些基本的东西都是在这篇文章中涉及到的。

这篇文章写的时候时间比较紧,乱糟糟的,大家见谅。中间参考了JVM规范、jdk文档和代码、《Inside
Java Virtual Machine》一书等资料。

文章中肯定包含了一些错误,欢迎指出,谢谢!

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


java类加载器体系结构(含hotswap原理)

jvm classLoader architecture :

a, Bootstrap ClassLoader/启动类加载器

主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作.

b, Extension ClassLoader/扩展类加载器

主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作

c, System ClassLoader/系统类加载器

主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作.

b, User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)

在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.

类加载器的特性:

1, 每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。

2, 为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 ” 双亲委派的加载链 ” 结构.

如下图:



Class Diagram:



类图中, BootstrapClassLoader是一个单独的java类, 其实在这里, 不应该叫他是一个java类。

因为, 它已经完全不用java实现了。

它是在jvm启动时, 就被构造起来的, 负责java平台核心库。(具体上面已经有介绍)

启动类加载实现 (其实我们不用关心这块, 但是有兴趣的, 可以研究一下 ):

bootstrap classLoader 类加载原理探索

自定义类加载器加载一个类的步骤 :



ClassLoader 类加载逻辑分析, 以下逻辑是除 BootstrapClassLoader 外的类加载器加载流程:
// 检查类是否已被装载过
Class c = findLoadedClass(name);
if (c == null ) {
// 指定类未被装载过
try {
if (parent != null ) {
// 如果父类加载器不为空, 则委派给父类加载
c = parent.loadClass(name, false );
} else {
// 如果父类加载器为空, 则委派给启动类加载加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 启动类加载器或父类加载器抛出异常后, 当前类加载器将其
// 捕获, 并通过findClass方法, 由自身加载
c = findClass(name);
}
}


用Class.forName加载类

Class.forName使用的是被调用者的类加载器来加载类的.

这种特性, 证明了java类加载器中的名称空间是唯一的, 不会相互干扰.

即在一般情况下, 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的.
public static Class forName(String className)
throws ClassNotFoundException {
return forName0(className, true , ClassLoader.getCallerClassLoader());
}

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


上图中 ClassLoader.getCallerClassLoader 就是得到调用当前forName方法的类的类加载器

线程上下文类加载器

java默认的线程上下文类加载器是 系统类加载器(AppClassLoader).
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader" );
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);


以上代码摘自sun.misc.Launch的无参构造函数Launch()。

使用线程上下文类加载器, 可以在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.

典型的例子有, 通过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派.

大部分java app服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。

还有一些采用 hotswap 特性的框架, 也使用了线程上下文类加载器, 比如 seasar (full stack framework in japenese).

线程上下文从根本解决了一般应用不能违背双亲委派模式的问题.

使java类加载体系显得更灵活.

随着多核时代的来临, 相信多线程开发将会越来越多地进入程序员的实际编码过程中. 因此,

在编写基础设施时, 通过使用线程上下文来加载类, 应该是一个很好的选择.

当然, 好东西都有利弊. 使用线程上下文加载类, 也要注意, 保证多根需要通信的线程间的类加载器应该是同一个,

防止因为不同的类加载器, 导致类型转换异常(ClassCastException).

自定义的类加载器实现

defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)

是java.lang.Classloader提供给开发人员, 用来自定义加载class的接口.

使用该接口, 可以动态的加载class文件.

例如,

在jdk中, URLClassLoader是配合findClass方法来使用defineClass, 可以从网络或硬盘上加载class.

而使用类加载接口, 并加上自己的实现逻辑, 还可以定制出更多的高级特性.

比如,

一个简单的hot swap 类加载器实现:
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
* 可以重新载入同名类的类加载器实现
*

* 放弃了双亲委派的加载链模式.
* 需要外部维护重载后的类的成员变量状态.
*
* @author ken.wu
* @mail ken.wug@gmail.com
* 2007-9-28 下午01:37:43
*/
public class HotSwapClassLoader extends URLClassLoader {

public HotSwapClassLoader(URL[] urls) {
super (urls);
}

public HotSwapClassLoader(URL[] urls, ClassLoader parent) {
super (urls, parent);
}

public Class load(String name)
throws ClassNotFoundException {
return load(name, false );
}

public Class load(String name, boolean resolve)
throws ClassNotFoundException {
if ( null != super .findLoadedClass(name))
return reload(name, resolve);

Class clazz = super .findClass(name);

if (resolve)
super .resolveClass(clazz);

return clazz;
}

public Class reload(String name, boolean resolve)
throws ClassNotFoundException {
return new HotSwapClassLoader( super .getURLs(), super .getParent()).load(
name, resolve);
}
}

public class A {
private B b;

public void setB(B b) {
this .b = b;
}

public B getB() {
return b;
}
}

public class B {}


这个类的作用是可以重新载入同名的类, 但是, 为了实现hotswap, 老的对象状态

需要通过其他方式拷贝到重载过的类生成的全新实例中来。(A类中的b实例)

而新实例所依赖的B类如果与老对象不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException).

为了解决这种问题, HotSwapClassLoader自定义了load方法. 即当前类是由自身classLoader加载的, 而内部依赖的类还是老对象的classLoader加载的.
public class TestHotSwap {
public static void main(String args[]) {
A a = new A();
B b = new B();
a.setB(b);

System.out.printf("A classLoader is %s n" , a.getClass().getClassLoader());
System.out.printf("B classLoader is %s n" , b.getClass().getClassLoader());
System.out.printf("A.b classLoader is %s n" ,   a.getB().getClass().getClassLoader());

HotSwapClassLoader c1 = new HotSwapClassLoader( new URL[]{ new URL( "file:\e:\test\")} , a.getClass().getClassLoader());
Class clazz = c1.load(" test.hotswap.A ");
Object aInstance = clazz.newInstance();

Method method1 = clazz.getMethod(" setB ", B.class);
method1.invoke(aInstance, b);

Method method2 = clazz.getMethod(" getB ", null);
Object bInstance = method2.invoke(aInstance, null);

System.out.printf(" reloaded A.b classLoader is %s n", bInstance.getClass().getClassLoader());
}
}


输出

A classLoader is sun.misc.Launcher$AppClassLoader@19821f

B classLoader is sun.misc.Launcher$AppClassLoader@19821f

A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f

reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f
参考文献:http://kenwublog.com/structure-of-java-class-loader
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: