【JVM】- 类加载与字节码结构1

52 阅读8分钟

类文件结构

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

1. 魔数(magic)和版本号

  • magic (4字节): 固定值0xCAFEBABE,用于识别类文件格式
  • minor_version (2字节): 次版本号
  • major_version (2字节): 主版本号,对应Java版本
    • Java 8: 52 (0x34)
    • Java 11: 55 (0x37)
    • Java 17: 61 (0x3D)

2. 常量池(constant_pool)

  • constant_pool_count (2字节): 常量池中项的数量加1(从1开始索引)
  • constant_pool[]: 常量池表,包含多种类型的常量:
    • Class信息 (CONSTANT_Class_info)
    • 字段和方法引用 (CONSTANT_Fieldref_info, CONSTANT_Methodref_info)
    • 字符串常量 (CONSTANT_String_info)
    • 数值常量 (CONSTANT_Integer_info, CONSTANT_Float_info等)
    • 名称和描述符 (CONSTANT_NameAndType_info)
    • 方法句柄和类型 (CONSTANT_MethodHandle_info, CONSTANT_MethodType_info)
    • 动态调用点 (CONSTANT_InvokeDynamic_info)

3. 类访问标志(access_flags)

表示类或接口的访问权限和属性,如:

  • ACC_PUBLIC (0x0001): public类
  • ACC_FINAL (0x0010): final类
  • ACC_SUPER (0x0020): 使用新的invokespecial语义
  • ACC_INTERFACE (0x0200): 接口
  • ACC_ABSTRACT (0x0400): 抽象类
  • ACC_SYNTHETIC (0x1000): 编译器生成的类
  • ACC_ANNOTATION (0x2000): 注解类型
  • ACC_ENUM (0x4000): 枚举类型

4. 类和父类信息

  • this_class (2字节): 指向常量池中该类名的索引
  • super_class (2字节): 指向常量池中父类名的索引(接口的super_class是Object)

5. 接口(interfaces)

  • interfaces_count (2字节): 实现的接口数量
  • interfaces[]: 每个元素是指向常量池中接口名的索引

6. 字段(fields)

  • fields_count (2字节): 字段数量
  • field_info[]: 字段表,每个字段包括:
    • 访问标志(如public, private, static等)
    • 名称索引(指向常量池)
    • 描述符索引(指向常量池)
    • 属性表(如ConstantValue, Synthetic等)

7. 方法(methods)

  • methods_count (2字节): 方法数量
  • method_info[]: 方法表,每个方法包括:
    • 访问标志(如public, synchronized等)
    • 名称索引(指向常量池)
    • 描述符索引(指向常量池)
    • 属性表(最重要的Code属性包含字节码)

8. 属性(attributes)

  • attributes_count (2字节): 属性数量
  • attribute_info[]: 属性表,可能包含:
    • SourceFile: 源文件名
    • InnerClasses: 内部类列表
    • EnclosingMethod: 用于局部类或匿名类
    • Synthetic: 表示由编译器生成
    • Signature: 泛型签名信息
    • RuntimeVisibleAnnotations: 运行时可见注解
    • BootstrapMethods: 用于invokedynamic指令

字节码执行流程

有一段java代码如下:

public class Demo02 {
    public static void main(String[] args) {
        int a = 10;
		int b = a++ + ++a + a--;
    }
}
  1. 常量池载入运行时常量池
  2. 方法字节码载入方法区
  3. main线程开始运行,分配栈帧内存 在这里插入图片描述

1. 变量初始化 a = 10

0: bipush 10      // 将常量10压入操作数栈
2: istore_1       // 将栈顶值(10)存储到局部变量1(a)

此时内存状态:

  • 局部变量表:a = 10
  • 操作数栈:[空]

2. 计算 a++ (第一个操作数)

3: iload_1        // 加载局部变量1(a)的值到栈顶 → 栈:[10]
4: iinc 1, 1      // 局部变量1(a)自增1 (a=11),注意这不会影响栈顶值

此时内存状态:

  • 局部变量表:a = 11
  • 操作数栈:[10] (a++表达式的值是自增前的值)

3. 计算 ++a (第二个操作数)

7: iinc 1, 1      // 局部变量1(a)先自增1 (a=12)
10: iload_1       // 然后加载a的值到栈顶 → 栈:[10, 12]

此时内存状态:

  • 局部变量表:a = 12
  • 操作数栈:[10, 12] (++a表达式的值是自增后的值)

4. 第一次加法 a++ + ++a

11: iadd          // 弹出栈顶两个值相加,结果压栈 → 10+12=22 → 栈:[22]

5. 计算 a-- (第三个操作数)

12: iload_1       // 加载a的值到栈顶 → 栈:[22, 12]
13: iinc 1, -1    // 局部变量1(a)自减1 (a=11),不影响栈顶值

此时内存状态:

  • 局部变量表:a = 11
  • 操作数栈:[22, 12] (a--表达式的值是自减前的值)

6. 第二次加法 (前两个之和) + a--

16: iadd          // 22+12=34 → 栈:[34]

7. 存储结果到b

17: istore_2      // 将栈顶值(34)存储到局部变量2(b)

最终内存状态:

  • 局部变量表:a = 11, b = 34
  • 操作数栈:[空]

完整字节码序列

0: bipush 10       // a = 10
2: istore_1
3: iload_1         // 开始计算a++
4: iinc 1, 1
7: iinc 1, 1       // 开始计算++a
10: iload_1
11: iadd           // 前两个相加
12: iload_1        // 开始计算a--
13: iinc 1, -1
16: iadd           // 与第三个相加
17: istore_2       // b = 结果

后置自增/减(i++):

  • 先使用变量的当前值参与运算
  • 执行自增/减操作
  • 字节码表现为先iload后iinc

前置自增/减(++i):

  • 先执行自增/减操作
  • 使用新值参与运算
  • 字节码表现为先iinc后iload

案例分析

分析i++

public class Demo02 {
    public static void main(String[] args) {
        int i = 0, x = 0;
        while(i < 10) {
            x = x++;
            ++i;
        }
        System.out.println(x); // 0
    }
}

由于是x++,所以x先iload进入操作数栈【0】,再执行iinc进行自增【1】。自增后进行复制,又将操作数栈中的x赋值给x【0】,此时操作数栈中x的值为0。一次循环后,x的值还是0;最终x输出0。

构造方法<cinit>()V

public class Demo03 {
    static int i = 10;
    static {
        i = 20;
    }
    static {
        i = 30;
    }

    public static void main(String[] args) {
        System.out.println(i); // 30
    }
}

编译器会按照从上至下的顺序, 收集所有的static静态代码块和静态成员赋值的代码,合并成一个特殊的方法<cinit>()V

静态变量和静态代码块按代码中的书写顺序依次执行,后执行的会覆盖前边的赋值。

构造方法<init>()V

public class Demo04 {
    private String a = "s1";
    {
        b = 20;
    }
    private int b = 10;
    {
        a = "s2";
    }
    public Demo04(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Demo04 d = new Demo04("s3", 30);
        System.out.println(d.a + " " + d.b); // s3 30
    }
}

编译器会按照从上往下的顺序,收集所有的{}代码块和成员变量赋值的代码,形成新的构造方法,但是原始构造方法内的代码总是在最后边。

方法调用

public class Demo05 {
    private void test1(){}
    private final void test2(){}
    public void test3(){}
    public static void test4(){}

    public static void main(String[] args) {
        Demo05 d = new Demo05();
        d.test1();
        d.test2();
        d.test3();
        d.test4();
        Demo05.test4();
    }
}

在这里插入图片描述

每次调用方法的时候都是先把对象入栈,调用方法后再出栈。 对于使用对象调用静态方法时(紫色框),先入栈再出栈,再调用,这样相当于多了两个无效的操作。所以如果要调用静态方法时,推荐使用类调用。

多态的原理

public class Demo06 {
    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal);
    }
    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }
}

abstract class Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}
class Dog extends Animal {
    public void eat() {
        System.out.println("啃骨头");
    }
}

class Cat extends Animal {
    public void eat() {
        System.out.println("吃鱼");
    }
}

运行时的内存状态:

  1. test(new Cat())调用时:
    • 堆中创建Cat对象
    • 方法区中Cat类的虚方法表(vtable)包含:
      • eat() -> Cat.eat()
      • toString() -> Animal.toString()
  2. 方法调用过程:
    • JVM通过对象头中的类指针找到Cat
    • 通过虚方法表找到实际要调用的eat()实现
    • toString()调用则直接使用Animal中的实现

finally案例1

public class Demo07 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        }catch (Exception e) {
            i = 20;
        }finally {
            i = 30;
        }
    }
}

finally中的代码会被复制三份,分别放入:try分支、catch能被匹配到的分支、catch不能被匹配到的分支,确保他一定被执行。 JVM使用异常表(Exception table)来确定异常处理跳转位置,每个条目定义了受保护的代码范围(from-to)、处理代码位置(target)和异常类型 在这里插入图片描述

finally案例2

public class Demo07 {
    public static int test() {
        try{
            int i = 1/0;
            return 10;
        }finally {
            return 20;
        }
    }
    public static void main(String[] args) {
        System.out.println(test()); // 20
    }
}

字节码如下:

public static int test();
  Code:
     0: iconst_1          // 将1压入栈
     1: iconst_0          // 将0压入栈
     2: idiv              // 执行除法(1/0),这里会抛出ArithmeticException
     3: istore_0          // (不会执行)存储结果到局部变量0
     4: bipush 10         // (不会执行)将10压入栈
     6: istore_1          // (不会执行)存储到局部变量1(临时返回值)
     7: bipush 20         // finally块开始:将20压入栈
     9: ireturn           // 直接从finally块返回20
     
     // 异常处理部分
    10: astore_2          // 异常对象存储到局部变量2
    11: bipush 20         // finally块:将20压入栈
    13: ireturn           // 从finally块返回20
     
Exception table:
     from    to  target type
         0     7    10   any

finally块中的return会完全覆盖try块中的return或抛出的异常,这题输出20而不会抛异常。(原本的ArithmeticException被丢弃,因为finally中有return) 控制流变化:

  • 正常情况下:try → finally(return)
  • 异常情况下:try → catch → finally(return)
  • 两种路径最终都执行finally中的return

fianlly 案例3

public class Demo08 {
    public static int test() {
        int i = 10;
        try{
            return i;
        }finally {
            i = 20;
        }
    }
    public static void main(String[] args) {
        System.out.println(test()); // 10
    }
}

如果在try中return值了,就算在finally中修改了这个值,返回的结果也仍然不会改变,因为在return之前会先做一个暂存(固定返回值),然后执行finally中的代码,再把暂存的值恢复到栈顶, 返回的还是之前暂存的值。在这里插入图片描述