Java字节码介绍【一】

本文为翻译的文章,作者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栈:对每一个线程,栈被分配来保存局部变量,方法参数和返回值。下面的插图显示了三个线程的栈:


Java字节码介绍【一】

:所有线程共享的内存区域,并且保存了对象(类实例和数组)。对象释放是由垃圾回收器来管理的。

Java字节码介绍【一】

方法区:对于每一个加载的类,它保存了方法的代码和一个符号表(比如字段或者方法的引用),以及常量池中的常量。

Java字节码介绍【一】

一个JVM栈由栈帧组成,方法调用时,栈帧入栈,方法结束时出栈(不管是正常地返回还是抛出了异常)。

每个栈帧由下面的组成:

1. 一个局部变量数组,索引序号从0到数据长度减1。长度是由编译器计算的。一个局部变量可以保存任何类型,除了long和double类型的值,它们占用两个局部变量

2. 一个操作数栈,用来保存计算的中间结果,它们要么是指令的操作数,要么是方法调用的参数。

Java字节码介绍【一】