0x01 概念
Java是一个依赖于JVM实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader
加载类字节码,ClassLoader
会调用JVM的native方法(defineClass0/1/2)
来定义一个java.lang.Class
实例。
Java类加载器(Java Classloader)
是Java运行时环境的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac
编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。
0x02 类文件编译流程
这张图解释了类加载的流程,当我们创建一个ClassLoaderTest.java
文件,并经过javac
编译成ClassLoaderTest.class
字节码文件,这个java文件和生成的class
文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class
文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作)。
在生命周期的加载阶段,JVM会找到需要加载的类并把类的信息加载到JVM的方法区中(
InstanceKlass
对象),然后在堆区中实例化一个java.lang.Class
对象,作为方法区中这个类的信息的入口。因为方法区中的对象是用C++编写的,java并不能操作这个对象,所以JVM创建一个java.lang.Class
对象来包装,可以让开发者在写代码时获取到。同时java.lang.Class
对象中的字段内容少于InstanceKlass
对象,因为InstanceKlass
对象中的一部分字段内容仅面向JVM,所以在堆区实例化一个java.lang.Class
对象,将InstanceKlass
对象拷贝过来,并剔除掉开发者不需要的内容。连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
验证阶段JVM会检查字节码的合法性,确保文件内容符合JVM规范,防止恶意代码。
准备阶段JVM会分配内存空间,为类静态变量
(static)
分配内存并设默认值(0/null)
,静态常量(final static)
的默认值为程序中设定的值。这些静态字段的数据会存放在堆区。解析的任务就是把常量池中的符号引用转换为直接引用,举个例子,比如我们要在内存中找一个类里面的一个叫做
show
的方法,显然是找不到。但是在解析阶段,JVM就会把show
这个名字转换为指向方法区的的一块内存地址,比如c17164
,通过c17164
就可以找到show
这个方法具体分配在内存的哪一个区域了。这里show
就是符号引用,而c17164
就是直接引用。所以在解析阶段,JVM会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:
通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。(
final
修饰的并且等号右边是常量不会触发初始化)通过反射方式执行以上三种行为。
初始化子类的时候,会触发父类的初始化。
作为程序入口直接运行时(也就是直接调用
main
方法)。
类初始化完成后,堆区的 Class
对象通过内部指针引用方法区的字节码数据。
0x03 类加载器分类
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader
的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader
。
值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null
值,如:java.lang.Srting.class.getClassLoader()
将返回一个null
对象,因为java.lang.Srting
类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)
加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。
3.1启动类加载器
引导类加载器(Bootstrap ClassLoader
),底层原生代码是C++语言编写,属于JVM一部分,不继承java.lang.ClassLoader
类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar
目录当中。(同时处于安全考虑,Bootstrap ClassLoader
只加载包名为java
、javax
、sun
等开头的类)。
3.2扩展和应用程序类加载器
扩展类加载器(Extensions ClassLoader
),由sun.misc.Launcher$ExtClassLoader
类实现,用来在/jre/lib/ext
或者java.ext.dirs
中指明的目录加载java
的扩展库。
App类加载器(App ClassLoader
),由sun.misc.Launcher$AppClassLoader
实现,一般通过通过(java.class.path
或者Classpath
环境变量)来加载Java
类,也就是我们常说的Classpath
路径,通常我们是使用这个加载类来加载Java
应用类。
扩展类加载器和App类加载器源码都位于sun.misc.Launcher
中,是一个静态内部类,继承自URLClassloader
,具备通过目录或指定jar包将字节码文件加载到内存中。
0x04 双亲委派机制
4.1概念
通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class
文件加载到内存当中生成class
对象进行使用,且加载过程使用的是双亲委派模式,即把需要加载的类交由父加载器进行处理。
双亲委派机制有两个作用:
保证类加载的安全性,避免恶意代码替换JDK中的核心类库,比如
java.lang.Srting
,确保核心类库的完整性和安全性。避免重复加载,双亲委派机制可以避免一个类被多次加载。
双亲委派机制:每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则将加载请求委派给父类加载器。如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载,看上去是自顶向下加载。一句话概括就是:自底向上查询是否加载过,再由定向下进行加载。
4.2ClassLoader核心方法
首先先分析ClassLoader
的原理,ClassLoader
包含了四个核心方法,双亲委派机制的核心代码就位于loadClass
中。
public Class<?> loadClass(String name)
:类加载的入口,提供了双亲委派机制,内部会调用findclass
;protected Class<?> findClass(String name)
:由类加载器子类实现,获取二进制数据调用defineClass
,比如URLClassLoader
会根据文件路径去获取类文件中的二进制数据;protected final Class<?> defineClass(String name, byte[] b, int off, int len)
:做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中;protected final void resolveClass (Class<?> c)
:执行类生命周期中的连接阶段。
理解Java类加载机制并非易事,这里我们以一个Java的HelloWorld来学习ClassLoader
。
ClassLoader
加载com.hspstudy.classloader.HelloWorld
类loadClass
重要流程如下:
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载com.hspstudy.classloader.HelloWorld
类。调用
findLoadedClass
方法检查TestHelloWorld
类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。如果创建当前
ClassLoader
时传入了父类加载器就使用父类加载器加载TestHelloWorld
类,直至父类加载器为null
则使用JVM的Bootstrap ClassLoader
加载。(向上委派)如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。(向下加载)如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.hspstudy.classloader.HelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。如果调用loadClass的时候传入的
resolve
参数为true,那么还需要调用resolveClass
方法链接类,默认为false。返回一个被JVM加载后的
java.lang.Class
类对象。
这里再对第五步做下解释,所有类加载器都继承自ClassLoader
基类,而基类的findClass
默认就是抛异常的。所以任何没有重写findClass
的加载器(包括用户自定义的)在第五步都会失败。其次,像AppClassLoader
这样的系统加载器其实重写了findClass
,所以它们能在这一步从classpath
找到类。比如我们用于加载jar包的java.net.URLClassLoader
其本身通过继承java.lang.ClassLoader
类,重写了findClass
方法从而实现了加载目录class文件甚至是远程资源文件。同时在双亲委派链中,父加载器(如Bootstrap
)根本没有findClass
概念,只有到了子加载器(如AppClassLoader
)才会用到这个方法。自定义加载器作为委派链最末端,必须自己实现findClass
。
0x05 自定义ClassLoader
既然已知ClassLoader
具备了加载类的能力,我们不仅希望使用classpath
中指定的类或JAR包,有时还需要通过自定义类加载器加载本地磁盘文件或网络资源。
package com.hspstudy.classloader;
//测试类
public class HelloWorld {
public String hello() {
return "Hello World~";
}
}
在com.hspstudy.classloader.HelloWorld
存在的情况下我们可以使用如下代码即可实现调用hello
方法并输出:
HelloWorld t = new HelloWorld();
String str = t.hello();
System.out.println(str);
但是如果com.hspstudy.classloader.HelloWorld
根本就不存在于我们的classpath
,那么我们可以使用自定义类加载器重写findClass
方法,然后在调用defineClass
方法的时候传入HelloWorld
类的字节码的方式来向JVM中定义一个HelloWorld
类,最后通过反射机制就可以调用HelloWorld
类的hello
方法了。
import java.lang.reflect.Method;
public class TestClassLoader extends ClassLoader {
// HelloWorld类名
private static String testClassName = "com.hspstudy.classloader.HelloWorld";
// HelloWorld类字节码
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 52, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
101, 70, 105, 108, 101, 1, 0, 15, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46,
106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114,
108, 100, 126, 1, 0, 35, 99, 111, 109, 47, 104, 115, 112, 115, 116, 117, 100, 121, 47,
99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 72, 101, 108, 108, 111, 87, 111,
114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101,
99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29,
0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0,
3, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0,
0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12
};
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理HelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义HelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
// 创建自定义的类加载器
TestClassLoader loader = new TestClassLoader();
try {
// 使用自定义的类加载器加载HelloWorld类
Class testClass = loader.loadClass(testClassName);
// 反射创建HelloWorld类,等价于 HelloWorld t = new HelloWorld();
Object testInstance = testClass.newInstance();
// 反射获取hello方法
Method method = testInstance.getClass().getMethod("hello");
// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}