java的类字节码文件如何加载到内存中的?

类加载过程

类加载的触发条件

JVM并不会一次性加载所有类,而是采用“按需加载”策略,即在需要的时候才会进行类的加载。

触发类加载的操作

一般在这些情况下,如果类没有被加载,那么会被自动加载:

  • 使用 new 关键字创建对象时
MyClass obj = new MyClass();
  • 访问类的静态变量(包括读取或写入,不是 final 修饰的常量)时
int value = MyClass.staticField;
  • 调用类的静态方法时
MyClass.staticMethod();
  • 使用反射机制(如 Class.forName()Class.getMethod() 等)时
Class<?> clazz = Class.forName("com.example.MyClass");
  • 子类初始化时,如果父类尚未初始化,会先触发父类的加载
class A { static int a = 10; }
class B extends A { static int b = 20; }
System.out.println(B.b); // 先加载 A,再加载 B
  • 作为 main 方法所在的类
public class Main {
public static void main(String[] args) { } // Main 类被加载
}

不会触发类加载的操作

  • 访问 final 修饰的静态变量,final 变量在编译时已确定,不会触发类的初始化
class A {
static final int CONSTANT = 10;
}
System.out.println(A.CONSTANT); // A 类不会被加载
  • 通过数组定义类的引用,只是创建了一个引用类型数组,并未真正使用A类
A[] array = new A[10];	// A 类不会被加载
  • 通过子类访问父类的静态变量
class A {
static int a = 10;
}
class B extends A {}
System.out.println(B.a); // 只会触发 A 的加载,B 不会被加载

类加载过程

Java 类的加载过程主要包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)五个阶段

加载

  1. 通过类的全限定名查找字节码文件(如.class文件)
  2. 将字节码文件转换为内存中的二进制数据
  3. 在方法区(JDK 8之前)或元空间(JDK 8及之后)中创建类的运行时数据结构
  4. 在堆内存中生成一个Class对象,作为方法区或元空间中数据的访问入口

验证

  • 检查字节码文件的正确性和安全性,确保其符合JVM规范
  • 包括文件格式验证、元数据验证、字节码验证和符号引用验证
    • 文件格式验证:是否符合 .class 文件规范(开头八位数,即魔数CAFEBABE
    • 元数据验证:类、方法、字段的定义是否合法
    • 字节码检查:方法调用是否合法,是否越界访问
    • 符号引用检查:解析阶段前,确保引用的类、方法、字段存在
  • 如果验证失败,会抛出 VerifyError

准备

  • 类的静态变量分配内存,并赋默认值(非初始化值)
  • 变量赋的是默认值int -> 0boolean -> falsereference -> null)。
  • 如果静态变量是常量(final),则直接赋值为指定的初始值

解析

将常量池中的符号引用转换为直接引用

符号引用:类的字段、方法的名称和描述符

  • com/example/A 之类的字符串引用类
  • A.f 代表字段
  • A.m() 代表方法

直接引用:字段、方法在内存中的实际地址

初始化

执行类的初始化代码,包括静态变量的赋值和静态代码块的执行

按照定义顺序执行类的静态初始化代码,包括:

  • 静态变量赋值
  • 静态代码块
  • 执行 <clinit>() 方法

执行顺序:

  1. 父类静态变量 & 静态代码块
  2. 子类静态变量 & 静态代码块
  3. 父类实例变量 & 构造代码块
  4. 父类构造方法
  5. 子类实例变量 & 构造代码块
  6. 子类构造方法

类的卸载

条件:

  • 类的 ClassLoader 被回收
  • 该类的所有实例都不可达(GC)
  • 没有其他类引用该类

特点:

  • 只会卸载用户自定义类,JDK的核心类不会被卸载

类加载器

Java提供了类加载器,以便更好地控制类加载,可以自定义类加载器,也可以使用官方自带的类加载器去加载类。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。也就是说,一个类可以由不同的类加载器加载,并且,不同的类加载器加载的出来的类,即使来自同一个Class文件,也是不同的,只有两个类来自同一个Class文件并且是由同一个类加载器加载的,才能判断为是同一个

默认情况下,所有的类都是由JDK自带的类加载器进行加载,类加载器实现了类的动态加载、隔离和安全性控制。

概述

类加载器是JVM的一部分,用于查找、加载和定义Java类。主要负责:

  1. 加载类文件(从磁盘、网络、JAR包等)
  2. 将字节码转换为 Class 对象
  3. 缓存已加载的类,避免重复加载

Java采用双亲委派机制来组织多个类加载器,确保类的加载顺序和安全性

主要有三种由JDK提供的类加载器:

类加载器 作用 负责加载的类
Bootstrap ClassLoader(启动类加载器) 最顶层的类加载器,由 JVM 实现 rt.jar(如 java.lang.Stringjava.util.List
Extension ClassLoader(扩展类加载器) 加载扩展库中的类 lib/ext 目录下的 JAR
Application ClassLoader(应用类加载器) 加载应用程序的类 classpath 指定的类

工作机制

  1. 检查缓存:如果类已加载,则直接返回Class对象(避免重复加载)
  2. 双亲委派
    • 先让父类加载器尝试加载类
    • 如果找不到,再由当前类加载器加载
  3. 转换&解析
    • 将字节码转换成Class对象,并存入JVM方法区
  4. 返回Class对象,允许实例化对象

双亲委派

  1. 当一个类加载器要加载某个类时,先让自己的“父加载器”尝试加载
  2. 如果父加载器找不到该类,才由当前类加载器自己加载

工作流程:

  1. 类加载请求从最底层的ClassLoader向上层传递。
  2. 顶层加载器(Bootstrap ClassLoader)先尝试加载
  3. 如果上层加载器找不到类,才交给当前类加载器进行加载
  4. 如果类已加载,直接返回Class对象

parents_

自定义类加载器

Java允许自定义类加载器,用于加载网络、加密、动态生成的类,通过继承 java.lang.ClassLoader 类,可以实现自定义的类加载器。举例:

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
private String classPath; // 类路径

public MyClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 读取字节码文件
byte[] data = loadClassData(name);
// 生成 Class 对象
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException("类未找到: " + name, e);
}
}

// 加载字节码文件
private byte[] loadClassData(String name) throws IOException {
String path = classPath + name.replace(".", "/") + ".class";
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}

public static void main(String[] args) throws Exception {
// 创建自定义类加载器
MyClassLoader myClassLoader = new MyClassLoader("path/to/classes/");
// 加载类
Class<?> clazz = myClassLoader.loadClass("com.example.MyClass");
// 实例化对象
Object obj = clazz.getDeclaredConstructor().newInstance();
System.out.println("类加载器: " + clazz.getClassLoader());
}
}

findClass方法:

  • 自定义类加载器需要重写findClass方法,用于加载类的字节码文件

defineClass方法:

  • 将字节码文件转换为Class对象

loadClassData方法:

  • 从指定路径读取字节码文件