阅读提示:
全文认真阅读大约需要1个半小时时间,如果你需要在IDE中验证并理解,大约需要3个小时,如果你想自己写个类似的类加载器并调试,估计还需要3个小时。
该知识点的掌握检测与否,你可以尝试其回答Java每日一题(9) 的问题。
Point One
将J2EE应用程序移植到WebSphere应用程序服务器
http://www.ibm.com/developerworks/cn/websphere/library/techarticles/0408_baigang/part2.html
Point Two
类加载器的种类:
- Bootstrap ClassLoader/启动类加载器
主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作。
- Extension ClassLoader/扩展类加载器
主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作。
- System ClassLoader/系统类加载器
主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作。
- User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)
在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。
类加载器的特性:
- 每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。
- 为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 " 双亲委派的加载链 " 结构。
classloader-architecture
classloader-class-diagram
类图中,BootstrapClassLoader是一个单独的java类, 其实在这里, 不应该叫他是一个java类。因为,它已经完全不用java实现了。它是在jvm启动时, 就被构造起来的, 负责java平台核心库。
自定义类加载器加载一个类的步骤
classloader-load-class
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);
}
}
线程上下文类加载器
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)。
为什么要使用这种双亲委托模式呢?
- 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
- 考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。
java动态载入class的两种方式:
- implicit隐式,即利用实例化才载入的特性来动态载入class
- explicit显式方式,又分两种方式:
- java.lang.Class的forName()方法
- java.lang.ClassLoader的loadClass()方法
用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方法的类的类加载器
static块在什么时候执行?
- 当调用forName(String)载入class时执行,如果调用ClassLoader.loadClass并不会执行.forName(String,false,ClassLoader)时也不会执行.
- 如果载入Class时没有执行static块则在第一次实例化时执行.比如new ,Class.newInstance()操作
- static块仅执行一次
各个java类由哪些classLoader加载?
- java类可以通过实例.getClass.getClassLoader()得知
- 接口由AppClassLoader(System ClassLoader,可以由ClassLoader.getSystemClassLoader()获得实例)载入
- ClassLoader类由bootstrap loader载入
NoClassDefFoundError和ClassNotFoundException
- NoClassDefFoundError:当java源文件已编译成.class文件,但是ClassLoader在运行期间在其搜寻路径load某个类时,没有找到.class文件则报这个错
- ClassNotFoundException:试图通过一个String变量来创建一个Class类时不成功则抛出这个异常
Point Three
JVM规范定义了两种类型的类装载器:启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader)。
一. ClassLoader基本概念
1.ClassLoader分类
类装载器是用来把类(class)装载进JVM的。
JVM规范定义了两种类型的类装载器:
启动内装载器
(bootstrap)
和用户自定义装载器
(user-defined class loader)。
JVM在运行时会产生三个ClassLoader:Bootstrap ClassLoader、Extension ClassLoader和AppClassLoader.Bootstrap是用C++编写的,我们在Java中看不到它,是null,是JVM自带的类装载器,用来装载核心类库,如java.lang.*等。
AppClassLoader
的
Parent
是
ExtClassLoader
,而
ExtClassLoader
的
Parent
为
Bootstrap ClassLoader
。
Java
提供了抽象类
ClassLoader
,所有用户自定义类装载器都实例化自
ClassLoader
的子类。
System Class Loader
是一个特殊的用户自定义类装载器,由
JVM
的实现者提供,在编程者不特别指定装载器的情况下默认装载用户类。系统类装载器可以通过ClassLoader.getSystemClassLoader() 方法得到。
例1,测试你所使用的JVM的ClassLoader
/*LoaderSample1.java*/
public class LoaderSample1 {
public static void main(String[] args) {
Class c;
ClassLoader cl;
cl = ClassLoader.getSystemClassLoader();
System.out.println(cl);
while (cl != null ) {
cl = cl.getParent();
System.out.println(cl);
}
try {
c = Class.forName( " java.lang.Object " );
cl = c.getClassLoader();
System.out.println( " java.lang.Object's loader is " + cl);
c = Class.forName( " LoaderSample1 " );
cl = c.getClassLoader();
System.out.println( " LoaderSample1's loader is " + cl);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在我的机器上(Sun Java 1.4.2)的运行结果
sun.misc.Launcher$AppClassLoader@1a0c10f
sun.misc.Launcher$ExtClassLoader@e2eec8
null
java.lang.Object's loader is null
LoaderSample1's loader is sun.misc.Launcher$AppClassLoader@1a0c10f
第一行表示,系统类装载器实例化自类sun.misc.Launcher$AppClassLoader
第二行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader
第三行表示,系统类装载器parent的parent为bootstrap
第四行表示,核心类java.lang.Object是由bootstrap装载的
第五行表示,用户类LoaderSample1是由系统类装载器装载的
二.
parent delegation模型
从1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。
在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的
parent
去装载,若
parent
能装载,则返回这个类所对应的
Class
对象,若
parent
不能装载,则由
parent
的请求者去装载。
图 1 parent delegation模型
如图1所示,loader2的parent为loader1,loader1的parent为system class loader。假设loader2被要求装载类MyClass,在parent delegation模型下,loader2首先请求loader1代为装载,loader1再请求系统类装载器去装载MyClass。若系统装载器能成功装载,则将MyClass所对应的Class对象的reference返回给loader1,loader1再将reference返回给loader2,从而成功将类MyClass装载进虚拟机。若系统类装载器不能装载MyClass,loader1会尝试装载MyClass,若loader1也不能成功装载,loader2会尝试装载。若所有的parent及loader2本身都不能装载,则装载失败。
若有一个能成功装载,实际装载的类装载器被称为定义类装载器,所有能成功返回Class对象的装载器(包括定义类装载器)被称为初始类装载器。如图1所示,假设loader1实际装载了MyClass,则loader1为MyClass的定义类装载器,loader2和loader1为MyClass的初始类装载器。
需要指出的是,Class Loader是对象,它的父子关系和类的父子关系没有任何关系。
那么parent delegation模型为什么更安全了?
因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可以自由选择不用把请求委托给
parent
,但正如上所说,会带来安全的问题。
三.命名空间及其作用
每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。
例2演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装载,LoaderSample3由自定义的装载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了LoaderSample3所对应的Class对象的reference,所以它可以访问LoaderSampl3中公共的成员(如age)。
例2不同命名空间的类的访问
/*LoaderSample2.java*/
import java.net. * ;
import java.lang.reflect. * ;
public class LoaderSample2 {
public static void main(String[] args) {
try {
String path = System.getProperty( " user.dir " );
URL[] us = { new URL( " file:// " + path + " /sub/ " )};
ClassLoader loader = new URLClassLoader(us);
Class c = loader.loadClass( " LoaderSample3 " );
Object o = c.newInstance();
Field f = c.getField( " age " );
int age = f.getInt(o);
System.out.println( " age is " + age);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*sub/Loadersample3.java*/
public class LoaderSample3 {
static {
System.out.println( " LoaderSample3 loaded " );
}
public int age = 30 ;
}
编译:javac LoaderSample2.java; javac sub/LoaderSample3.java
运行:java LoaderSample2
LoaderSample3 loaded
age is 30
从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。
运行时包(runtime package)
由同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看的定义类装载器是否相同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自己定义了一个类java.lang.Yes,并用用户自定义的类装载器装载,由于java.lang.Yes和核心类库java.lang.*由不同的装载器装载,它们属于不同的运行时包,所以java.lang.Yes不能访问核心类库java.lang中类的包可见的成员。
总结
命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了
Java
的安全,运行时包增加了对包可见成员的保护。
二.
扩展ClassLoader
方法
我们目的是从本地文件系统使用我们实现的类装载器装载一个类。
为了创建自己的类装载器我们应该扩展
ClassLoader
类,这是一个抽象类。我们创建一个
FileClassLoader extends ClassLoader
。我们需要覆盖
ClassLoader
中的
findClass(String name)
方法,这个方法通过类的名字而得到一个
Class
对象。
public Class findClass(String name)
{
byte [] data = loadClassData(name);
return defineClass(name, data, 0 , data.length);
}
我们还应该提供一个方法loadClassData(String name),通过类的名称返回class文件的字
节数组。然后使用ClassLoader提供的defineClass()方法我们就可以返回Class对象了。
public byte [] loadClassData(String name)
{
FileInputStream fis = null ;
byte [] data = null ;
try
{
fis = new FileInputStream( new File(drive + name + fileType));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch = 0 ;
while ((ch = fis.read()) != - 1 )
{
baos.write(ch);
}
data = baos.toByteArray();
} catch (IOException e)
{
e.printStackTrace();
}
return data;
}