字节码的组成

+——————————+
| 魔数(Magic Number) |
+——————————+
| 版本信息(Version) |
+——————————+
| 常量池(Constant Pool) |
+——————————+
| 访问标志(Access Flags)|
+——————————+
| 类信息(This/ Super) |
+——————————+
| 接口(Interfaces) |
+——————————+
| 字段(Fields) |
+——————————+
| 方法(Methods) |
+——————————+
| 属性(Attributes) |
+——————————+

  1. 一个简单的HelloWorld程序
    1
    2
    3
    4
    5
    6
    public class HelloWorld {
    public static void main(String[] args) {
    System.out.println("Hello, World!");
    }
    }

  2. 打开class文件
  • 0-3字节(ca fe ba be),表示它是否是class类型的文件

    在Java中,所有的.class文件都以魔数ca fe ba be开头,这个魔数的前4个字节用于识别该文件是否为Java类文件,如果这个魔数不匹配,那么Java虚拟机将无法加载该文件。

  1. 类文件结构
  • 主版本号-44 = JDK版本

常量池

  • 常量池的组成
    当你编写 Java 代码后,编译器会将常量(如字面量、类名、字段名、方法签名等)存入 .class 文件中的常量池表(Constant Pool Table)。
Tag类型描述
1CONSTANT_Utf8UTF-8编码的字符串
3CONSTANT_Integer整型常量
4CONSTANT_Float浮点型常量
5CONSTANT_Long长整型常量(占两个槽)
6CONSTANT_Double双精度浮点常量(占两个槽)
7CONSTANT_Class类或接口的符号引用
8CONSTANT_String字符串字面量的引用
9CONSTANT_Fieldref字段的符号引用
10CONSTANT_Methodref方法的符号引用
11CONSTANT_InterfaceMethodref接口方法的引用
12CONSTANT_NameAndType字段或方法的名称与描述符
15CONSTANT_MethodHandle方法句柄
16CONSTANT_MethodType方法类型
18CONSTANT_InvokeDynamic动态调用信息
  • 常量池的特点
    • 字符串常量池是运行时常量池的一部分,存储字符串字面量,并支持字符串常量共享。
      1
      2
      3
      4
      5
      6
      7
          public class Test {
      public static void main(String[] args) {
      String s1 = "Hello";
      String s2 = "Hello";
      System.out.println(s1 == s2); // true,因为指向相同的字符串常量池对象
      }
      }

      s1和s2会指向同一地址

字节码指令

  • 解析 i=i++
    1
    2
    3
    4
    5
    6
    7
    public class HelloWorld {
    public static void main(String[] args) {
    int i=0;
    i=i++;
    System.out.println(i);
    }
    }
  • 这是一个简单的i=i++,我们来看这个程序的字节码指令
    1
    2
    3
    4
    5
    6
    7
    8
    9
     0 iconst_0
    1 istore_1
    2 iload_1
    3 iinc 1 by 1
    6 istore_1
    7 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
    10 iload_1
    11 invokevirtual #13 <java/io/PrintStream.println : (I)V>
    14 return
    当这个程序开始运行后:
    1. 在栈中开辟内存
      会开启两个空间,数组[0] 是 main函数的参数 args
    2. iconst_0
      将0放入操作数栈中
    3. istore_1
      将操作数栈中的值放入数组[1]中
    4. iload_1
      数组[1]中的值放入操作数栈中
    5. iinc 1 by 1
      将 数组[1]中的值 加 1
    6. istore_1
      将操作数栈中的值放入数组[1]中
    7. getstatic #7
      从类 System 中获取静态字段 out
    8. iload_1
      数组[1]中的值放入操作数栈中
    9. invokevirtual #13
      调用对象的实例方法。
  1. return
    方法结束

类的生命周期

加载


连接(验证 → 准备 → 解析)


初始化(执行


使用


卸载(由 GC 回收 Class 对象)

加载

  • JVM 通过类的全限定名查找 .class 文件 ,由类加载器将字节码读入内存(即方法区),创建 Class 对象
  • 在堆中创建一个 java.lang.Class 对象用于封装方法区中的类信息,程序中我们使用 MyClass.class 或 Class.forName() 得到的就是这个对象

连接

连接是 JVM 将类的二进制表示转换为可以被虚拟机使用的状态的过程,包括验证、准备和解析三个子阶段。

🧪 验证

✅ 目的:
确保字节码文件的合法性、安全性、符合 JVM 规范,避免崩溃或恶意代码入侵。

🔍 主要验证内容:
文件格式验证:字节码文件的魔数、版本号等是否合法。
元数据验证:类的父类是否存在,字段类型是否正确。
字节码验证:方法中指令是否合法,操作数栈使用是否正确。
符号引用验证:常量池中的类名、字段、方法是否合法。

🧱 准备

✅ 目的:
为类中的 静态变量(static 字段)分配内存,并赋予默认值。
🔍 特点:
赋默认值,不是编写代码中设置的值(final除外)。
🌰 示例:

1
2
3
4
public class Example {
static int a = 10;
static final int b = 20;
}

准备阶段时:
a = 0(默认值)
b 会直接在编译期被放入常量池(JVM 优化,可能不参与连接)

数据类型默认值
byte0
short0
int0
long0L
float0.0f
double0.0d
char\u0000(空字符)
booleanfalse
引用类型(如 String)null

🧩 解析

✅ 目的:
将常量池中的符号引用(如类名、字段名、方法名)转为 直接引用(指向内存地址的指针、偏移量等)。

类型举例
符号引用“java/lang/String”
直接引用指向 String.class 的地址

✅ 初始化

✅ 目的:
初始化阶段指执行类的初始化方法,完成静态变量的显式赋值和静态代码块的执行,确保类处于可用状态。

  • 初始化顺序示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Example {
    static int a = 10;
    static {
    System.out.println("静态代码块执行");
    a = 20;
    }

    public static void main(String[] args) {
    System.out.println("a = " + a);
    }
    }
    初始化过程:
  1. 准备阶段时,a 被赋默认值 0。

  2. 初始化阶段执行

    • a = 10;

    • 执行静态代码块,a = 20;,同时打印输出。

  3. main 方法输出:a = 20

📋 何时触发初始化:
首次主动使用类时触发,如:

- 创建类实例(new)
- 访问类的静态字段(非 final 常量)
- 调用静态方法
- 使用反射 Class.forName()
- 初始化子类时,先初始化父类

类加载器

常见的类加载器:

类加载器简称是否 Java 实现加载内容类加载路径
启动类加载器Bootstrap❌(C/C++ 实现)JDK 核心类库,如 java.lang.*java.util.*$JAVA_HOME/lib/rt.jar(JDK8及以下)
扩展类加载器ExtensionJDK 扩展类库$JAVA_HOME/lib/ext/-Djava.ext.dirs
应用类加载器AppClassLoader应用程序类(用户编写的类)classpath 环境变量指定的路径
自定义类加载器Custom根据用户定义的逻辑加载类自定义路径、加密文件、网络等

双亲委派机制

双亲委派机制的核心是解决一个类应该由哪个类加载器去加载

工作原理

双亲委派机制的执行流程如下:

  1. 当某个类加载器收到类加载请求时,它不会立刻去尝试加载这个类;
  2. 它会先将这个请求委托给父加载器去加载;
  3. 如果父加载器还有父加载器,则继续向上委托,直到顶层的启动类加载器(Bootstrap ClassLoader);
  4. 如果父加载器能找到这个类,就直接返回类的Class对象;
  5. 如果父加载器找不到,才由当前类加载器尝试自己去加载这个类。

    类加载器的层次结构

Bootstrap ClassLoader(启动类加载器)

ExtClassLoader(扩展类加载器)

AppClassLoader(应用程序类加载器)

自定义ClassLoader

为什么使用双亲委派机制

安全性:防止核心 Java 类被恶意覆盖或篡改。例如自定义一个 java.lang.String 类。
避免重复加载:保证同一个类只会被一个类加载器加载一次,确保类的唯一性。
模块化和可扩展性:通过自定义类加载器,实现模块隔离(例如 OSGi、Tomcat 的 WebAppClassLoader)。

打破双亲委派的情况

有些场景中需要打破双亲委派,例如:
热部署(如 Tomcat 加载 Web 应用);
插件化开发(如 IDEA 插件机制);
某些框架需要优先加载自己的类版本(比如 Spring Boot 的 LaunchedURLClassLoader)。

方法:
自定义类加载器