虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码块被称为“类加载器”
Java中的类加载器主要有2类,一类是系统提供的,另一类是由Java应用开发人员编写的
系统提供的类加载器主要有下面3个:
1,启动类加载器(Bootstarp ClassLoader)
这个ClassLoader由JVM自己控制
主要加载JVM自身工作需要的类:将%JAVA_HOME%\lib路径下或-Xbootclasspath参数指定路径下的、能被虚拟机识别的类库(仅按照文件名识别,如:rt.jar,名字不符合的类库不会被加载)加载至虚拟机内存中
启动类加载器无法被Java程序直接引用
2,扩展类加载器(Extension ClassLoader)
该类加载器由sun.misc.Launcher类的静态内部类ExtClassLoader实现
负责加载java.ext.dirs参数(默认值是%JAVA_HOME%\jre\lib\ext,可由VM参数-Djava.ext.dirs指定)指定路径中的所有类库
开发者可以直接使用扩展类加载器
3,应用程序类加载器(Application ClassLoader)
该类加载器由sun.misc.Launcher类的静态内部类AppClassLoader实现
负责加载java.class.path参数(默认值是系统环境变量classpath的值,可由VM参数-Djava.class.path指定)指定路径中的所有类库
System.out.println(System.getProperty("java.class.path"));
在eclipse中打印这个参数时,结果既不是系统环境变量classpath的值,也不是VM参数-Djava.class.path指定的值,这是因为每个eclipse工程下都包含一个.classpath文件,实际上输出的是该文件中指定的值
开发者可以直接使用应用程序类加载器
自定义类加载器
除了系统提供的类加载器之外,开发人员可以通过继承java.lang.ClassLoader类并重写该类的findClass方法的方式实现自己的类加载器
package com.test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("Use myclassloader findClass method.");
//name = com.test.Test
//fileName = Test.class
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
byte[] bytes = loadClassData("c:\\seanApp\\"+fileName);
return defineClass(name, bytes, 0, bytes.length);
}
public byte[] loadClassData(String name) {
FileInputStream fileInput = null;
ByteArrayOutputStream bytesOutput = null;
try {
fileInput = new FileInputStream(new File(name));
bytesOutput = new ByteArrayOutputStream();
int b = 0;
while ((b = fileInput.read()) != -1) {
bytesOutput.write(b);
}
return bytesOutput.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(fileInput != null)
fileInput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
try {
Class<? extends Object> testClass = myClassLoader.loadClass("com.test.Test");
Object obj = testClass.newInstance();
System.out.println(obj.getClass().getClassLoader().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
将Test类编译后生成的Test.class文件放到c:\seanApp\路径下
public class Test {
public Test(){}
}
运行结果为:
sun.misc.Launcher$AppClassLoader@761db1c5
是不是非常奇怪?不是由MyClassLoader类加载的,要弄清楚这点,需要了解ClassLoader的上级委托机制(有的书中也称为双亲委派机制,之所以叫这个名字,是因为在下级类加载器中保存有指向上级类加载器的全局变量,而这个全局变量的变量名为parent)
启动类加载器属于JVM内部使用的类加载器,既没有上级类加载器,也没有下级类加载器,因此不遵守ClassLoader的上级委托机制,只能算是JVM的一个类加载工具,因此我们从扩展类加载器和应用程序类加载器开始
在sun.misc.Launcher类的构造方法中会创建ExtClassLoader类的实例,并用该实例创建AppClassLoader类的实例
public Launcher() {
// 创建扩展类加载器
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError("Could not create extension class loader");
}
// 通过已经创建的扩展类加载器,创建应用程序类加载器
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError("Could not create application class loader");
}
......
扩展类加载器的实例化过程
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException{
final File[] dirs = getExtDirs();
......
return new ExtClassLoader(dirs);
......
}
private static File[] getExtDirs() {
// 加载路径
String s = System.getProperty("java.ext.dirs");
......
}
public ExtClassLoader(File[] dirs) throws IOException {
// 第二个参数指定上级类加载器,而这里是null
// 说明扩展类加载器没有上级类加载器
super(getExtURLs(dirs), null, factory);
this.dirs = dirs;
}
......
}
应用程序类加载器的实例化过程,由此可判断
应用程序类加载器的上级是扩展类加载器
static class AppClassLoader extends URLClassLoader {
// extcl是ExtClassLoader类的实例
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
......
return new AppClassLoader(urls, extcl);
......
}
AppClassLoader(URL[] urls, ClassLoader parent) {
// 应用程序类加载器的上级是扩展类加载器
super(urls, parent, factory);
}
......
}
由于需要通过继承ClassLoader类实现自定义类加载器,故在自定义类加载器的构造函数中,会首先调用ClassLoader类的构造函数,来看下ClassLoader类的构造函数
private ClassLoader parent;
private static ClassLoader scl;
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
public static ClassLoader getSystemClassLoader() {
// scl在改方法中创建
initSystemClassLoader();
.....,
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
// 获取Launcher类的实例
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 参照前面Launcher类的构造函数,scl就是应用程序类加载器
scl = l.getClassLoader();
......
sclSet = true;
}
}
}
可见自定义类加载器的上级是应用程序类加载器
如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给上级类加载器去完成,每一个层次的类加载器都是如此,所有加载器的加载请求最终都应该传送至最顶层的类加载器中(扩展类加载器),只有当上级类加载器反馈自己无法完成这个加载请求(它的类加载范围中没有找到所需的类)时,下级类加载器才会去尝试自己加载这个类,这便是类加载器的上级委托机制
虚拟机将调用ClassLoader类中的loadClassInternal方法加载类
// This method is invoked by the virtual machine to load a class.
private synchronized Class loadClassInternal(String name)
throws ClassNotFoundException{
return loadClass(name);
}
//Invoking this method is equivalent to invoking loadClass(name,false).
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//Subclasses of ClassLoader are encouraged to override findClass(String),
//rather than this method.
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 = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c); }
return c;
}
通过loadClass方法的源代码可以看出,类加载器会先检查类是否已经被加载过,如果没有加载过则调用上级类加载器加载该类(如果上级类加载器为空则查看启动类加载器是否加载了该类),如果上级类加载器加载失败,调用下级类加载器的findClass方法进行加载
为了保证自定义类加载器依然遵循上级委托机制,比起重写loadClass方法,JDK更推荐通过重写findClass方法实现自定义类加载器(详见备注1)
来看看JDK对findClass方法的描述:
Finds the class with the specified binary name.
This method should be overridden by class loader implementations that
follow the delegation model for loading classes, and will be invoked by
the loadClass method after checking the
parent class loader for the requested class. The default implementation
throws a ClassNotFoundException
对loadClass方法中的resolveClass方法也比较好奇,顺带查看了下这个方法的作用:
Links the specified class. This (misleadingly named) method may be
used by a class loader to link a class. If the class c has
already been linked, then this method simply returns. Otherwise, the
class is linked as described in the "Execution" chapter of the
Java Language Specification(http://java.sun.com/docs/books/jls/).
可以发现虚拟机将调用该方法完成类的连接过程,类的连接过程详见:http://blog.csdn.net/a19881029/article/details/17068191
查看ClassLoader类的源代码会发现很多方法是通过调用本地方法(native修饰符修饰的方法)的方式实现的
使用上级委托机制组织类加载器之间的关系的好处是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:java.lang.Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委托给启动类加载器进行加载,启动类加载器在其搜索范围内只能搜索到rt.jar中的java.lang.Object类(详见备注2),这样可以保证Object类始终由启动类加载器从rt.jar中的java.lang.Object加载得到,确保了Object类的唯一性(详见备注3)
如果没有使用上级委托机制,由各个类加载器自行加载的话,如果用户自己实现一个名为java.lang.Object类,并用自定义的类加载器进行加载,系统中将出现多个不同的Object类,Java类型体系中最基础的行为将无法保证,应用程序也将变得一片混乱
备注:
1,为什么JDK不推荐通过重写loadClass方法实现自定义类加载器?
通过重写findClass方法实现自定义类加载器:当调用loadClass方法加载类时,由于自定义类加载器没有重写loadClass方法,实际调用的是ClassLoader类的loadClass方法,该方法保证如果上级类加载器能够加载所需加载的类,则把加载动作委托给上级类加载器完成,当上级类加载器无法完成加载动作时,才把加载动作交由下级类加载器的findClass方法完成,符合类加载器上级委托机制
通过重写loadClass方法实现自定义类加载器:当调用loadClass方法加载类时,将直接调用自定义类加载器中重写的loadClass方法完成加载动作,如果重写的loadClass方法中没有实现首先尝试将加载动作委托给上级类加载器这一过程,将破坏上级委托机制,该机制是可以打破的,但是需要更好的理由(JDBC,JNDI就打破了上级委托机制)
当然,如果在重写的loadCLass方法中首先尝试让上级类加载器完成加载过程,则本质上也是没有没有问题的,只是依然别扭罢了,首先就是为什么不使用现成的实现?其次如果上级类加载器无法完成加载动作,还是要把加载过程委托给自定义类加载器的findClass方法,关键问题是,在ClassLoader类中,findClass是一个空方法,也就是说你还是得重写自己的findClass方法,绕了一大圈,又回来了,除非你能确定上级类加载器能够完成加载动作,这时将不会调用自定义类加载器的findClass方法,不过这样一来,你为什么要实现自己的类加载器?所以说,通过重写findClass方法实现自定义的类加载器就对了,不过下面依然尝试了一下通过重写loadClass方法实现自定义类加载器:
2,在自定义类加载器中,使用defineClass方法加载一个我自己实现的java.lang.Object类
package java.lang;
public class Object {
public Object(){}
}
运行时抛出下面的异常(被禁止的包名称):
java.lang.SecurityException: Prohibited package name: java.lang
事实上,加载所有以"java."开头的类都会抛出这个异常,这应该是出于JDK对其自身实现的基础类的保护
......
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
......
3,比较2个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这2个类是源于同一个Class文件,只要加载它们的类加载器不同,这2个类必定不相等