当前位置: 首页 > news >正文

【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--;}
}
  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();@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("吃鱼");}
}

运行时的内存状态:

  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),这里会抛出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中的代码,再把暂存的值恢复到栈顶, 返回的还是之前暂存的值。在这里插入图片描述

相关文章:

  • Spring AI详细使用教程:从入门到精通
  • RabbitMQ缓存详解:由来、发展、核心场景与实战应用
  • ubuntu之坑(十四)——安装FFmpeg进行本地视频推流(在海思平台上运行)
  • 软件工程的实践
  • ffmpeg subtitles 字幕不换行的问题解决方案
  • Yarn与NPM缓存存储目录迁移
  • MySQL查询缓存深度剖析
  • ffmpeg rtmp推流源码分析
  • 3GPP协议PDF下载
  • 【信创-k8s】重磅-鲲鹏arm+麒麟V10离线部署k8s1.30+kubesphere4.1.3
  • 从SQL Server到分布式大数据平台:重构企业数据架构
  • 四数之和-力扣
  • Python让自动驾驶“看见未来”:环境建模那些事儿
  • GaussDB 分布式数据库调优(架构到全链路优化)
  • 前端项目Excel数据导出同时出现中英文表头错乱情况解决方案。
  • 用Java实现常见排序算法详解
  • java中合并音频
  • C#使用ExcelDataReader高效读取excel文件写入数据库
  • 【Qt】Qt控件
  • 三星MZQL2960HCJR-00BAL高性能固态硬盘控制器SSD云计算和高端存储专用 电子元器件解析
  • 湖南营销型网站建设报价/全球疫情最新数据消息
  • 有什么网站用名字做图片/百度快照是干什么的
  • wordpress网站图片加载速度慢/头条今日头条新闻
  • dw做网站投票/万网域名注册查询
  • 个人网站备案后可以做行业内容吗/windows优化大师的作用
  • 展示型网站建设流程/seo页面链接优化