这篇文章能让你对Java字节码有个了解,这可以帮你成为一个更好的程序员。就像C或C++编译器将源码编译为汇编码,Java编译器会将Java源码编译成字节码。Java程序员应该花费时间去理解什么是字节码,它是怎样工作的,更重要地是,Java编译器产生了什么样的字节码。在某些情况下,产生的字节码并非是你能预料的。
此处关于字节码的信息和提供的字节码都是基于Java 2 SDK标准版v1.2.1 javac编译器。通过其他编译器产生的字节码可能和这稍微有些不同。
一、为什么要了解字节码?
字节码是Java程序的中间表示,就好比汇编是C或C++程序的中间表示。C和C++程序员最了解他们编译的处理器汇编指令集。在调试,优化性能和调节内存分配时,这项知识是至关重要的。了解编译器为你写的代码生成的汇编指令,有助于帮你认识到如何以不同的编码实现内存或性能目标。此外,当跟踪一个问题的时候,使用调试器(debugger)对源码反汇编,然后对正在执行的汇编代码进行单步调试是有益的。
Java经常忽视的方面就是通过javac编译器产生的字节码。了解什么是字节码及Java编译器可能会产生什么样的字节码对Java程序员的帮助和了解汇编对C或C++程序员的帮助是相同的。程序中的字节码。不管是运行时JIT还是HotSpot,字节码都是你程序大小和执行速度的重要的一部分。注意,你拥有的字节码越多,.class文件就越大,JIT或HotSpot运行时也就需要编译更多的代码。文章剩余的部分将会使你对Java字节码有个更深的理解。
二、产生字节码
javac Employee.java javap -c Employee > Employee.bc
Compiled from Employee.java class Employee extends java.lang.Object { public Employee(java.lang.String,int); public java.lang.String employeeName(); public int employeeNumber(); } Method Employee(java.lang.String,int) 0 aload_0 1 invokespecial #3 <Method java.lang.Object()> 4 aload_0 5 aload_1 6 putfield #5 <Field java.lang.String name> 9 aload_0 10 iload_2 11 putfield #4 <Field int idNumber> 14 aload_0 15 aload_1 16 iload_2 17 invokespecial #6 <Method void storeData(java.lang.String, int)> 20 return Method java.lang.String employeeName() 0 aload_0 1 getfield #5 <Field java.lang.String name> 4 areturn Method int employeeNumber() 0 aload_0 1 getfield #4 <Field int idNumber> 4 ireturn Method void storeData(java.lang.String, int) 0 return这个类很简单。它包含两个实例变量,一个构造器和三个方法。字节码文件的前5行列出了用于产生该代码的文件名,类定义,它的继承层次(默认,所有类都继承自java.lang.Object),构造器和方法。接下来,每个构造器的字节码被列出。然后,每个方法和它们的字节码被以字母顺序列出。
注意:单独的代码常称为操作码。复杂的操作码常称为字节码。
三、字节码详情
为了理解字节码的详细信息,我们需要讨论Java虚拟机(JVM)是如何处理执行过程中的字节码的。JVM是基于栈的机器。每一个线程都有一个用来存储帧集(frames)的JVM栈。每次方法调用都会创建一个帧,这个帧包括一个操作栈,一个本地变量的数组和一个运行时常量池的引用。
从概念上,帧如下图所示:
图1、一个帧
本地变量的数组也称为本地变量表,包括方法的参数,它也被用来存储本地变量的值。首先存放的是参数,从0开始编码。如果帧是一个构造器或实例方法的,this引用将会存储在地址0处。地址1存放第一个参数,地址2存储第二个参数,依次类推。对于静态方法,第一个方法参数被存放在地址0,第二个存放在地址1,依次类推。
本地变量数组的大小是在编译期间决定的,它取决于本地变量和正常方法参数的数量和大小。操作栈是一个用于push和pop值的后进先出的栈。它的大小也是在编译期决定。一些操作码指令将值push到操作栈;其他的操作码指令从栈上获取操作数,操作它们,将结果push回去。操作栈常用来接收方法的返回值。
public String employeeName() { return name; } Method java.lang.String employeeName() 0 aload_0 1 getfield #5 <Field java.lang.String name> 4 areturn这个方法的字节码由3个操作码指令组成。第一个操作码,aload_0,用于将本地变量表中索引为0的变量的值推送(push)到操作栈上。前面提到过,本地变量表是用来为方法传递参数的。构造器和实例方法的this引用总是存放在本地变量表的地址0处。this引用必须入栈,因为方法需要访问实例的数据,名称和类。
图2、employeeName方法的字节码数组
实际上,字节码数组包含代表指令的字节。使用一个16进制的编辑器查看class文件,可能看到字节码数组中有下面的值:
图3、字节码数组中的值
2A,B4和B0分别对应于aload_0,getfield和areturn。
public Employee(String strName, int num) { name = strName; idNumber = num; storeData(strName, num); } Method Employee(java.lang.String,int) 0 aload_0 1 invokespecial #3 <Method java.lang.Object()> 4 aload_0 5 aload_1 6 putfield #5 <Field java.lang.String name> 9 aload_0 10 iload_2 11 putfield #4 <Field int idNumber> 14 aload_0 15 aload_1 16 iload_2 17 invokespecial #6 <Method void storeData(java.lang.String, int)> 20 return
第一个操作码在位置0,aload_0,将引用推送到操作栈上。(记住,本地变量表用于实例方法和构造器的第一个入口就是该引用)。
下一个操作码指令在位置1,invokespecial,调用父类的构造器。因为,所有没有明确从任何其他类继承的类都隐式继承了java.lang.Object。编译器提供必需的字节码用于调用基类的构造器。在这些操作码中,操作栈的顶部值将会弹出。
下两个操作码,位于位置4和5,将本地变量表中的前两个实体推送到操作栈。第一个值是一个引用。第二个值是构造器的第一个正式的参数,strName。这些推送的值是为位于位置6的putfield操作码准备的。
putfield操作码弹出位于操作栈顶部的两个值,存储strName的一个引用到通过this引用的对象的实例属性name中。
下3个操作码指令位于9,10和11,使用第二个正常的构造器参数num,和实例变量idNumber,执行相同的操作。
接着的3个操作码指令,位于14,15和16,为storeData的方法调用准备栈数据。这些指令分别将this引用,strName和num入栈。这个引用必须入栈,因为一个实例方法被调用。如果该方法被声明为静态的,这个this引用就不需要入栈。由于strName和num是storeData方法的参数,所以它们的值需要入栈。当storeData方法执行时,this引用,strName和num,将分别占据该方法对应帧的本地变量表的0,1和2索引。
四、大小和速度问题
对于很多使用Java开发的桌面和服务端应用,性能是一个关键的问题。伴随着Java将这些系统迁移到更小的内嵌设备,大小问题也变的十分重要。了解对于一系列的Java指令将会产生什么样的字节码能帮你写更小,更高效的代码。例如,考虑Java中的同步。下面的两个方法返回一个通过数组实现的整数栈的顶部元素。两个方法都使用同步,功能上是等价的:
public synchronized int top1() { return intArr[0]; } public int top2() { synchronized (this) { return intArr[0]; } }这些方法,尽管使用不同的同步方式,但效果是一致的。不明显的是,它们有不同的性能和字符数量。在这个例子中,top1大约比top2快百分之13,同时也更小。通过检查生成的字节码可以看到这些方法的不同。字节码中添加的注释用于解释每个操作码的作用。
Method int top1() 0 aload_0 //将本地变量表中索引为0的对象引用this入栈。 1 getfield #6 <Field int intArr[]> //弹出对象引用this,将访问常量池的intArr对象引用入栈。 4 iconst_0 //将0入栈。 5 iaload //弹出栈顶的两个值,将intArr中索引为0的值入栈。 6 ireturn //弹出栈顶的值,将其压入调用方法的操作栈,并退出。 Method int top2() 0 aload_0 //将本地变量表中索引为0的对象引用this入栈。 1 astore_2 //弹出this引用,存放到本地变量表中索引为2的地方。 2 aload_2 //将this引用入栈。 3 monitorenter //弹出this引用,获取对象的监视器。 4 aload_0 //开始进入同步块。将this引用压入本地变量表索引为0的地方。 5 getfield #6 <Field int intArr[]> //弹出this引用,压入访问常量池的intArr引用。 8 iconst_0 //压入0。 9 iaload //弹出顶部的两个值,压入intArr索引为0的值。 10 istore_1 //弹出值,将它存放到本地变量表索引为1的地方。 11 jsr 19 //压入下一个操作码(14)的地址,并跳转到位置19。 14 iload_1 //压入本地变量表中索引为1的值。 15 ireturn //弹出顶部的值,并将其压入到调用方法的操作栈中,退出。 16 aload_2 //同步块结束。将this引用压入到本地变量表索引为2的地方。 17 monitorexit //弹出this引用,退出监视器。 18 athrow //弹出this引用,抛出异常。 19 astore_3 //弹出返回地址(14),并将其存放到本地变量表索引为3的地方。 20 aload_2 //将this引用压入到本地变量索引为2的地方。 21 monitorexit //弹出this引用,并退出监视器。 22 ret 3 //从本地变量表索引为3的值(14)指示的地方返回。 Exception table: //如果在位置4(包括4)和位置16(排除16)中出现异常,则跳转到位置16. from to target type 4 16 16 anytop2比top1大,还慢,是因为采取的同步和异常处理方式。注意到top1使用synchronized方法修饰符,这不会产生额外的代码。相反,top2在方法体中使用synchronized语句。
javac编译器提供了一些你有必要了解的可选项。第一个是-O。JDK文档中声明-O能够优化你的代码以提高运行速度。在Sun Java 2 SDK的javac编译器中使用-O对产生的字节码没有影响。Sun javac编译器的老版本提供了一些基本的字节码优化,但那些都被删除了。然而SDK文档一直没有更新。保留-O作为一个可选项的原因是因为它兼容老版本。因此,当前没有必要使用它。
这意味着通过javac编译器产生的字节码并没有比你写的代码好很多。例如,如果你写一个包含一个不变量的循环,javac编译器并不会移除这个不变量。程序员常常使用其他语言的编译器来清除那些代码异味。不幸的是,javac不能做这些。更重要的是,javac编译器不会执行简单的优化,比如循环展开,代数化简,复杂运算简化等。为了获取这些好处和其他简单的优化,程序员需要在Java源码级别来做这些,而不是依赖于javac编译器。这里有很多技术你可以用来让javac编译器产生更快,更小的字节码。不幸的是,为了获取这些好处,在Java编译器执行它们之前,你必须实现它们。
javac编译器也支持-g和-g:none的可选项。-g选项告诉编译器产生所有的调试信息。g:none选项告诉编译器不产生调试信息。使用-g:none选项编译可能产生最小的类文件。
因此,当试着产生最小的类文件时可以使用该选项。
六、Java调试器
我见过的一个非常有用的Java编辑器的特性就是和C或C++调试器类似的反编译视图。反编译Java代码可能会暴露字节码,就像反编译C或C++代码会暴露汇编代码。除了这个特性外,另一个比较有用的特性可能是通过字节码进行单步调试的能力,一次执行一个操作码。
这种级别的功能允许程序员看到一手的,通过Java编译器产生的字节码,同时也能在调试期间通过它进行单步执行。程序员获取产生和执行的代码信息越多,就越有机会避免出错。调试器的类型特性鼓励程序员去查看,理解生成的字节码。
七、总结
这篇文章为你展示一个Java字节码到的综述和理解。任何语言的最好的程序员理解高级语言转换的在执行前的中间形式。对于Java,中间形式就是字节码。理解它,知道它如何工作,更重要的是,针对特别的源码,Java编译器会产生什么样的字节码,这可能会写出最快,最小的代码。
注:本篇文章翻译自Java bytecode: Understanding bytecode makes you a better programmer
翻译的惨不忍睹,各位见谅,如有其他翻译本文的,还请提供下链接,参考下。
原文:http://blog.csdn.net/qbg19881206/article/details/22896935