Java字节码(一)

2014-11-23 23:26:38 · 作者: · 浏览: 0

这篇文章能让你对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 
  
   
4 aload_0
5 aload_1
6 putfield #5 
   
     9 aload_0 10 iload_2 11 putfield #4 
    
      14 aload_0 15 aload_1 16 iload_2 17 invokespecial #6 
     
       20 return Method java.lang.String employeeName() 0 aload_0 1 getfield #5 
      
        4 areturn Method int employeeNumber() 0 aload_0 1 getfield #4 
       
         4 ireturn Method void storeData(java.lang.String, int) 0 return
       
      
     
    
   
  
这个类很简单。它包含两个实例变量,一个构造器和三个方法。字节码文件的前5行列出了用于产生该代码的文件名,类定义,它的继承层次(默认,所有类都继承自java.lang.Object),构造器和方法。接下来,每个构造器的字节码被列出。然后,每个方法和它们的字节码被以字母顺序列出。
通过检查字节码,你可能注意到一些以'a'或'i'作为前缀的操作码。比如,在Employee类的构造器中,你可以看到aload_0和iload_2。这些前缀代表操作码的类型。前缀‘a’表示操作码正在操纵一个对象引用。前缀‘i’意味着操作码正在操纵一个整数。其他的操作码使用前缀'b'来代表byte,‘c’代表char,‘d’代表double等。这些前缀能帮你了解正在操纵的数据类型。

注意:单独的代码常称为操作码。复杂的操作码常称为字节码。

三、字节码详情

为了理解字节码的详细信息,我们需要讨论Java虚拟机(JVM)是如何处理执行过程中的字节码的。JVM是基于栈的机器。每一个线程都有一个用来存储帧集(frames)的JVM栈。每次方法调用都会创建一个帧,这个帧包括一个操作栈,一个本地变量的数组和一个运行时常量池的引用。

从概念上,帧如下图所示:

frame

图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 
  
   
4 areturn
  
这个方法的字节码由3个操作码指令组成。第一个操作码,aload_0,用于将本地变量表中索引为0的变量的值推送(push)到操作栈上。前面提到过,本地变量表是用来为方法传递参数的。构造器和实例方法的this引用总是存放在本地变量表的地址0处。this引用必须入栈,因为方法需要访问实例的数据,名称和类。
下一个操作码指令,getfield,用于从对象中提取字段。当该操作码执行的时候,操作栈顶部的值就会弹出(pop).然后#5被用来在类的运行时常量池中构建一个用于存放字段name引用的地址的索引。当这个引用被提取的时候,它将会推送到操作栈上。
最后一个指令,areturn,返回一个来自方法的引用。比较特殊的是,areturn的执行会导致操作栈顶部的值,name字段的引用都会被弹出,然后推送到调用方法的操作栈。
employeeName方法相当简单。在考虑一个更复杂的例子之前,我们需要检查每个操作码左边的值。在employeeName方法的字节码中,这些值是0,1,和4。每一个方法都有一个对应的字节码数组。这些值对应每个操作码和它们的参数数组中的索引。你可能好奇为什么这些值不是顺序的。正如字节码这个名字所显示的那样,每个指令占据1个字节,那为何索引不是0,1,2?原因是,一些操作码含有参数,这些参数会占据字节数组的空间。比如,aload_0指令没有参数,自然地在字节数组中就占据一个字节。因此,下一个操作码,getfield就在位置1上。然而,areturn在位置4上。因为getfield操作码和它的参数占据了位置1,2,和3。位置1被getfield操作码使用,位置2和位置3被用于存放参数。这些参数用于构成在类的运行时常量池中存放值的地方的一个索引。下面的图展示了employeeName方法的字节码数组看起来是什么样子的:
bytecode array

图2、employeeName方法的字节码数组

实际上,字节码数组包含代表指令的字节。使用一个16进制的编辑器查