前言
上一篇 Java虚拟机——类加载机制 说到整个类加载过程中除了加载(加载是类加载的一个阶段)阶段,用户可以自定义类加载器参与之外,其余的阶段均完全由虚拟机主导和控制,实际上已经明确了类加载器工作的阶段和主要作用。
把“根据类的全限定名获取定义这个类的二进制字节流”这个动作放到虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为“类加载器”。
对于任意一个类,都需要这个类的类加载器和这个类本身才能确定其在虚拟机内存中的唯一性。换句话说就是两个类相等的必要不充分条件就是它们是由同一个类加载器加载的。
分类
从虚拟机(HotSpot)的角度来说,类加载器分为两类:Bootstrap ClassLoader
和其他类加载器。Bootstrap ClassLoader是C++实现的,是虚拟机本身的一部分。其他类加载器都是由Java语言实现的,独立于虚拟机之外。从Java开发者的角度来说,JDK提供了三种常见的类加载器:Bootstrap ClassLoader
、Extension ClassLoader
、Application ClassLoader
。
- Bootstrap ClassLoader:启动类加载器,这个类加载器负责将
%JAVA_HOME%\bin
目录,或者-Xbootclasspath
所指定的目录中能被虚拟机识别的类库加载到虚拟机内存中。 - Extension ClassLoader:扩展类加载器,负责将
%JAVA_HOME%\lib\ext
目录中的,或者被java.ext.dirs
系统变量所指定路径中的所以有类库。 - Application ClassLoader:应用程序加载器,也叫
System ClassLoader
。负责加载classpath所指定的类库
除了JDK提供的三种类加载器外,用户还可以自己定义类加载器。
双亲委派机制
各种类加载器的关系如图所示:
类加载器之间这种层次关系称为类加载器的双亲委派模型,双亲委派模型要求除了Bootstrap ClassLoader外,其余的类加载器必须有自己的父加载器。这里类加载器的父子关系一般不会以继承(Inheritance)的方式实现,而是通过组合(Composition)方式复用父类代码。双亲委派机制不是虚拟机强制规定,而是虚拟机对类加载器实现的一种规范。
双亲委派机制的工作原理是:当类加载器收到加载类的请求时,先把请求传递给父类加载器,每个加载器均是如此,所以每次请求都会到达Bootstrap ClassLoader,只有当父类加载器无法完成该类的加载时,子类加载器才会尝试自己加载。具体过程如下
双亲委派机制的好处有两点
- 安全,避免了JDK核心类库被替换
- 节约资源,避免虚拟机内存中出现两份一样的字节码。
举个例子,自定义一个java.lang.String
类:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("java.lang.String");
}
}
可以看出自定义的java.lang.String
可以编译,但是不能运行(准确的说是不能被加载),因为JDK自带的java.lang.String
已经被Bootstrap ClassLoader加载过了。
实现原理
双亲委派的实现代码定义在java.lang.ClassLoader
中:
/**
* @param name 类名
* @param resolve 是否需要解析类
*
* @return The resulting <tt>Class</tt> object
* @throws ClassNotFoundException If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,检查是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果父类加载器不为null,调用父类加载器来加载该类
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为null,调用Bootstrap ClassLoader
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.
// 如果仍然没有加载该类,则执行findClass方法进行类加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
根据源码可以知道,当父加载器无法加载指定类时,需要调用findClass(name)
方法来自己尝试加载类,findClass(name)
方法定义如下
/**
* Finds the class with the specified <a href="#name">binary name</a>.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class. The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
根据类注释以及这里的实现可以知道,该方法就是用来给类加载器实现类来重写的。这种设计模式叫模板方法模式。既然如此,那就来定义自己类加载器。
自定义ClassLoader
想要简单的实现一个ClassLoader
,只需要继承ClassLoader
类,并重写findClass
方法即可
/**
* 自定义类加载器
* @author sicimike
*/
public class SicimikeClassLoader extends ClassLoader {
private static final String PATH_NAMESPACE = "C:/test/";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = PATH_NAMESPACE + name;
path = path.replace('.', '/').concat(".class");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = null;
try {
is = new FileInputStream(path);
int n = 0;
byte[] buffer = new byte[1024];
while ((n = is.read(buffer)) != -1) {
baos.write(buffer, 0, n);
}
byte[] bytes = baos.toByteArray();
/**
* 如有需要,此处可对加密字节码进行解密或者一些别的特殊操作
*/
// 将字节数组转换成Class对象
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭IO流
}
return super.findClass(name);
}
}
测试代码
/**
* @author sicimike
*/
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader classLoader = new SicimikeClassLoader();
try {
Class<?> cls = classLoader.loadClass("com.sicimike.loader.Hello");
System.out.println(cls.getClassLoader());
System.out.println(classLoader.getParent());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
执行结果
com.sicimike.loader.SicimikeClassLoader@330bedb4
sun.misc.Launcher$AppClassLoader@18b4aac2
根据执行结果可以看出,类Hello
(无实际内容,只是为了被加载)确实是被自定义的类加载器SicimikeClassLoader
加载的。并且父加载器也是AppClassLoader
。想要达到以上效果,需要把Hello
类编译后的字节码拷贝到classpath目录外,并且删除target目录下的Hello.class
,否则会被AppClassLoader
加载而导致第一行输出是AppClassLoader
。
参考
《深入理解Java虚拟机》