01. Java字节码技术:不积细流,无以成江河(1)
kimmking
Java 中的字节码,英文名为 bytecode, 是 Java 代码编译后的中间代码格式。JVM 需要读取并解析字节码才能执行相应的任务。
从技术人员的角度看,Java 字节码是 JVM 的指令集。JVM 加载字节码格式的 class 文件,校验之后通过 JIT 编译器转换为本地机器代码执行。 简单说字节码就是我们编写的 Java 应用程序大厦的每一块砖,如果没有字节码的支撑,大家编写的代码也就没有了用武之地,无法运行。也可以说,Java 字节码就是 JVM 执行的指令格式。
那么我们为什么需要掌握它呢?不管用什么编程语言,对于卓越而有追求的程序员,都能深入去探索一些技术细节,在需要的时候,可以在代码被执行前解读和理解中间形式的代码。对于 Java 来说,中间代码格式就是 Java 字节码。 了解字节码及其工作原理,对于编写高性能代码至关重要,对于深入分析和排查问题也有一定作用,所以我们要想深入了解 JVM 来说,了解字节码也是夯实基础的一项基本功。同时对于我们开发人员来时,不了解平台的底层原理和实现细节,想要职业进阶绝对不是长久之计,毕竟我们都希望成为更好的程序员, 对吧?
任何有实际经验的开发者都知道,业务系统总不可能没有 BUG,了解字节码以及 Java 编译器会生成什么样的字节码,才能说具备扎实的 JVM 功底,会在排查问题和分析错误时非常有用,也能更好地解决问题。
而对于工具领域和程序分析来说, 字节码就是必不可少的基础知识了,通过修改字节码来调整程序的行为是司空见惯的事情。想了解分析器 (Profiler),Mock 框架,AOP 等工具和技术这一类工具,则必须完全了解 Java 字节码。
4.1 Java 字节码简介
有一件有趣的事情,就如名称所示, Java bytecode 由单字节 (byte) 的指令组成,理论上最多支持 256 个操作码 (opcode)。实际上 Java 只使用了 200 左右的操作码, 还有一些操作码则保留给调试操作。
操作码, 下面称为 指令, 主要由类型前缀和操作名称两部分组成。
例如,'i' 前缀代表 ‘integer’,所以,'iadd' 很容易理解, 表示对整数执行加法运算。
根据指令的性质,主要分为四个大类:
栈操作指令,包括与局部变量交互的指令
程序流程控制指令
对象操作指令,包括方法调用指令
算术运算以及类型转换指令
此外还有一些执行专门任务的指令,比如同步 (synchronization) 指令,以及抛出异常相关的指令等等。下文会对这些指令进行详细的讲解。
4.2 获取字节码清单
可以用 javap 工具来获取 class 文件中的指令清单。 javap 是标准 JDK 内置的一款工具, 专门用于反编译 class 文件。
让我们从头开始, 先创建一个简单的类,后面再慢慢扩充。
代码很简单, main 方法中 new 了一个对象而已。然后我们编译这个类:
使用 javac 编译 ,或者在 IDEA 或者 Eclipse 等集成开发工具自动编译,基本上是等效的。只要能找到对应的 class 即可。
javac 不指定 -d 参数编译后生成的 .class 文件默认和源代码在同一个目录。
注意: javac 工具默认开启了优化功能, 生成的字节码中没有局部变量表 (LocalVariableTable),相当于局部变量名称被擦除。如果需要这些调试信息, 在编译时请加上 -g 选项。有兴趣的同学可以试试两种方式的区别,并对比结果。
JDK 自带工具的详细用法, 请使用: javac -help 或者 javap -help 来查看 ; 其他类似。
然后使用 javap 工具来执行反编译, 获取字节码清单:
javap 还是比较聪明的, 使用包名或者相对路径都可以反编译成功, 反编译后的结果如下所示:
OK,我们成功获取到了字节码清单, 下面进行简单的解读。
4.3 解读字节码清单
可以看到,反编译后的代码清单中, 有一个默认的构造函数 public demo.jvm0104.HelloByteCode(), 以及 main 方法。
刚学 Java 时我们就知道, 如果不定义任何构造函数,就会有一个默认的无参构造函数,这里再次验证了这个知识点。好吧,这比较容易理解!我们通过查看编译后的 class 文件证实了其中存在默认构造函数,所以这是 Java 编译器生成的, 而不是运行时 JVM 自动生成的。
自动生成的构造函数,其方法体应该是空的,但这里看到里面有一些指令。为什么呢?再次回顾 Java 知识, 每个构造函数中都会先调用 super 类的构造函数对吧? 但这不是 JVM 自动执行的, 而是由程序指令控制,所以默认构造函数中也就有一些字节码指令来干这个事情。
基本上,这几条指令就是执行 super() 调用;
至于其中解析的 java/lang/Object 不用说, 默认继承了 Object 类。这里再次验证了这个知识点,而且这是在编译期间就确定了的。
继续往下看 c,
main 方法中创建了该类的一个实例, 然后就 return 了, 关于里面的几个指令, 稍后讲解。
4.4 查看 class 文件中的常量池信息
常量池 大家应该都听说过, 英文是 Constant pool。这里做一个强调: 大多数时候指的是 运行时常量池。但运行时常量池里面的常量是从哪里来的呢? 主要就是由 class 文件中的 常量池结构体 组成的。
要查看常量池信息, 我们得加一点魔法参数:
在反编译 class 时,指定 -verbose 选项, 则会 输出附加信息。
结果如下所示:
其中显示了很多关于 class 文件信息: 编译时间, MD5 校验和, 从哪个 .java 源文件编译得来,符合哪个版本的 Java 语言规范等等。
还可以看到 ACC_PUBLIC 和 ACC_SUPER 访问标志符。ACC_PUBLIC 标志很容易理解:这个类是 public 类,因此用这个标志来表示。
但 ACC_SUPER 标志是怎么回事呢? 这就是历史原因, JDK1.0 的 BUG 修正中引入 ACC_SUPER 标志来修正 invokespecial 指令调用 super 类方法的问题,从 Java 1.1 开始, 编译器一般都会自动生成ACC_SUPER 标志。
有些同学可能注意到了, 好多指令后面使用了 #1, #2, #3 这样的编号。这就是对常量池的引用。 那常量池里面有些什么呢?
这是摘取的一部分内容, 可以看到常量池中的常量定义。还可以进行组合, 一个常量的定义中可以引用其他常量。比如第一行: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V</init>, 解读如下:
#1 常量编号, 该文件中其他地方可以引用。
= 等号就是分隔符.
Methodref 表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的 #4, 方法签名指向的 #13; 当然双斜线注释后面已经解析出来可读性比较好的说明了。
同学们可以试着解析其他的常量定义。 自己实践加上知识回顾,能有效增加个人的记忆和理解。
总结一下,常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。
公开
同步至部落
取消
完成
0/2000
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JVM 核心技术 100 讲 (尊享版)》
《JVM 核心技术 100 讲 (尊享版)》
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
精选留言
由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论