Java字节码
Java 字节码(英語:Java bytecode)是Java虚拟机执行的一种指令格式。大多数操作码都是一个字节长,而有些操作需要参数,导致了有一些多字节的操作码。而且并不是所有可能的256个操作码都被使用;其中有51个操作码被保留做将来使用。除此之外,原始Java平台开发商,昇陽微系统,额外保留了3个代码永久不使用。
与Java的关系
编辑一个Java程序员并不需要理解所有的Java字节码。但是,就像IBM developerWorks周刊建议的那样:“理解字节码以及理解Java编译器如何生成Java字节码与学习汇编知识对于C/C++程序员有一样的意义。”[1]
指令
编辑指令码为一个字节,有256个可能的代码值(28=256),因此一个字节的指令码最多可能有256种不同的操作。其中,0x00、0xFE、0xCA、0xFF被指定保留。例如0xCA作为一个Java调试器的中断指令而从未被语言使用。相似地,0xFE和0xFF也未被语言使用[2]。
指令可以基本分为以下几类:
- 存储指令 (例如:aload_0,istore)
- 算术与逻辑指令 (例如: ladd,fcmpl)
- 类型转换指令 (例如:i2b,d2i)
- 对象创建与操作指令 (例如:new,putfield)
- 堆栈操作指令 (例如:swap,dup2)
- 控制转移指令 (例如:ifeq,goto)
- 方法调用与返回指令 (例如:invokespecial,areturn)
除此之外,还有一些更特殊的指令,作为异常抛出或同步等作用。
大多数的指令有前缀和(或)后缀来表明其操作数的类型。如下表
前/后缀 | 操作数类型 |
---|---|
i |
整数 |
l |
长整数 |
s |
短整数 |
b |
字节 |
c |
字符 |
f |
单精度浮点数 |
d |
双精度浮点数 |
z |
布尔值 |
a |
引用 |
例如,"iadd"指令将两个整数相加;而"dadd"指令将两个double浮点数相加。
"const"、 "load"、 "store"等涉及存储命令还会使用"_n"后缀,其中 "load"和"store"命令中的n可以为0到3之间的整数;而"const"命令中的n由类型指定。"const"指令把一个指定类型的值放入堆栈。例如"iconst_5"指令将一个整数5放入堆栈;而"dconst_1"将一个双精度浮点数1放入堆栈。"aconst_null"指令是放入一个null进堆栈。而对于"load" "store"指令中的n,指定了变量表中的存储位置。"aload_0"指令把在变量0中的对象(通常是"this"对象)放入堆栈,"istore_1"指令把栈顶的一个整数放入变量1。对于更高的变量,后缀将被去除,而这条指令将需要操作数。
计算模型
编辑Java字节码的计算模型是面向堆栈结构计算机的。例如,一个x86处理器的汇编代码如下
mov eax, byte [ebp-4]
mov edx, byte [ebp-8]
add eax, edx
mov ecx, eax
这段代码将两个数值相加,并存入另一个地址。相似的反汇编字节码如下
0 iload_1
1 iload_2
2 iadd
3 istore_3
在这里,需要相加的两个操作数被放入堆栈,而相加操作就在栈中进行,其结果也被放入堆栈。存储指令之后把栈顶的数据放入一个变量地址。在每条指令前面的数字仅仅是表示这条指令到方法开始处的偏移值。这种堆栈结构也可以推广到面向对象模型上。例如,有一个"getName"方法如下
Method java.lang.String getName()
0 aload_0 // "this"对象被存入变量0
1 getfield #5 <Field java.lang.String name>
// 这个指令从栈顶取出一个对象,并从中搜索一个指定的域
// 并将相应的数据存入栈顶。
// 这个例子中,"name"域对应于该类中的第五个常量。
4 areturn // 返回栈顶的对象作为函数的返回值
例子
编辑考虑如下Java代码
outer:
for (int i = 2; i < 1000; i++) {
for (int j = 2; j < i; j++) {
if (i % j == 0)
continue outer;
}
System.out.println (i);
}
假设上述代码位于一个函数中,Java编译器可能将代码翻译成下述的Java字节码。
0: iconst_2
1: istore_1
2: iload_1
3: sipush 1000
6: if_icmpge 44
9: iconst_2
10: istore_2
11: iload_2
12: iload_1
13: if_icmpge 31
16: iload_1
17: iload_2
18: irem
19: ifne 25
22: goto 38
25: iinc 2, 1
28: goto 11
31: getstatic #84; //Field java/lang/System.out:Ljava/io/PrintStream;
34: iload_1
35: invokevirtual #85; //Method java/io/PrintStream.println:(I)V
38: iinc 1, 1
41: goto 2
44: return
基于Java字节码的语言
编辑最常用的基于Java字节码的语言就是开发出Java字节码的Java语言。刚开始,只存在一个由昇陽微系统开发的一个编译器javac。而现在Java字节码规范已经可以得到,因此,第三方公司亦开发出支持Java字节码的编译器。例如:
- Jikes,编译Java源代码到Java字节码(由IBM开发,用C++实现)
- Espresso,编译Java源代码到Java字节码(仅支持Java 1.0)
- GCJ,GNU Compiler for Java,编译Java代码到Java字节码;亦可以编译到机器代码。作为GNU Compiler Collection (GCC)的一部分提供。
有一些项目提供Java汇编器以便于直接用Java字节码进行开发。主要的Java汇编器如下:
- Jasmin,读取Java类的文字描述;用一种简单的使用Java虚拟机指令的类汇编语法,输出Java类文件 [3]
- Jamaica, 一种为Java虚拟机编写的宏汇编语言。其中,类与接口由Java语法定义,而其中的方法却由Java字节码定义。[4]
还有其他的一些编译器,对于其他语言生成Java字节码,使其可以运行在Java虚拟机之上。
- ColdFusion
- JRuby和Jython, 两种基于Ruby和Python的脚本语言
- Groovy, 一种基于Java的脚本语言
- Scala,一种类型安全的通用编程语言,支持面向对象编程和函数式编程
- JGNAT和AppleMagic,编译Ada语言到Java字节码
- Clojure, 一种函数式的通用编程语言,提供优秀的并发性。是一种LISP方言
- MIDletPascal
- JavaFX Script 由昇陽微系统公司开发的一种脚本语言,运行于Java虚拟机之上
运行
编辑当前已经有很多种Java虚拟机产品,包括了自由软件和商业软件。 如果在Java虚拟机之中执行Java字节码并不理想,则可以使用一些工具例如GNU Compiler for Java将Java代码或Java字节码编译成机器码并由硬件直接运行。 而有一些处理器可以直接运行Java字节码,这种处理器名为Java处理器。
对动态语言的支持
编辑Java虚拟机对动态类型语言提供了一定的支持。但绝大多数的Java虚拟机指令集是基于静态类型语言的。在静态类型机制下,方法调用中的类型分析都是在编译时执行的,而且缺乏一种机制在运行时确定一个类型已经确定相应的方法。
JSR292[5]中,在Java虚拟机层次增加了一种支持动态类型的指令invokedynamic
,以支持在动态类型检测中的方法调用。 达芬奇机器则是一种支持这种动态类型调用的虚拟机。 而所有支持JSE 7的Java虚拟机都应支持invokedynamic操作码。
参考文献
编辑- ^ Understanding bytecode makes you a better programmer. [2014-01-28]. (原始内容存档于2008-12-08).
- ^ 存档副本. [2014-01-28]. (原始内容存档于2013-02-21).
- ^ Jasmin Home Page. [2020-06-21]. (原始内容存档于2020-04-17).
- ^ Jamaica: The Java Virtual Machine (JVM) Macro Assembler. [2014-01-28]. (原始内容存档于2012-06-24).
- ^ see JSR 292. [2014-01-28]. (原始内容存档于2020-12-20).