前言
本文主要介绍了类的加载机制和类加载器,通过流程图展示类的加载过程和类加载的详细流程,对其中的一些概念性名词进行了解释;介绍分析了双亲委派机制;通过分析源码填补文字描述中存疑的点,并通过源码展示双亲委派机制底层是如何实现的;展示了如何自定义类加载器,如何用自定义类加载器打破双亲委派机制。
作者能力欠佳,如文中有描述不清或有存在纰漏的地方,欢迎留言讨论。
一、类的加载过程
首先看一张类加载以及前后过程的流程图
流程图中可以看到一个类的生命周期分为:加载→验证→准备→解析→初始化→使用→卸载七个阶段,其中标绿的五个阶段为一个类的完整加载过程,这五个步骤统称为类的加载,以下着重对这五个步骤进行简单介绍
- 加载:通过类的全名在硬盘上搜索并获取其二进制字节流,并将其读入JVM方法区,同时在堆内存中创建该类的Class对象。
- 验证:通过文件格式验证、元数据验证、字节码验证和符号引用验证四个验证来确保被加载类的正确性,保证.class文件中的二进制字节流符合JVM要求且不会危害JVM。
- 准备:仅为类中的静态变量(被static修饰的变量)分配内存并初始化为默认值,不会为普通成员变量分配内存,其中用被final static修饰的变量在编译阶段就已经完成分配。
- 解析:将常量池中的符号引用转换为直接引用。
- 初始化:若有该类有父类未被初始化,则先初始化其父类,随后为类的静态变量赋初始设定值,执行静态代码块。
二、类加载器
类加载器负责将.class文件加载到JVM内存中,并生成与之相对应的Class对象。
类加载器一共分为三种:启动类加载器、扩展类加载器、应用类加载器。
- 启动类加载器(BootstrapClassLoader):也叫引导类加载器,本加载器由C++实现,负责加载jre/lib目录下的核心类库,比如rt.jar、charsets.jar等。
注:JAVA的沙箱安全机制严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱安全机制体现在启动类加载器上就是该启动类只会加载指定包(java、sun等包)下的类,且会优先加载JDK自带的类,同包同名外部类也不会被加载,保护了JAVA核心源代码。 - 扩展类加载器(ExtClassLoader):负责加载jre/lib/ext目录中的JAR类包(通常为非原生被引用的jar包)。
- 应用类加载器(AppClassLoader):也叫系统类加载器,负责加载用户自定义路径下的class字节码文件,通常该加载器为程序默认加载器。
除了以上三种类加载器,我们还可以自定义类加载器,根据需求扩展类的加载方式,后文中会进行演示。
三、双亲委派机制
双亲委派机制如下图所示、
可以简单吧双亲委托机制看成两个部分:委托和加载(非官方,作者个人理解)
- 委托:当收到类加载请求时,最先调用应用类加载器(前文中有提及,通常应用类加载器为程序中的默认加载器),判断此前应用类加载器是否加载过该类,若没有加载过,则向上委托给其父类扩展类加载器,扩展类加载器执行同样的操作,若扩展类加载器也没有加载过,则向上委托给其父类启动类加载器,同样先执行判断操作。在以上三次判断操作中,若发现曾加载过此类,则直接返回,不再向上进行委托;若直到启动类加载器也没有加载过该类,则进行加载操作。
- 加载:加载操作由启动类加载器先开始,启动类先搜索jre/lib目录下的核心类库中是否有该类,若有,加载后返回,若没有,则由其子类继续执行加载操作,扩展类加载器和应用类加载器同理。
注:启动类加载器、扩展类加载器以及应用类加载器之间并无继承关系,提及父类子类是因为在ClassLoader类中存在一个parent变量,后文中会提及。
使用双亲委派机制的优点:
- 先判断再加载的机制避免了类的重复加载问题
- 加载时,加载顺序为启动类加载器→扩展类加载器→应用类加载器,实质上是从核心类库到自定义类的加载顺序,这样的顺序解决了一些安全问题,比如用户自定义了一个String类,这与java.lang包下的String类同名,使用双亲委派机制就会优先加载JDK自带的String类而忽视用户自定义的String类,防止JAVA核心代码被篡改
四、类加载的详细流程
作者在测试项目中创建了如下测试类ClassLoaderTest1
当想要运行这个类时,详细类加载流程如下,后文会对流程要点进行说明。
五、类加载源码分析
本部分主要通过查看源码来解决一些前文中可能存在的疑问,并进行梳理
后文中的源码大都进行了部分省略和注释添加,只保留讲解需要部分,下文不再赘述
1. Launcher启动类与扩展/应用类加载器的关系
如上两图所示,ExtClassLoader和AppClassLoader本质上就是Launcher类下的两个静态内部类。
2. 创建Launcher类实例时同时创建其他两个类加载器/应用类加载器通常为程序中默认类加载器
Launcher源码如下
public class Launcher {
private static Launcher launcher = new Launcher();
//ClassLoader变量用于存放启动类中默认的类加载器
private ClassLoader loader;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
//创建扩展类加载器实例
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
//创建启动类加载器实例,同时赋值给私有成员变量loader
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
}
}
如上图所示,在Launch启动类中,存在一个Classloader私有类加载器,在构造方法中分别创建了两个成员内部类(扩展类加载器、应用类加载器)的实例,这对应了类加载流程图中的"创建Launcher实例的同时创建其他两个类加载器"。其中用loader接收应用类加载器实例,相当于将应用类加载器设为了程序中的默认类加载器。
3. 各类加载器的关系
3.1 各类加载器的继承关系
前文提到过启动类加载器、扩展类加载器以及应用类加载器之间并无实际继承关系,但存在一个parent变量表述他们的委托关系,下面来进行验证。
在继承关系中可以看到应用类加载器和扩展类加载器都是之间继承URLClassLoader,没有启动类加载器是因为启动类加载器由C++实现。
3.2 各类加载器之间的委托父子关系是如何实现的
在所有类加载器的父类ClassLoader类中可以找到parent变量,源码对该变量的描述为"The parent class loader for delegation-委托关系中的父类加载器",委托关系就靠这个变量表述。
3.3 各类加载器的parent变量是怎么设置的
首先先关注一下应用类加载器,源码如下。在前文中可以了解到,在Launcher的构造方法中创建了应用类加载器的实例,创建实例使用的是getAppClassLoader(var1)方法,并传入var1(var1为先前创建的扩展类加载器实例)。
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
//getAppClassLoader方法为Launcher构造方法中创建应用类加载器时使用的方法
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
//下面主要获取了系统变量(java.class.path为系统类路径)并进行解析 非关注重点
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
//var1x变量为若干jar包路径
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
//此处创建并返回的一个应用类加载器实例
//传入两个参数,分别为(若干jar包的路径)URL数组和由Launcher构造方法传入的扩展类加载器实例。
//创建AppClassLoader实例时调用下方的构造方法
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
//创建时调用构造方法
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
}
以上部分的代码进行到了调用应用类加载器的构造方法,主要关注点在super和参数var2上,后续源码如下:
//AppClassLoader构造方法
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
//关注super调用的父类构造方法 ↓ ↓ ↓
-------------------------------
//URLClassLoader类构造方法(AppClassLoader父类)
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
//此处参数名变为parent,继续调用父类构造方法
super(parent);
//本构造方法其余代码省略
}
//关注super调用的父类构造方法 ↓ ↓ ↓
-------------------------------
//SecureClassLoader类构造方法(URLClassLoader父类)
protected SecureClassLoader(ClassLoader parent) {
super(parent);
//本构造方法其余代码省略
}
//继续关注super调用的父类构造方法 ↓ ↓ ↓
-------------------------------
//ClassLoader类构造方法(SecureClassLoader父类)
protected ClassLoader(ClassLoader parent) {
//调用本类其他构造方法
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
//在此处将扩展类加载器赋值给parent变量
this.parent = parent;
//本构造方法其余代码省略
}
以上就是应用类加载器中parent变量的赋值过程,扩展类加载器同理,但注意,由于启动类加载器是由C++实现,JAVA中没有对应的类,因此扩展类加载器的构造方法中将父类指定为null,源码如下
public ExtClassLoader(File[] var1) throws IOException {
//此处设置为null 其余流程同应用类加载器
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
下面编写一小段代码进行验证
public class ClassLoaderTest1 {
public static void main(String[] args) {
Launcher launcher = new Launcher();
ClassLoader c1 = launcher.getClassLoader();//获取Launcher类的默认类加载器 也就是启动类加载器AppClassLoader
ClassLoader c2 = c1.getParent();//getParent()方法返回父类加载器
ClassLoader c3 = c2.getParent();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
}
}
输出结果如下,符合预期结果。
sun.misc.Launcher$AppClassLoader@7ef20235
sun.misc.Launcher$ExtClassLoader@27d6c5e0
null
4.双亲委派机制的实现
双亲委派机制的实现主要关注两个方法,分别是Classloader类中的loadClass方法和findClass方法,具体流程见源码中标注的注释
首先看应用类加载器对loadClass方法进行的重写,源码如下
//AppClassLoader的loadClass方法重写
//其中参数var1为例如"com.example.Test.classLoaderTest1"的全路径
//参数var2表示该类是否需要被解析 和ClassLoader中的resolveClass方法相关 类的解析非本文重点 后文中仅在源码注释提及
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
//进行安全检查
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
//此方法用于检查传入包名是否与受限包名相等
var4.checkPackageAccess(var1.substring(0, var3));
}
}
//此处省略无关代码
} else {
//调用父类的loadClass方法
//AppClassLoader的直接父类没有重写该方法
//此处跳转至ClassLoader类中的loadClass方法
return super.loadClass(var1, var2);
}
}
AppClassLoader重写的loadClass方法主要增加了包名安全检查等功能,核心部分代码仍在ClassLoader下的loadClass方法内,ClassLoader内相关源码如下。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
//首先通过findLoadedClass方法检查此前是否以及读取过该类
Class<?> c = findLoadedClass(name);
//如果 c!=null 表明目标类此前已经由该类加载器加载过
//直接跳出 检查是否需要解析后返回
if (c == null) {
long t0 = System.nanoTime();
//向上委托(查找)
try {
//由于启动类加载器由C++实现,所以此处进行判断
//当委托父类不为启动类加载器时
//直接调用父类加载器的loadClass方法 完成向上委托
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
}
//向下委托(加载)
//到此步骤如过c仍为null,表明目标类此前未被任何加载器加载过
//开始向下委托进行加载
if (c == null) {
//关键方法findClass
c = findClass(name);
//部分无关代码省略
}
}
if (resolve) {
//resolveClass方法使类的Class对象创建完成也同时被解析
resolveClass(c);
}
return c;
}
}
//此方法可以理解为启动类加载器的loadClass方法(实际不存在)
private Class<?> findBootstrapClassOrNull(String name)
{
//checkName方法判断如果name为null或可能是有效的二进制名称,则返回true
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// 此方法为native方法,非JAVA实现,不关注具体是如何实现的
private native Class<?> findBootstrapClass(String name);
//ClassLoader中的findClass方法无实质内容,由子类重写
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
以上源码主要涉及双亲委派机制的关键方法loadClass,findClass方法主要在UrlClassLoader类中进行的重写,源码如下。
protected Class<?> findClass(final String name) throws ClassNotFoundException{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//首先是对路径进行解析,将.替换为/,并且在字符串末尾追加.class
//例如将com.example.Test.classLoaderTest1
//转换为com/example/Test/classLoaderTest1.class
String path = name.replace('.', '/').concat(".class");
//res为从.class文件重新创建的源代码
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//将byte字节流转换为JVM可以解析的Class对象
//可以将class文件实例化为Class对象
//若成功加载,则加载完成,不再向下委托
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
// 返回null则继续由子类加载器进行尝试加载
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
六、自定义类加载器及测试
前文提到过,双亲委派机制实现的核心方法是loadClass,类加载实现的核心方法是findClass,所以自定义类加载器只需要把关注点放在这两个方法上即可
1. 不打破双亲委派机制
双亲委派机制的主要实现在loadClass方法中,既然不想要打破,那就只需要去重写findClass方法就好了
1-1 首先重写findClass方法
下面代码中的CustomClassLoader的委托父类为AppClassLoader
public class ClassLoaderTest1 {
//模仿应用类加载器和扩展类加载器 写一个静态内部类
//此处直接继承ClassLoader类
//在双亲委派机制中CustomClassLoader的委托父类为AppClassLoader
static class CustomClassLoader extends ClassLoader{
//类路径
private String classPath;
public CustomClassLoader(String classPath){
//用于获取Java类路径 不需要加载外部类可以用这个
//this.classPath = System.getProperty("java.class.path");
this.classPath = classPath;
}
//路径转换方法 返回一个字节数组
public byte[] getByte(String name) throws IOException {
String path = name.replace('.', '/').concat(".class");
FileInputStream fileInputStream = new FileInputStream(this.classPath + "/" + path);
byte[] b = new byte[fileInputStream.available()];
fileInputStream.read(b);
fileInputStream.close();
return b;
}
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
try {
//获取class类路径
byte[] b = getByte(name);
//将字节数组转化为一个Class对象
return defineClass(name,b,0,b.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
}
1-2 准备一个用于测试的.class文件
按图中操作即可
1-3 生成测试方法进行测试
我这里的测试方法为本机ClassLoaderTest1类下的main方法,代码如下
//ClassLoaderTest1类下的main方法
public static void main(String[] args) throws Exception {
CustomClassLoader customClassLoader = new CustomClassLoader("E:/ClassLoaderTest");
Class c1 = customClassLoader.loadClass("com.example.Test.Test");
//输出是哪个类加载器读取了Test类
System.out.println(c1.getClassLoader());
//反射调用test方法
Method m = c1.getMethod("test");
m.invoke(c1.newInstance());
}
对下面三种情况进行测试
1. target包下和外部包下的Test.class文件同时存在
输出结果如下,可以看见是由应用类加载器加载了Test类,这是因为我们没有打破双亲委派机制,向下委托过程中先委托到了应用类加载器,成功加载
sun.misc.Launcher$AppClassLoader@18b4aac2
HelloWorld!
2.只有target包下存在Test.class文件
输出结果如下,跟第一种情况相同,外部包的.class文件是否存在不影响结果
sun.misc.Launcher$AppClassLoader@18b4aac2
HelloWorld!
3.只有外部包下存在Test.class文件
输出结果如下,委托到应用类加载器时,应用类加载器不能找到Test.class,因此继续向下委托给我们的自定义类加载器
com.example.Test.ClassLoaderTest1$CustomClassLoader@27d6c5e0
HelloWorld!
2. 打破双亲委派机制
前文中多次提及,实现双亲委派机制的核心方法是loadClass方法,因此想要在自定义类加载器中打破双亲委派机制有两种方法
2-1 直接使用findClass方法加载类
直接将loadClass方法替换为findClass方法
public static void main(String[] args) throws Exception {
CustomClassLoader customClassLoader = new CustomClassLoader("E:/ClassLoaderTest");
//此处将loadClass方法直接替换为我们重写过的findClass方法
Class c1 = customClassLoader.findClass("com.example.Test.Test");
System.out.println(c1.getClassLoader());
Method m = c1.getMethod("test");
m.invoke(c1.newInstance());
}
此时进行测试,三种.class文件的存在情况输出结果均如下,成功打破双亲委派机制
com.example.Test.ClassLoaderTest1$CustomClassLoader@27d6c5e0
HelloWorld!
2-2 重写loadClass方法
代码如下
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
//首先依然是先判断是否加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
//关键
//此处相当于单独把com.example.Test包抽离出双亲委派机制
//在com.example.Test包下的类都会由我们的自定义类加载器进行加载
//这么做一方面在加载Test类时打破了双亲委派机制
//另一方面让其他的类(比如Object等原生类)正常加载 (因为沙箱安全机制,无法使用自定义类加载器完成这项工作)
if(!name.startsWith("com.example.Test")){
c = this.getParent().loadClass(name);
}else{
c = this.findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
target包下和外部包下的Test.class文件同时存在时运行结果如下,成功打破双亲委派机制
com.example.Test.ClassLoaderTest1$CustomClassLoader@27d6c5e0
HelloWorld!
3. 沙箱安全机制的体现
这部分的验证比较简单,代码也有重复,直接上截图
首先自定义了一个在java.lang包下的String类
之后将.class文件丢到外部包中
之后打破双亲委派机制,使用自定义类加载器直接尝试进行加载自定义的String类
结果如下,果不其然加载失败,体现了java对于恶意代码的防护机制
本篇内容到此结束
作者才疏学浅,如文中出现纰漏,还望指正