本文为翻译的文章,作者Mahmoud Anouti,原文:
https://dzone.com/articles/introduction-to-java-bytecode
阅读编译好的java字节码是很乏味的,即使对于有经验的java开发者来说也是如此。我们为什么首先需要了解如此底层的东西?这里有一个我上周碰到的简单场景:好久以前,我在自己的机器上修改了一些代码,然后编译成JAR包并部署到一台服务器上,以便测试对一个性能问题的修改。不幸的是,那些代码从来都没有签入到一个版本控制系统中,并且不知道什么原因,本地的改动也被删除了,也没有跟踪回溯。几个月之后 ,我又需要这些源码的变更了(需要花很多功夫才能补上),但我找不到他们了。
幸运的,编译后的代码在远程服务器上仍然存在。我松了一口气,重新把JAR取下来,用一个反编译编辑器打开。。。只有一个问题:反编译GUI不是一个没有瑕疵的工具,由于某些原因,在JAR包里面很多的类中,只有我要找的那个反编译的那个类导致一个UI的bug,每当我打开它的时候,反编译器就会崩溃。
绝望的时刻召唤绝望的手段。幸运的是,我熟悉原始的字节码,我宁愿花些时间来手动地反编译一些代码片段,而不愿重新去修改代码并测试。因为我仍然记得至少在哪里修改代码,阅读字节码帮助我定位到确切的修改,并且把它们构建回源代码的形式。(我确保从错误中吸取了教训,这次要把源代码保存好!)
字节码有一个好外是,你只需要学习它的语法一次,就能把它运用在所有Java支持的平台上----因为它是代码的一个中间状态,而不是CPU真正可执行的代码。而且,字节码比原生的机器码要简单,因为JVM架构很简单,因而简化了指令集。另外一个好处是所有的指令集在Oracle文档中都有。
然而,在我们开始学习字节码的指令集之前,让我们先熟悉一下JVM的一些东西,因为它们是先决条件。
JVM数据类型
Java是静态类型的语言,这影响了字节码指令的设计:一条指令期望它自己与特定类型的值进行操作。比如,有多个加法指令来对两个数值求和:iadd,ladd,fadd,daad。它们期望的操作数类型分别是:int,long,fload,double。大部分字节码都有这个特点:取决于操作数类型,同样的功能有不同的形式。
JVM定义的数据类型有:
1. 原始类型:
- 数值类型:byte(8位,二进制补码),short(16位,二进制补码),int(32位,二进制补码),long(64位,二进制补码),char(16位,无符号Unicode),float(32位,IEEE 754单精度浮点数),double(64位, IEEE 754双精度浮点数)
- boolean类型
- returnAddress:指令指针
2. 引用类型
- Class类型
- 数组类型
- 接口类型
字节码对于boolean类型的支持有限。比如,没有指令可以直接在boolean上进行操作。Boolean值被编译器转换成了int类型,使用对应的int指令进行操作。
Java开发者应该熟悉上面所有的除了returnAddress的类型,它在编程语言的类型中没有对等物。
基于栈的架构
字节码指令集的简易性,大部分要归功于Sun公司设计了一个基于栈而不是基于寄存器的虚拟机架构。一个JVM进程使用了多种内存区域,但是要从本质上掌握字节码指令,只有JVM栈需要详细研究。
程序计数器:对于Java程序中每一个运行的线程,一个程序计数器保存了当前指令的地址。
JVM栈:对每一个线程,栈被分配来保存局部变量,方法参数和返回值。下面的插图显示了三个线程的栈:
堆:所有线程共享的内存区域,并且保存了对象(类实例和数组)。对象释放是由垃圾回收器来管理的。
方法区:对于每一个加载的类,它保存了方法的代码和一个符号表(比如字段或者方法的引用),以及常量池中的常量。
一个JVM栈由栈帧组成,方法调用时,栈帧入栈,方法结束时出栈(不管是正常地返回还是抛出了异常)。
每个栈帧由下面的组成:
-
一个局部变量数组,索引序号从0到数据长度减1。长度是由编译器计算的。一个局部变量可以保存任何类型,除了long和double类型的值,它们占用两个局部变量
-
一个操作数栈,用来保存计算的中间结果,它们要么是指令的操作数,要么是方法调用的参数。
欢迎关注微信公众号,获取更多信息。