admin 管理员组

文章数量: 887021


2024年2月7日发(作者:游戏前端开发做些什么)

Java字节码揭秘

写在前面

这一两年,在JVM上使用其他替代语言越来越热门了。现在至少有三门语言有幸在Java

Community Process中得到了官方认可:JRuby、Groovy和Bean-Shell。另外,代号为野马(Mustang)的Java 6发布了包含了一个专为封装不同脚本引擎的API层,就像JDBC访问数据库的模式一样。再加上Java版本5也在语言本身上做了很大的调整。总之,就像我之前翻译的一篇BLOG一样,Java平台的编程语言的前景已经发生了巨大的改变。虽然如此,只有一样东西没有变,它是所有这些语言的基础,无论这些语言有多么吸引人的特性和功能,最终都会在JVM的混合语言中运行,即JVM字节码。这又提起了我在JVM/Java字节码方面的兴趣。所以书写本文,在其中将介绍JVM字节码集合,用一些代码来描述它的工作方式,也将介绍一些可以直接操纵字节码的工具。

首先我要说明的是,直接了解JVM字节码感觉是奇怪的事情,因为我们总不可能自己来书写字节码。但是,我们如果知道编译器干了些什么可能会更好一点。比如,你肯定想知道编译后的StringBuffer和String的区别、编译器到底有没有给你加上默认构造函数„„当你了解了JVM字节码——这是我看见过的最简单的“可装配语言”——你就能够验证你的这些假设是否正确。

分解Java

考虑到大家对Java都已经比较熟悉了,所以我们这样开始可能比较容易:我们从编译后的Java代码开始,然后对其进行分解。这样可能比一开始就直接讲述Java字节码的规则要好一些。我们先从最简单的Hello World程序开始。

public class HelloWorld{ public static void main(String[] args)

{ n("Hello, world!"); }}

我们通过两种方式来一起研究Java字节码。第一个是太久时间都没有见到过的javap。javap是字节码分解器,意思就是它编译.class文件并将文件结构输出到控制台,其中包括组成方法的字节码。如下例:

$ javap -verbose -c -private HelloWorldCompiled from ""public class HelloWorld

extends SourceFile: "" minor version: 0

major version: 50 Constant pool:const #1 = Method #6.#15; //

java/lang/Object."":()Vconst #2 = Field #16.#17; //

java/lang/:Ljava/io/PrintStream;const #3 = String #18; // Hello, world!const #4 =

Method #19.#20; // java/io/n:(Ljava/lang/String;)Vconst #5 = class #21; //

HelloWorldconst #6 = class #22; // java/lang/Objectconst #7 = Asciz ;const #8 = Asciz

()V;const #9 = Asciz Code;const #10 = Asciz LineNumberTable;const #11 = Asciz main;const #12 =

Asciz ([Ljava/lang/String;)V;const #13 = Asciz SourceFile;const #14 = Asciz ;const

#15 = NameAndType #7:#8;// "":()Vconst #16 = class #23; // java/lang/Systemconst #17 =

NameAndType #24:#25;// out:Ljava/io/PrintStream;const #18 = Asciz Hello, world!;const #19 =

class #26; // java/io/PrintStreamconst #20 = NameAndType #27:#28;//

println:(Ljava/lang/String;)Vconst #21 = Asciz HelloWorld;const #22 = Asciz

java/lang/Object;const #23 = Asciz java/lang/System;const #24 = Asciz out;const #25 = Asciz

Ljava/io/PrintStream;;const #26 = Asciz java/io/PrintStream;const #27 = Asciz println;const #28 =

Asciz (Ljava/lang/String;)V; {public HelloWorld(); Code: Stack=1,

Locals=1, Args_size=1 0: aload_0 1: invokespecial #1;

//Method java/lang/Object."":()V 4: return LineNumberTable:

line 1: 0 public static void main([]); Code: Stack=2,

Locals=1, Args_size=1 0: getstatic #2; //Field

java/lang/:Ljava/io/PrintStream; 3: ldc #3; //String Hello, world!

5: invokevirtual #4; //Method java/io/n:(Ljava/lang/String;)V

8: return LineNumberTable: line 5: 0 line 6: 8}

在刚才讲述的.class文件实际并不准确,JVM无所谓输入的二进制流从哪儿来,只不过因为我们的习惯和JDK 1.0的发布所以我们说成是.class文件。所以,所谓的“.class文件”应该被理解为符合JVM标准的二进制格式流。

上面我们使用了javap。其中,-c指示需要显示方法字节码;-private指示无论可访问性显示所有成员;-verbose是需要显示类的常量池。检查HelloWorld分解后的内容,会觉得非常有趣,我们立马就可以验证一些假设。例如,第一,如果类没有显式声明其父类的话,它将继承于。第二,javap也验证了如果类中没有显式声明构造函数的话,编译器会插入一个缺省无参的构造函数(构造函数在JVM级别是显示成的普通函数)。

加上了-verbose选项的javap输出中一个重要的部分就是常量池。每个类都会有个常量池,所有的常量——比如字符串、类名、方法名、属性名——都是保存在类的中心位置,通过对该池的索引进行参照访问。通常,这些特殊的细节内容都是由工具来处理的,这也是javap通过注释来显示这些常量值的原因。但是这些内容对我们认识常量池非常有用,也能够简化我们对分解代码的理解。例如,第5行代码n("Hello, world!");它调用了println方法,显示在常量池的编号为4的分片(const #4),它依次由编号为19的分片和编号为20的分片组成(const #4 = Method #19.#20;),这样就最终解决了n(String[])的问题。你可以参照JVM标准来了解所有不同的常量类型以及他们在.class文件中的格式。

在这里,我们主要来分析自动生成的HelloWorld构造函数:

public HelloWorld(); Code: Stack=1, Locals=1, Args_size=1

0: aload_0 1: invokespecial #1; //Method java/lang/Object."":()V

4: return LineNumberTable: line 1: 0

在JVM中,所有字节码都是通过一个基本的原则来进行堆栈操作的:每个操作符可能会消费一个或多个操作计数,并可能最后将一个操作计数推送到执行堆栈。需要注意的是,每个分片(slot)都是32位的,这就意味着long或者是double的值会消耗两个分片(slot)(很多人认为这个是JVM实现中的最大缺憾)。另外,每个方法都会有一个本地的结合,本地变量和参数都在此保存。因此,例如“aload_0”指示符将第一个参数带入方法,并将其推送至执行堆栈。“invokespecial”指示符,不言而喻,它将调用实例的方法,但是忽略传统的动态绑定(因为我们显示调用基类版本的覆盖方法,该特殊的操作符用在父“super”调用)。因为Object的构造函数需要一个参数(this指针),所以它将消耗执行堆栈中的一个分片(记住,这是我们

刚才推送的参数——this指针,指向我们自己的实例的this指针),而且它不返回任何值(最后有一个V字),当方法返回时它将不往堆栈内推送任何内容。此时,HelloWorld的构造函数已完成任务,所以它通过“return”操作符进行简单返回。

我们接下来在看看写在HelloWorld里面的主方法(main):

public static void main([]); Code: Stack=2, Locals=1,

Args_size=1 0: getstatic #2; //Field java/lang/:Ljava/io/PrintStream;

3: ldc #3; //String Hello, world! 5: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 8: return

LineNumberTable: line 5: 0 line 6: 8

因为它是静态方法,所以最显著的区别就是第一个参数并不是this指针,除此之外,它和HelloWorld的构造函数看起来都差不多。第一个操作符“getstatic”将获取一个static区域并将其值推送至堆栈中,在本例中是的引用,由#2常量池分片描述,并在操作符后使用注释显示。接下来,就对字符串“Hello, World!”进行加载,它在#3常量池分片中存储。通过堆栈上的两个引用,我们就可以调用“invokevirtual”n(String[])方法了。因其需要一个参数,再加上调用该方法需要的初始this引用,我们刚才推送至堆栈的这两项就被消费了,println(String[])不返回任何值,所以完成后堆栈上就为空了。一个简单的“return”操作符中止了该方法,任务完成了。

后面的内容会比现在的复杂一些,但总的来说,了解Java字节码的重要部分是需要了解每个操作符是如何操作执行堆栈的。

Java字节码分类

JVM字节码集合基本上是分为几个不同的大类的。我们不会逐一介绍字节码的操作符,我们讨论类别,然后着重拿出一些常用的操作符,其余的均可通过JVM规范来获取详情。

堆栈操作。

•pop、pop2:将堆栈的值弹出。pop2用来弹出64位的值,pop用来弹出32位的。

•dup、dup2:复制堆栈顶端的值。用来形成高效的pop/push/push组合。dup2也是用在64位上的。

•const_null将null的引用推送至堆栈。

•bipush将单字节的常量值(-127~128)推送至堆栈。

•sipush将一个短整型类型的常量值(-32K~32K)推送至堆栈。

•ldc将常量值从常量池中推送至堆栈。

•Xload,X可为a、d、f、l或者i,是将一个本地(参数或变量)的指定类型推送至堆栈。a指引用、b指布尔类型、c指字符、d指双精度类型、f指浮点类型、i指整型、l指长整型、s指短整型。该编码模式会在操作符的名字中重复出现。

•Xstore,X可为a、d、f、l或者i,将堆栈顶端的值弹出并放入本地分片中。

•Xconst_Y是操作符集合中一系列的优化操作,设计用来将X类型的常量Y值推送至堆栈。例如,iconst_0就是将整数常量0推送至堆栈中,是bipush的高效硬编码变种。

分支与控制流。

•nop,啥也不做。

•if(条件),条件可以是null、notnull、eq、ne、gt、lt、_icmpeq、_icmpne„„。

•goto。Java代码虽然不支持goto,但JVM是支持的。

•return和Xreturn。X可以为a、d、f、l或者i,从当前调用方返回,将堆栈顶端作为X类型返回。

•lookupswitch提供了对switch/case表的实现。

算法指令。JVM操作符合其他CPU指令集一样有一些基本的算术运算符,例如加减乘除等,也包含一些基本的转换操作符进行放大与缩小的转换:

•数据转换操作符采用XtoY的形式,X和Y可以是a、d、f、l或者i,堆栈的顶端取符合X格式的数,并将转换成Y格式后推送回堆栈。

•算术运算符采用XOP的格式,X可为d、f、i或者l,OP可为加减乘除和取余。

•字节操作符采用iOP的格式,OP可为与、或、异或、左移位(shift left)、右移位(shift right)。

•比较操作符采用XcmpY的格式,X可以是d,即基于双精度的比较;f,即基于浮点的比较;或l,即基于长整型的比较。Y可以是g或者l。两个数比较,第一个>第二个,则将1推送至堆栈;如果=,则推送0;如果<,则推送-1。

对象模型指令。JVM内置的专门为对象工作的操作符:创建对象、调用方法、访问属性等:

•new、newarray、anewarray:创建一个新对象、创建一个数组和创建一个对象应用的数组。对象或数组被推送至堆栈的顶端。在new操作符时,并未调用构造函数,调用构造函数是后续代码的工作。

•getfield、setfield、getstatic、setstatic。在设置值时,值在堆栈的顶端,而对象引用就正好在下方跟着。如果是静态属性的话,显然不需要任何对象引用。

•invokevirtual、invokestatic、invokespecial、invokeinterface。它们都是调用方法的操作符,方法则由操作计数指定的常量池入口描述。使用推送至堆栈的值作为由左至右的调用参数,即调用的第一个参数位于堆栈的最下部;this引用位于第一个,也就是在堆栈的最下部引用。Invokevirtual操作符表示调用是对对象方法的普通调用,invokeinterface就是当通过接口的引用调用方法时,invokestatic表示调用静态方法,invokespecial表明无需考虑动态绑定的方法调用——为了调用特定版本的类的方法,而不管衍生覆盖类型。

•castclass、instanceof。这两个操作符处理堆栈顶部的引用转换为操作计数隐含的类型。如果成功,新引用或true将被推送至堆栈顶端,如果失败,则CastClassException异常或false被推送。

块同步(同步块或方法)。块同步由两个操作符处理,monitorenter与monitorexit。当调用对象试图获取监控器的代码时,它们都在堆栈上分别持有该对象的引用。事实上是编译器负责保证均衡的出入口调用,所以同步方法或同步块通常需要在try/finally块中来保证monitorexit操作符一定会被调用。如果不这样做的话,就会让监控器一直被线程占有,最终导致死锁。

异常处理。异常处理并非通过特殊的操作符集合来处理,而是通过创建一个表格,里面标记了块指令——监视并创建一系列的包括需要做什么的入口,也即当特定类型的异常抛出后的操作符偏移量。

以下是一个简单的异常处理例子:

public class ExFun{ public static void main(String[] args) { try

{ n("In try block"); throw

new Exception(); } catch (ption ioEx)

{ n("In catch IOException block");

n(ioEx); } catch (Exception ex)

{ n("In catch Exception block");

n(ex); } finally

{ n("In finally block"); } }}

字节码为:

Compiled from ""public class ExFun extends SourceFile:

"" minor version: 0 major version: 50 Constant pool:

(snipped for simplicity){public ExFun(); (snipped for simplicity) public static void

main([]); Code: Stack=2, Locals=3, Args_size=1

0: getstatic #2; //Field java/lang/:Ljava/io/PrintStream; 3: ldc #3;

//String In try block 5: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 8: new #5; //class

java/lang/Exception 11: dup 12: invokespecial #6; //Method

java/lang/Exception."":()V 15: athrow 16: astore_1

17: getstatic #2; //Field java/lang/:Ljava/io/PrintStream; 20: ldc #8;

//String In catch IOException block 22: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 25: getstatic #2; //Field

java/lang/:Ljava/io/PrintStream; 28: aload_1 29:

invokevirtual #9; //Method java/io/n:(Ljava/lang/Object;)V

32: getstatic #2; //Field java/lang/:Ljava/io/PrintStream; 35: ldc #10;

//String In finally block 37: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 40: goto 81

43: astore_1 44: getstatic #2; //Field java/lang/:Ljava/io/PrintStream;

47: ldc #11; //String In catch Exception block 49: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 52: getstatic #2; //Field

java/lang/:Ljava/io/PrintStream; 55: aload_1 56:

invokevirtual #9; //Method java/io/n:(Ljava/lang/Object;)V

59: getstatic #2; //Field java/lang/:Ljava/io/PrintStream; 62: ldc #10;

//String In finally block 64: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 67: goto 81

70: astore_2 71: getstatic #2; //Field java/lang/:Ljava/io/PrintStream;

74: ldc #10; //String In finally block 76: invokevirtual #4; //Method

java/io/n:(Ljava/lang/String;)V 79: aload_2

80: athrow 81: return Exception table: from to

target type 0 16 16 Class java/io/IOException 0 16 43 Class

java/lang/Exception 0 32 70 any 43 59 70 any

70 71 70 any LineNumberTable: (snipped) StackMapTable: (snipped)}

“try”块内的操作符是在0~16的范围内,Java源代码的“catch”块是被编在异常(Exception)表的开始两行(0(来自)~16(目标)~16(类型) ption,0~6~43 ion),

catch的操作符偏移量分别是16和43。也需要注意的是,异常表的最后三行表明了方法的其他地方抛出异常的情况(在异常块内部也一样),必须跳转到操作符70处来进行finally块的处理。而且需要注意的是,每个catch块都会对应一个finally块,所以finally块在字节码中重复出现了两次。这只是一个简单的例子,让我们看见了javac编译产生的字节码输出。

JVM字节码集合有212个操作符,46个保留字为后续扩展使用。完全的集合可以通过JVM规范来了解.

实践中的Java字节码

对Java字节码有了一定了解之后,我们可以来看看一些常用的和熟悉的Java语言的内容是如何与字节码映射的,也可以获得一些Java实现的细节内容。

Java 5:自动封装(autoboxing)

Java 5版本的一个新特性是自动封装(autoboxing),基础数据类型因语义环境的需要能转换成为对象类型,例如:

public class Autoboxing{ public static void main(String[] args)

{ int x = 5; ist al = new ist();

(x); }}

在Java 5之前,这样的写法是错误的,因为x并不是对象。在Java 5下,编译后的字节码如下:

0: iconst_51: istore_12: new #2; //class java/util/ArrayList5: dup6: invokespecial #3; //Method

java/util/ArrayList."":()V9: astore_210: aload_211: iload_112: invokestatic #4; //Method

java/lang/f:(I)Ljava/lang/Integer;15: invokevirtual #5; //Method

java/util/:(Ljava/lang/Object;)Z18: pop19: return

编号为0的行将整数常量5推送至堆栈,编号为1的行将堆栈顶端的5存储至第一个本地分片中。接下来,有四个操作符指令,new/dup/invokespecial/astore,是通常用来新创建对象并存储在本地变量中的做法。接下来,在编号为10的行,将ArrayList的引用推送至队战,然后再将x本地的值推送至堆栈。编号为12的行我们看到Java调用了静态的f方法,它需要一个单独的堆栈分片,并消费整数值5,然后将包含着5的Integer对象推送到位。然后,这个对象就成为了add方法的参数,调用add方法就消费了Integer和ArrayList的引用,并将add方法的返回值推送回堆栈。

内部类(Inner Class)

在JDK 1.1发布时,Sun引入了内部类,支持创建与外部类有着特殊的私有可见关系的嵌套类。JVM并未引入像C++那样的friend功能,这就有点让Java使用者有个疑惑:在JVM本身强迫私有访问性时,而且把内部类看作跟其他类一样,Java如何对类的访问进行授权?

在下面这个例子中,内部类显然可以访问外部类的data私有属性:

class Outer{ private int data = 12; public Inner getInner()

{ return new Inner(); } public class Inner

{ public int getData() { return

data; } }} public class NestedFun{ public static void

main(String[] args) { Outer o = new Outer();

i = er(); n(a());

// prints 12; how? }}

对于这段代码,编译器如何进行工作呢?我们从(String[])开始看字节码:

public static void main([]); Code: Stack=2, Locals=3,

Args_size=1 0: new #2; //class Outer 3: dup

4: invokespecial #3; //Method Outer."":()V 7: astore_1 8:

aload_1 9: invokevirtual #4; //Method er:()LOuter$Inner;

12: astore_2 13: getstatic #5; //Field java/lang/:Ljava/io/PrintStream;

16: aload_2 17: invokevirtual #6; //Method Outer$a:()I

20: invokevirtual #7; //Method java/io/n:(I)V 23: return

这段字节码还是比较直接的:Java使用了常用的new/dup/invokespecial/astore组合来创建Outer的实例,对er()和getData()的调用,其中对getData()调用的返回值直接传入了println()方法(注意,编译器选择先获取,然后再是getData(),所以才能保证执行堆栈的位置顺序正确)。这一段基本没啥,我们再来看e()方法:

public class Outer$Inner extends SourceFile: ""

InnerClass: public #21= #4 of #18; //Inner=class Outer$Inner of class Outer

minor version: 0 major version: 50 Constant pool: (snipped) {final Outer this$0;

public Outer$Inner(Outer); Code: Stack=2, Locals=2, Args_size=2

0: aload_0 1: aload_1 2: putfield #1; //Field this$0:LOuter;

5: aload_0 6: invokespecial #2; //Method java/lang/Object."":()V

9: return public int getData(); Code: Stack=1, Locals=1, Args_size=1

0: aload_0 1: getfield #1; //Field this$0:LOuter; 4:

invokestatic #3; //Method $000:(LOuter;)I 7: ireturn}

这是去掉了一些输出后的结果,以便阅读。首先,我们看到了在Java规范中的“outer this”引用被显式加入内部类中作为一个属性,名为“this$0”,并标记为final。其次,编译器也生成了内部类的构造函数,用一个外部类的引用为“outer this”赋值,所以我们可以假定在外部类的getInner()方法中的new Inner()会用到本构造函数。第三,在内部类的getData()方法上,访问了一个外部类的静态方法叫“access$000”,来获取数据。

紧接着,我们可以看看外部类。

class Outer extends {private int data; Outer(); Code: 0:

aload_0 1: invokespecial #2; //Method java/lang/Object."":()V

4: aload_0 5: bipush 12 7: putfield #1; //Field data:I

10: return public Outer$Inner getInner(); Code: 0: new #3; //class

Outer$Inner 3: dup 4: aload_0 5:

invokespecial #4; //Method Outer$Inner."":(LOuter;)V 8: areturn static int

access$000(Outer); Code: 0: aload_0 1: getfield #1;

//Field data:I 4: ireturn}

我们可以看见编译器生成了一个静态方法专为访问data开了个口子,不过“access$000”是包内私有的,也就是说在同包内的类才能访问该方法。

Java字节码工具

Java字节码功能工具很多,包括:

•Javassist

•Jasmin

•„„

也许,最重要的拆解字节码的工具还是javap。

本文来自CSDN博客,转载请标明出处:/BU_BetterYou/archive/2008/06/19/


本文标签: 方法 堆栈 字节 调用 推送