深入理解JVM - 类加载机制
类加载过程
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking),如图:
image.png加载、验证、准备、初始化和卸载这五个阶段的顺序是确定,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始。
加载
在加载阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,不要混淆了。
验证
验证的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,如:
- 是否以魔数
0xCAFEBABE
开头 - 主、次版本号是否在当前Java虚拟机接受范围之内
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
- ......
元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求,如:
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
- ……
字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的,如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换总是有效的,例如
父类=子类对象
是合法的,返回来就是非法的 - ……
符号引用验证
符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,即解析阶段。主要目的是检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源,如:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问
- ……
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,当类变量被final修饰时,在准备阶段就直接会被复制,不是使用初始值,如:
public static int a = 123;
public static final int B = 123;
在准备阶段a的值是0,B的值是123。
下面是,基本数据类型的初始值:
类型 | 默认值 |
---|---|
int | 0 |
long | 0L |
byte | (byte)0 |
short | (short)0 |
char | '\u000' |
float | 0.0f |
double | 0.0d |
boolean | false |
reference | null |
解析阶段
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,引用的目标并不一定是已经加载到虚拟机内存当中的资源;
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在;
初始化
初始化阶段就是执行类构造器 <clinit>()
方法的过程,它是真正开始执行Java代码的阶段,比如给类属性赋真实的值。
public static int a = 123;
在初始化阶段后,a的值才等于123。
- <clinit>() 方法是由编译器自动收集类中的所有类变量(static变量)的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集顺序是源文件中的代码顺序;
- <clinit>() 方法不是必须的,如果我们的源文件中没有静态语句块和静态属性的赋值,那么久不会有<clinit>() 方法。
- <clinit>() 方法在多线程情况下会通过加锁的方式来保证同步,并且只会被执行一次
- 子类 <clinit>() 方法执行之前需要保证先执行父类的 <clinit>() 方法,所以Object类的 <clinit>() 方法是第一个执行的
类的加载时机
类的初始化时机
虚拟机没有规定什么时候开始加载一个类,但是严格规定了什么时候需要初始化一个类,有且仅有6种情况:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如:
- 使用new关键字实例化对象;
- 读取或设置一个类型的静态字段(final除外);
- 调用一个类的静态方法;
- 使用java.lang.reflect包的方法对类型进行反射调用的时候;
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
- 当使用JDK 7新加入的动态语言支持时;
- 当一个接口中定义了JDK 8新加入的默认方法时(被default关键字修饰的接口方法);
除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。
被动引用
通过子类引用父类的静态字段,不会导致子类初始化:
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
运行结果:
SuperClass init!
123
上述代码运行之后,只会输出“SuperClass init!”,而不会输出“SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
通过数组定义引用类,不会触发此类的初始化:
/**
* 被动使用类字段演示二:
* 通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization2 {
public static void main(String[] args) {
SubClass[] subClasses = new SubClass[1];
}
}
运行过后什么都没输出。
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的:
/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的
* 类的初始化
**/
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization3 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
运行结果:
hello world
通过javap -verbose NotInitialization3
我们可以发现hello world
已经在当前类的常量池了:
PS E:\> javap -verbose NotInitialization3.class
...
Constant pool:
#1 = Methodref #7.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #24 // com/xiaolyuh/ConstClass
#4 = String #25 // hello world
...
#25 = Utf8 hello world
...
反编译NotInitialization3.class结果,可以发现ConstClass.HELLOWORLD
被优化了,如下:
package com.xiaolyuh;
public class NotInitialization3 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
接口的初始化时机
编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。
接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
类加载器类型
-
启动类加载器(Bootstrap Class Loader):负责加载存放在
<JAVA_HOME>\lib
目录,或者被-Xbootclasspath
参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中; -
扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载
<JAVA_HOME>\lib\ext
目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库; -
应用程序类加载器(Application Class Loader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现,也称为"系统类加载器"。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器;
双亲委派模型
image.png如图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(ParentsDelegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程是:所有类的加载都委托给父加载器去完成,当父加载器无法加载这个类的时候,子加载器才会尝试加载。
双亲委派模型最大的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,保证同一个类只会被一个加载器加载。
双亲委派模型的实现
// 使用synchronized来保证线程安全
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
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载时
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
破坏双亲委派模型
双亲委派模型主要出现过3次较大规模“被破坏”的情况:
- 在1.2之前,由于实现自定义类加载器只有覆盖
loadClass()
方法,导致了双亲委派模型的破坏,在1.2之后引入了findClass()
方法之后得以解决。 - 基础类型无法调用回用户的代码,如JNDI、JDBC、JCE、JAXB和JBI等,他们的接口定义是基础类型,但是他们的实现是各各厂商,这就导致了基础类型需要调用用户代码。后来引入线程上下文类加载器(Thread Context ClassLoader)得以解决。
- 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(HotDeployment)等。
自定义类加载器
自定义类加载器需要继承ClassLoader
类,为了不破坏双亲委派模型,自定义类加器建议覆盖findClass()
方法,不建议覆盖loadClass()
方法。下面是我实现的一个加载加密class文件、防止反编译核心代码的类加载器。
加密Class文件
加密class文件的代码:
/**
* 加密Class文件
*
* @author yuhao.wang3
* @since 2020/1/20 10:39
*/
public class EncryptionClassFileTask extends RecursiveAction {
Logger logger = LoggerFactory.getLogger(EncryptionClassFileTask.class);
/**
* 公钥
*/
String publicKey;
/**
* 需要加密的目录
*/
private File file;
/**
* 需要加密的包名
*/
private List<String> packages;
/**
* 需要排除的Class名称
*/
private List<String> excludeClass;
/**
* @param file 需要加密文件的目录
* @param packages 需要加密的包名
* @param publicKey 公钥
*/
public EncryptionClassFileTask(File file, List<String> packages, String publicKey) {
this(file, packages, null, publicKey);
}
/**
* @param file 需要加密文件的目录
* @param packages 需要加密的包名
* @param excludeClass 需要排除的类名称
* @param publicKey 公钥
*/
public EncryptionClassFileTask(File file, List<String> packages, List<String> excludeClass, String publicKey) {
this.file = file;
this.excludeClass = excludeClass;
this.publicKey = publicKey;
this.packages = new ArrayList<>();
if (Objects.nonNull(packages)) {
for (String packageName : packages) {
this.packages.add(packageName.replace('.', File.separatorChar));
}
}
if (Objects.isNull(excludeClass)) {
this.excludeClass = new ArrayList<>();
}
this.excludeClass.add("RsaClassLoader");
}
@Override
protected void compute() {
if (Objects.isNull(file)) {
return;
}
File[] files = file.listFiles();
List<EncryptionClassFileTask> fileTasks = new ArrayList<>();
if (Objects.nonNull(files)) {
for (File f : files) {
// 拆分任务
if (f.isDirectory()) {
fileTasks.add(new EncryptionClassFileTask(f, packages, excludeClass, publicKey));
} else {
if (f.getAbsolutePath().endsWith(".class")) {
if (!CollectionUtils.isEmpty(excludeClass) && excludeClass.contains(f.getName().substring(0, f.getName().indexOf(".")))) {
continue;
}
// 如果packages为空直接加密文件夹下所有文件
if (CollectionUtils.isEmpty(packages)) {
encryptFile(f);
return;
}
// 如果packages不为空则加密指定报名下面的文件
for (String packageName : packages) {
if (f.getPath().contains(packageName.replace('.', File.separatorChar))) {
encryptFile(f);
return;
}
}
}
}
}
// 提交并执行任务
invokeAll(fileTasks);
for (EncryptionClassFileTask fileTask : fileTasks) {
// 等待任务执行完成
fileTask.join();
}
}
}
private void encryptFile(File file) {
try {
logger.info("加密[{}] 文件 开始", file.getPath());
byte[] bytes = RSAUtil.encryptByPublicKey(RSAUtil.toByteArray(file), publicKey);
try (FileChannel fc = new FileOutputStream(file.getPath()).getChannel()) {
ByteBuffer bb = ByteBuffer.wrap(bytes);
fc.write(bb);
logger.info("加密[{}] 文件 结束", file.getPath());
}
} catch (IOException e) {
logger.error("加密文件 {} 异常:{}", file.getPath(), e.getMessage(), e);
}
}
}
加密class文件的测试类:
public class EncryptionClassFileTaskTest {
public static void main(String[] args) throws Exception {
String testPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCE7vuntatVmQVp6xGlBa/U/cEkKtFjyhsTtn1inlYtw5KSasTfa/HMPwJKp1QchsGEt0usOkHHC9HuD8o1gKx/Dgjo6b/XGu6xhinyRjCJWLSHXGOq9VLryaThwZsRB4Bb5DU9NXkl8WE2ih8QEKO1143KeJ5SE38awi74im0dzQIDAQAB";
List<String> excludeClass = Lists.newArrayList("MainController");
List<String> packages = Lists.newArrayList("com.xiaolyuh.controller");
encryptionClassFile("D:\\aes_class", packages, excludeClass, testPublicKey);
}
private static void encryptionClassFile(String filePath, List<String> packages, List<String> excludeClass, String publicKey) throws Exception {
ForkJoinPool pool = new ForkJoinPool(16);
File classFile = new File(filePath);
if (!classFile.exists()) {
throw new NoSuchFileException("File does not exist!");
}
pool.invoke(new EncryptionClassFileTask(classFile, packages, excludeClass, publicKey));
}
}
输出:
16:38:03.396 [ForkJoinPool-1-worker-9] INFO com.xiaolyuh.utils.EncryptionClassFileTask - 加密[D:\aes_class\spring-boot-student-jvm-0.0.1-SNAPSHOT\BOOT-INF\classes\com\xiaolyuh\controller\UserController.class] 文件 开始
16:38:04.530 [ForkJoinPool-1-worker-9] INFO com.xiaolyuh.utils.EncryptionClassFileTask - 加密[D:\aes_class\spring-boot-student-jvm-0.0.1-SNAPSHOT\BOOT-INF\classes\com\xiaolyuh\controller\UserController.class] 文件 结束
解密Class文件加载器
/**
* RSA 加密的ClassLoader
*
* @author yuhao.wang3
* @since 2020/1/19 17:06
*/
public class RsaClassLoader extends ClassLoader {
private static final int MAGIC = 0xcafebabe;
Logger logger = LoggerFactory.getLogger(RsaClassLoader.class);
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
// 先使用父加载器加载
return getParent().loadClass(name);
} catch (Throwable t) {
return findClass(name);
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replace('.', File.separatorChar) + ".class";
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
logger.warn("请输入解密私钥,否则无法启动服务");
System.out.print("请输入解密私钥::");
String privateKey = br.readLine();
logger.info("解密[{}] 文件 开始", name);
byte[] bytes = RSAUtil.decryptByPrivateKey(RSAUtil.toByteArray(inputStream), privateKey);
logger.info("解密[{}] 文件 结束", name);
return this.defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
logger.info("解密 [{}] 文件异常: {}", name, e.getMessage(), e);
throw new ClassNotFoundException(String.format("解密 [%s] 文件异常: %s", name, e.getCause()));
}
}
}
测试类:
/**
* RSA 加密的ClassLoader
*
* @author yuhao.wang3
* @since 2020/1/19 17:06
*/
public class RsaClassLoaderTest {
public static void main(String[] args) throws Exception {
RsaClassLoader loader = new RsaClassLoader();
Object object = loader.loadClass("com.xiaolyuh.controller.UserController2").newInstance();
System.out.println("使用默认类加载器: class :" + object.getClass() + " ClassLoader:" + object.getClass().getClassLoader());
Object object2 = loader.loadClass("com.xiaolyuh.controller.UserController").newInstance();
System.out.println("使用自定义类加载器:class :" + object2.getClass() + " ClassLoader:" + object2.getClass().getClassLoader());
System.out.println(JSON.toJSONString(object2));
}
}
运行提示输入解密公钥,输入后类加载成功:
使用默认类加载器: class :class com.xiaolyuh.controller.UserController2 ClassLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
17:21:30.233 [main] WARN com.xiaolyuh.RsaClassLoader - 请输入解密私钥,否则无法启动服务
请输入解密私钥::MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAITu+6e1q1WZBWnrEaUFr9T9wSQq0WPKGxO2fWKeVi3DkpJqxN9r8c...
17:21:34.252 [main] INFO com.xiaolyuh.RsaClassLoader - 解密[com.xiaolyuh.controller.UserController] 文件 开始
17:21:34.737 [main] INFO com.xiaolyuh.RsaClassLoader - 解密[com.xiaolyuh.controller.UserController] 文件 结束
使用自定义类加载器:class :class com.xiaolyuh.controller.UserController ClassLoader:com.xiaolyuh.RsaClassLoader@1c93084c
源码
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-jvm工程
参考
《深入理解JAVA虚拟机》