【JVM】- 类加载与字节码结构1
类文件结构
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--;}
}
- 常量池载入
运行时常量池
中 - 方法字节码载入
方法区
中 - 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();@Overridepublic 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("吃鱼");}
}
运行时的内存状态:
test(new Cat())
调用时:- 堆中创建
Cat
对象 - 方法区中
Cat
类的虚方法表(vtable)包含:eat()
->Cat.eat()
toString()
->Animal.toString()
- 堆中创建
- 方法调用过程:
- JVM通过对象头中的类指针找到
Cat
类 - 通过虚方法表找到实际要调用的
eat()
实现 toString()
调用则直接使用Animal
中的实现
- JVM通过对象头中的类指针找到
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),这里会抛出ArithmeticException3: istore_0 // (不会执行)存储结果到局部变量04: bipush 10 // (不会执行)将10压入栈6: istore_1 // (不会执行)存储到局部变量1(临时返回值)7: bipush 20 // finally块开始:将20压入栈9: ireturn // 直接从finally块返回20// 异常处理部分10: astore_2 // 异常对象存储到局部变量211: bipush 20 // finally块:将20压入栈13: ireturn // 从finally块返回20Exception table:from to target type0 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中的代码,再把暂存的值恢复到栈顶, 返回的还是之前暂存的值。