1 基础信息
- 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用Java类型,就是虚拟机的类加载机制
- 类从被加载到虚拟机内存中开始,到卸载出内存为止,包含:加载、验证、准备、解析、初始化、使用和卸载等7个阶段,称为类生命周期,其中验证、准备、解析3个部分统称为连接
2 Java虚拟机类加载器结构简述
2.1 JVM三种预定义类加载器
JVM预定义的三种类型类加载器,其启动的时候,Java缺省使用下列三种类型类加载器:
- 启动类加载器(Bootstrap):引导类装入器是用本地代码实现的类装入器,它负责将Java_Home/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作;
- 扩张类加载器(Extesion):扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器
- 系统类加载器(System):系统类加载器是由Sun的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java-classpath或-Djava.class.path变量所指的目录下的类库加载内存中。开发者可以直接使用系统类加载器
2.2类加载双亲委托派机制介绍和分析
JVM在加载类时默认采用的是双亲委派机制。双亲委派模型的工作过程是:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载;
从JDK源码查看可知,类加载器均是继承自java.lang.ClassLoader
抽象类。让我们来看看其中几个重要方法:
//加载指定名称(包括包名)的二进制类型,供用户调用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException
//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承使用
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
//一般被loadClass方法调用去加载指定名称类,供继承使用
protected Class<?> findClass(String name) throws ClassNotFoundException
//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承(JVM已经实现了对应具体功能,解析对应的字节码,产生对应的内部数据结构放置方法区,所以无需覆写,直接调用即可)
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
现在我们再看看ClassLoader加载类最关键的逻辑代码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized 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 {
//不存在父类就委派给启动类加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才用调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通过上述源码解析,我们对于双亲委派类加载机制有了更明确认识,对于三种默认的类加载器之间的关系是如何的呢?
public class LoadTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}
输出:
sun.misc.Launcher$AppClassLoader@37b90b39
sun.misc.Launcher$ExtClassLoader@558fe7c3
null
通过上述试验,我们可以判断出系统类加载器的父类加载器是扩展类加载器,但是我们试图获取扩展类加载器的父类加载器时确是null,也就是说扩展类加载器本身强制设定父类加载器为null;
首先,我们看下ClassLoader抽象类中默认实现的两个构造函数:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
再看看ClassLoader抽象类中parent成员的声明:
private ClassLoader parent;
声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应setter方法),结合前面的测试代码的输出,我们可以推断:
- 系统类加载器(AppClassLoader)调用
ClassLoader(ClassLoader parent)
构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器)- 扩展类加载器(ExtClassLoader)调用
ClassLoader(ClassLoader parent)
构造函数将父类加载器设置为null(不设置为null,系统会默认设置为系统类加载器)
为什们这样设置呢?
其实从
loadClass(String name, boolean resolve)
看到当父类加载器不为null时直接调用父类加载器进行加载任务,为null时直接调用本地方法获取启动类加载器进行加载任务;
3Java程序动态扩展方式
Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译是一致的类或者接口,又允许用户自己定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。
运行时动态扩展Java应用程序有如下两个途径:
3.1调用Class.forName()加载类
public static Class<?> forName(String className) throws ClassNotFoundException {
return forName0(className, true, ClassLoader.getCallerClassLoader());
}
public static Class<?> forName(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException
{
if (loader == null) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader ccl = ClassLoader.getCallerClassLoader();
if (ccl != null) {
sm.checkPermission( SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader);
}
private static native Class forName0(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException;
- 其中initialize参数是很重要的,它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)
- 有些场景下需要将initialze设置为true来强制加载同时完成初始化。例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题。因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。
3.2 用户自定义类加载器
通过源码,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当作自定义类加载器来对待,唯一区别是否被虚拟机默认使用。
前面内容中已经对ClassLoader
抽象类中几个重要的方法做了介绍:
1 检查请求的类是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则进行2
2 委派类加载任务给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则进行3
3 调用本类加载器的findClass()
方法,试图获取对应的字节码,如果获取到,则调用defineClass()
导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass()
,loadClass()
转而抛异常,终止加载过程