本文介绍 JVM 的类加载过程,以及类加载器
什么是类加载
java 的源码编译为字节码 Class 文件,需要加载到 JVM
中才能运行,类加载过程就是如何将 Class 文件加载到 JVM 中的过程
注:
不要和对象的实例化搞混,这个过程不涉及创建对象
只有第一次使用这个类时才会加载
加载流程
类加载过程.drawio
loading(加载):
将 class 文件(二进制数据)写入内存
转化为 jvm 能识别的数据结构
在内存中生成代表该类的 java.lang.Class
对象,作为方法区访问该类的入口
Linking(链接):将内存中的 class 文件合并至 JVM
中,分为验证、准备、解析三个阶段
Verification(验证):对 class
文件进行合法校验,如:字节码格式、语法规范、引用验证
Preparation(准备):为类的静态变量 分配内存(在方法区中),此时静态变量的值为默认值,如static int A = 9;
这一步会为
A 在方法区中申请 4 字节的空间,由于该空间的二进制位全 0,所以此时 A =
0
注:
如果是public final static int A = 9;
则此时 A =
9,这是因为 static final 修饰的变量在编译期间会生成 ConstantValue
属性,在类加载的准备阶段根据 ConstantValue 的值为该字段赋值
static final
变量没有默认值,必须显式地赋值,否则编译时会报错
1.8之后,字符串常量放到堆空间中
Resolution(解析):将常量池中的符号引用解析为直接引用(目标的指针、偏移量、句柄)
Initializing(初始化):静态变量赋初始值,如static int A = 9;
这一步会将
A 赋为 9
类加载器
上面介绍了一个类加载到 JVM 内存的过程,类加载器执行其中的 loading
过程
分类
Bootstrap
Extension
App
Custom ClassLoader
每个类加载器都有一块缓存,保存自己加载过的类
类加载器.drawio
双亲委派
工作原理
一个类加载器收到了类加载的请求,先到自己的缓存中查找是否加载过
如果缓存中存在,则返回加载的类;如果缓存中不存在,交给父加载器
如果父加载器的缓存中存在,则返回加载到JVM内存的地址;如果没有,继续交给父加载器,直到
BootStrap
如果 BootStrap 的缓存中也不存在,开始从负责的范围中寻找
如果 BootStrap 负责的范围内找不到类的 Class
文件,依次交给子加载器
如果某个加载器在负责的范围内找到了要加载的类,写入自己的缓存区,并返回地址
上述步骤,1-3 为自下而上,4-6 为自上而下
为什么要有双亲委派来加载类?
安全性:如果核心类如 java.lang.String
可以由开发者自定义的类加载器加载,则开发者可以覆盖该类型,在里面执行恶意代码。如果使用双亲委派,自定义类加载器的缓存区中没有
java.lang.String 类型,只能交给上层加载器,从而避免了这个问题
源码分析
类加载源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime(); try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
查看一个类的加载器
1 System.out.println("String ClassLoader: " + String.class.getClassLoader());j
自定义类加载器
为什么需要自定义类加载器
举几个自定义类加载器的应用场景:
源码加密:Java
代码可以被反编译,如果需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用
JVM 的 ClassLoader 去加载类了,这时就需要自定义 ClassLoader
在加载类的时候先解密,然后再加载
从非标准源加载代码:如果你的字节码是放在数据库、某个服务器,就可以自定义类加载器,从指定的来源加载类
防止多个项目间出现重名的类:比如 Tomcat
需要部署多个项目,无法保证不同项目之间没有重名(package +
类名)的类,如果只有用一个加载器,无法同时加载多个重名的类(见下文例子)
热加载(Hot
Swap),热加载需要重新加载类,由于系统级别的加载器不能手动创建新的加载器对象,只要类加载过,就只能从缓存中读,不能清除缓存,无法重新加载,但是自定义的类加载器可以随意
new 新的,从而重新加载覆盖原有的类对象
热加载是针对单个字节码文件,重新编译后,不需要重启进程,应用程序就可以使用新的class
文件
源来:Class
Loaders in Java JVM——自定义类加载器
What
is the use of Custom Class Loader
如何自定义类加载器
继承 ClassLoader,重写 findClass,不建议重写 loadClass,因为重写
loadClass 如果打破了双亲委派,当要加载的类依赖于 java.lang.String
等需要父加载器加载的类,则会因无法加载而报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package org.example;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.nio.ByteBuffer;import java.nio.channels.Channels;import java.nio.channels.FileChannel;import java.nio.channels.WritableByteChannel;public class CustomerClassLoader extends ClassLoader { private String classFilePath; public customerClassLoader () {} public customerClassLoader (String classFilePath) { this .classFilePath = classFilePath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte [] bytes = getClassBytes(this .classFilePath); Class<?> c = this .defineClass(name, bytes, 0 , bytes.length); return c; } catch (IOException e) { throw new RuntimeException (e); } } private byte [] getClassBytes(String filePath) throws IOException { File file = new File (filePath); FileInputStream inputStream = new FileInputStream (file); FileChannel channel = inputStream.getChannel(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream (); WritableByteChannel writableByteChannel = Channels.newChannel(outputStream); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 ); while (true ) { int i = channel.read(byteBuffer); if (i == 0 || i == -1 ) { break ; } byteBuffer.flip(); writableByteChannel.write(byteBuffer); byteBuffer.clear(); } inputStream.close(); return outputStream.toByteArray(); } public static void main (String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CustomerClassLoader customerClassLoader = new customerClassLoader ("/Users/zhangjiaming/Desktop/consumeClassLoader/People.class" ); Class<?> clazz = customerClassLoader.loadClass("People" ); Constructor<?> constructor = clazz.getDeclaredConstructor(String.class); Object obj = constructor.newInstance("Bob" ); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } }
image-20230108134131391
自定义类加载器的应用
加载多个同名类
应用场景
比如一个方法有多个版本,我们希望同时使用多个版本的类;Tomcat
中运行了多个项目,项目中存在重名的类
一个类加载器为什么不能加载多个同名类?
因为类加载器 L 会先到缓存中查找是否已加载,对于同名类(package +
类名),当 A 项目中的 com.model.Dog.java 已经加载了,L 就不会再去加载 B
项目中的 com.model.Dog.java,这样 B 项目用了错误的类,导致 B
项目运行报错
验证
准备两个 java 文件,放到 A,B两个路径下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.zjm.model;public class People { private String name; public People (String name) { this .name = name; } public String toString () { return "I am a A people, my name is " + name; } public String toString () { return "I am a B people, my name is " + name; } }
自定义类加载器用上文的,修改 main 函数,令一个类加载器加载
1 2 3 4 5 6 7 8 9 10 11 12 13 private static void test (String path, String className, CustomerClassLoader customerClassLoader) throws Exception { customerClassLoader.setClassFilePath(path); Class<?> clazz = customerClassLoader.loadClass(className); Constructor<?> constructor = clazz.getDeclaredConstructor(String.class); Object obj = constructor.newInstance("Bob" ); System.out.println(obj); } public static void main (String[] args) throws Exception { CustomerClassLoader customerClassLoader = new CustomerClassLoader (); test("/Users/zhangjiaming/Desktop/cunstomerClassLoader/A/People.class" , "com.zjm.model.People" , customerClassLoader); test("/Users/zhangjiaming/Desktop/cunstomerClassLoader/B/People.class" , "com.zjm.model.People" , customerClassLoader); }
image-20230110140323056
可以发现,第二次没有加载新的类,而是使用了已加载的类,所以要使用不同的类加载器,如下:
1 2 3 4 5 6 public static void main (String[] args) throws Exception { ConsumerClassLoader consumerClassLoader = new ConsumerClassLoader (); test("/Users/zhangjiaming/Desktop/consumeClassLoader/A/People.class" , consumerClassLoader); ConsumerClassLoader consumerClassLoader1 = new ConsumerClassLoader (); test("/Users/zhangjiaming/Desktop/consumeClassLoader/B/People.class" , consumerClassLoader1); }
image-20230110140449779
热加载
应用场景
修改代码并编译后,不需要重启服务,直接生效
实现思路
监听 Class 文件变化,当 Class
文件变化时,创建新的类加载器加载类,覆盖原来的 Class 对象
验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static void test (String path) throws Exception { Class<?> clazz = null ; for (int i = 0 ; i < 1000 ; i++) { ConsumerClassLoader consumerClassLoader = new ConsumerClassLoader (); consumerClassLoader.setClassFilePath(path); clazz = consumerClassLoader.loadClass("com.zjm.model.People" ); Constructor<?> constructor = clazz.getDeclaredConstructor(String.class); Object obj = constructor.newInstance("Bob" ); System.out.println(obj); Thread.sleep(1000 ); } } public static void main (String[] args) throws Exception { test("/Users/zhangjiaming/Desktop/consumeClassLoader/A/People.class" ); }
image-20230110192624216
这里只是一个简单的例子,要实现热加载还是有很多点要考虑,待后续讨论
字节码加密
通过某种加密算法修改 Class 文件,然后通过解密算法解密,再加载
参考文献