彻底剖析JVM类加载机制
本文仍然基于JDK8版本,从JDK9模块化器,类加载器有一些变动。
0 javac编译
java代码
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
System.out.println("end");
}
}
javac 编译,javap -v -p 查看class文件
Classfile /F:/workspace/advanced-java/target/classes/com/lzp/java/jvm/classloader/Math.class
// 第1部分,描述信息:大小、修改时间、md5值等
Last modified 2022年1月8日; size 1006 bytes
MD5 checksum 4cece4543963b23a98cd219a59c1887c
Compiled from "Math.java"
// 第2部分,描述信息:编译版本
public class com.lzp.java.jvm.classloader.Math
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // com/lzp/java/jvm/classloader/Math
super_class: #11 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, attributes: 1
// 第3部分,常量池信息
Constant pool:
#1 = Methodref #11.#39 // java/lang/Object."<init>":()V
#2 = Class #40 // com/lzp/java/jvm/classloader/Math
#3 = Methodref #2.#39 // com/lzp/java/jvm/classloader/Math."<init>":()V
#4 = Methodref #2.#41 // com/lzp/java/jvm/classloader/Math.compute:()I
#5 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #44 // end
#7 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #47 // com/lzp/java/jvm/classloader/User
#9 = Methodref #8.#39 // com/lzp/java/jvm/classloader/User."<init>":()V
#10 = Fieldref #2.#48 // com/lzp/java/jvm/classloader/Math.user:Lcom/lzp/java/jvm/classloader/User;
#11 = Class #49 // java/lang/Object
#12 = Utf8 initData
#13 = Utf8 I
#14 = Utf8 ConstantValue
#15 = Integer 666
#16 = Utf8 user
#17 = Utf8 Lcom/lzp/java/jvm/classloader/User;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/lzp/java/jvm/classloader/Math;
#25 = Utf8 compute
#26 = Utf8 ()I
#27 = Utf8 a
#28 = Utf8 b
#29 = Utf8 c
#30 = Utf8 main
#31 = Utf8 ([Ljava/lang/String;)V
#32 = Utf8 args
#33 = Utf8 [Ljava/lang/String;
#34 = Utf8 math
#35 = Utf8 MethodParameters
#36 = Utf8 <clinit>
#37 = Utf8 SourceFile
#38 = Utf8 Math.java
#39 = NameAndType #18:#19 // "<init>":()V
#40 = Utf8 com/lzp/java/jvm/classloader/Math
#41 = NameAndType #25:#26 // compute:()I
#42 = Class #50 // java/lang/System
#43 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#44 = Utf8 end
#45 = Class #53 // java/io/PrintStream
#46 = NameAndType #54:#55 // println:(Ljava/lang/String;)V
#47 = Utf8 com/lzp/java/jvm/classloader/User
#48 = NameAndType #16:#17 // user:Lcom/lzp/java/jvm/classloader/User;
#49 = Utf8 java/lang/Object
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 java/io/PrintStream
#54 = Utf8 println
#55 = Utf8 (Ljava/lang/String;)V
{
// 第四部分,变量信息
public static final int initData;
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 666
public static com.lzp.java.jvm.classloader.User user;
descriptor: Lcom/lzp/java/jvm/classloader/User;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
public com.lzp.java.jvm.classloader.Math();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lzp/java/jvm/classloader/Math;
// 第五部分,方法信息
public int compute();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 9: 0
line 10: 2
line 11: 4
line 12: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/lzp/java/jvm/classloader/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/lzp/java/jvm/classloader/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String end
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 math Lcom/lzp/java/jvm/classloader/Math;
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #8 // class com/lzp/java/jvm/classloader/User
3: dup
4: invokespecial #9 // Method com/lzp/java/jvm/classloader/User."<init>":()V
7: putstatic #10 // Field user:Lcom/lzp/java/jvm/classloader/User;
10: return
LineNumberTable:
line 6: 0
}
方法中的#1/2,可以到ConstantPool找到对应符号。
参考字节码指令表:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html。
1 类加载过程
j经典的类加载过程如下图,包括加载、链接、初始化三部分。
1.1 加载class文件
字节码文件位于磁盘,当使用到某个类(例如,调用main()方法,new新对象),在磁盘中查找并通过IO读取文件的二进制流,转为方法区数据结构,并存放到方法区,在Java堆中产生 java.lang.Class
对象。Class对象是可以方法区的访问入口,用于Java反射机制,获取类的各种信息。
1.2 链接过程
验证:验证class文件是不是符合规范
-
文件格式的验证。验证是否以0XCAFEBABE开头,版本号是否合理
-
元数据验证。是否有父类,是否继承了final类(final类不能被继承),非抽象类实现了所有抽象方法。
-
字节码验证。(略)
-
符号引用验证。常量池中描述类是否存在,访问的方法或字段是否存在且有足够的权限。
-Xverify:none // 取消验证
准备:为类的静态变量分配內存,初始化为系统的初始值
final static修饰的变量:直接赋值为用户定义的值,比如 private final static int value=123,直接赋值123。
private static int value=123
,该阶段的值依然是0。
解析:符号引用转换成直接引用(静态链接)
Java代码中每个方法、方法参数都是符号,类加载放入方法区的常量池Constant pool中。
符号引用:应该可以理解成常量池中的这些字面量。【可能没理解对】
直接引用:符号对应代码被加载到JVM内存中的位置(指针、句柄)。
静态链接过程在类加载时完成,主要转换一些静态方法。动态链接是在程序运行期间完成的将符号引用替换为直接引用。
1.3 初始化(类初始化clinit-->初始化init)
执行< clinit>
方法, clinit
方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法。
-
初始化的顺序和源文件中的顺序一致
-
子类的
< clinit>
被调用前,会先调用父类的< clinit>
-
JVM会保证
clinit
方法的线程安全性
初始化时,如果实例化一个新对象,会调用<init>
方法对实例变量进行初始化,并执行对应的构造方法内的代码。
类加载过程是懒加载的,用到才会加载。
初始化示例
public class JVMTest2 {
static {
System.out.println("JVMTest2静态块");
}
{
System.out.println("JVMTest2构造块");
}
public JVMTest2() {
System.out.println("JVMTest2构造方法");
}
public static void main(String[] args) {
System.out.println("main方法");
new Sub();
}
}
class Super {
static {
System.out.println("Super静态代码块");
}
public Super() {
System.out.println("Super构造方法");
}
{
System.out.println("Super普通代码块");
}
}
class Sub extends Super {
static {
System.out.println("Sub静态代码块");
}
public Sub() {
System.out.println("Sub构造方法");
}
{
System.out.println("Sub普通代码块");
}
}
JVMTest2静态块
main方法
Super静态代码块
Sub静态代码块
Super普通代码块
Super构造方法
Sub普通代码块
Sub构造方法
执行main方法,并不需要创建JVMTest2实例。
对于普通代码块,以前认为是和clinit
一样顺序加载。其实是不一样的,普通代码块编译时对赋值语句和其他语句分别做了优化,如下赋值语句优化为int i = 1;
打印语句优化为构造方法的第一句。
源代码
public class JVMTest1 {
int i;
{
i = 1;
System.out.println("JVMTest1构造块");
}
public JVMTest1(){
System.out.println("JVMTest1构造方法");
}
}
反编译后的代码
public class JVMTest1 {
int i = 1;
public JVMTest1() {
System.out.println("JVMTest1构造块");
System.out.println("JVMTest1构造方法");
}
}
2 类加载器
查看当前JDK类加载器
public class PrintJDKClassLoader {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
ClassLoader parentParent = parent.getParent();
System.out.println(parentParent);
}
}
// JDK8
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@28a418fc
null
// JDK11
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1324409e
null
2.1 类加载器(JDK8)
类加载器初始化过程:Java通过调用jvm.dll文件创建JVM,创建一个引导类加载器(由C++实现),通过JVM启动器(sun.misc.Launcher)加载扩展类加载器和应用类加载器。
-
启动类加载器:负责加载lib目录下的核心类库。作为JVM的一部分,由C++实现。
-
扩展类/平台类加载器:负责加载lib目录下的ext扩展目录中的JAR 类包。
-
应用程序类加载器:负责加载用户类路径ClassPath路径下的类包,主要就是加载用户自己写的类。
-
自定义类加载器:负责加载用户自定义路径下的类包。
JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。
// Launcher构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
// 构造扩展类加载器,设置类加载器parent属性设为null。
var1 = Launcher.ExtClassLoader.getExtClassLoader();
// 构造应用类加载器,设置类加载器parent属性为扩展类加载器。
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
// 权限校验代码..
}
}
2.2 双亲委派模型
类加载器采用三层、双亲委派模型,类加载器的父子关系不是继承关系,而是组合关系。除了启动类加载器外,其他类加载器都是继承自ClassLoader类。
工作过程:类加载器收到类加载请求,首先判断类是否已经加载,如果未被加载,尝试将请求向上委派给父类加载器加载。当父类加载器无法完成加载任务,再由子类加载器尝试加载。
// ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 非启动类加载器
c = parent.loadClass(name, false);
} else {
// 启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载指定类
}
if (c == null) {
// 调用当前类加载器的findClass方法进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么使用双亲委派模型,感觉走了弯路?
双亲委派模型下,类加载请求总会被委派给最上层的启动类加载器。对于未加载的类来说,需要从底层走到顶层;如果用户定义的类已经被加载过,则不需要委派过程。
使用双亲委派机制有下面几个好处:
- 沙箱安全机制,防止核心类库代码被篡改。
- 避免类重复加载,父类加载器加载过,子类加载器不需要再次加载。
全盘负责委托机制
全盘负责 :即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class 通常 也由这个classloader负责载入。 委托机制 :先让parent(父)类加载器 寻找,只有在parent找不到的时候才从自己的类路径中去寻找。
参考Launcher构造方法
Thread.currentThread().setContextClassLoader(this.loader);
自定义类加载器
自定义类加载器操作主要是继承ClassLoader类,重写上面源码中的findClass(name)方法。
public class CustomClassLoaderTest {
static class CustomClassLoader extends ClassLoader {
private String classFilePath;
public CustomClassLoader(String classFilePath) {
this.classFilePath = classFilePath;
}
// 载入class数据流
private byte[] loadClassFile(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classFilePath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException{
try {
byte[] data = loadClassFile(name);
// 加载--链接--初始化等逻辑
return defineClass(name,data,0,data.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
public static void main(String[] args) throws Exception {
CustomClassLoader classLoader = new CustomClassLoader("F:");
Class<?> clazz = classLoader.loadClass("com.lzp.java.jvm.classloader.JVMTest");
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("add", null);
System.out.println(method.invoke(instance));
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
自定义类加载器的父加载器是应用类加载器。CustomClassLoader是使用AppClassLoader运行的,自然而然是父类加载器。
打破双亲委派机制
在一些场景下,打破双亲委派是必要的。例如Tomcat中可能有多个应用,引用了不同的Spring版本。打破双亲委派,可以实现应用隔离。
JVM使用loadClass方法实现双亲委派机制。重写loadClass方法,便可以打破双亲委派机制。
直接删除双亲委派代码是不可行的,Java代码继承自Object,总会需要双亲委派来加载核心代码。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 非自定义的类还是走双亲委派加载
if (!name.equals("com.lzp.java.jvm.classloader.JVMTest")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
注:JDK自带的核心库代码,是不允许自行配置修改的。例如,不可以将Object.class拷出来执行。沙箱隔离。