概述
Java字节简介
即使对于有经验的Java开发人员来说,读取编译的Java字节码也很繁琐。为什么我们首先需要了解这种低级别的东西?这是上周发生在我身上的一个简单场景:我很久以前在我的机器上进行了一些代码更改,编译了一个Jar并将其部署在服务器上以测试针对性能问题的潜在修复。遗憾的是,代码从未签入版本控制系统,无论出于何种原因,本地更改都被删除而没有跟踪。几个月后,我再次需要源代码形式的更改(这需要付出相当大的努力),但找不到它们!
幸运的是,编译后的代码仍然存在于该远程服务器上。于是松了一口气,我再次取出Jar并使用反编译器编辑器打开它。只有一个问题,反编译器GUI不是一个完美的工具,并且在那个Jar中的许多类中,由于某种原因,只有我想要反编译的特定类导致UI中的错误在我打开它时被执行并且反编译器崩溃了!
绝望的时候需要绝望的措施...幸运的是我熟悉原始字节码,我宁愿花一些时间手动反编译一些代码,而不是通过更改并再次测试它们。因为我至少还记得在代码中查找的地方,阅读字节码帮助我查明确切的更改并以源代码形式重新构建它们。(我确保从错误中吸取教训并保留它们!)
关于字节码的好处是你学习它的语法一次并且它适用于所有Java支持的平台,因为它是代码的中间表示,而不是底层CPU的实际可执行代码。此外,字节码比本机机器码简单,因为JVM架构相当简单,因此简化了指令集。另一个好处是Oracle 指令集中完整记录了该指令集中的所有指令。
在学习字节码指令集之前,让我们先熟悉一下JVM的一些必要条件。
JVM数据类型
Java是静态类型的,它影响字节码指令的设计,使得指令期望自己对特定类型的值进行操作。例如,有一些附加说明添加两个数字:iadd
,ladd
,fadd
,dadd
。他们期望类型的操作数分别为int,long,float和double。大多数字节码具有这样的特征:具有相同功能的不同形式但是根据操作数类型而不同。
JVM定义的数据类型是:
- 原始类型:
- 数字类型:
byte
(8位2的补码),short
(16位2的补码),int
(32位2的补码),long
(64位2的补码),char
(16位无符号Unicode),float
(32位IEEE 754单)精度FP),double
(64位IEEE 754双精度FP) boolean
类型returnAddress
:指令指针
- 数字类型:
- 参考类型:
- 类类型
- 数组类型
- 接口类型
该boolean
类型在字节码中的支持有限。例如,没有直接操作boolean
值的指令。而是int
由编译器转换为布尔值,并使用相应的int
指令。
Java开发人员应该熟悉所有上述类型,除了returnAddress
没有等效的编程语言类型。
基于堆栈的架构
字节码指令集的简单性很大程度上归功于Sun设计了基于堆栈的VM架构,而不是基于寄存器的架构。JVM进程使用各种内存组件,但只需要详细检查JVM堆栈,以便能够遵循字节码指令:
PC寄存器:对于在Java程序中运行的每个线程,PC寄存器存储当前指令的地址。
JVM堆栈:对于每个线程,分配堆栈,其中存储局部变量,方法参数和返回值。这是一个显示3个线程的堆栈的插图。
堆:所有线程共享的内存,以及存储对象(类实例和数组)。对象释放由垃圾收集器管理。
方法区域:对于每个加载的类,存储方法的代码和符号表(例如对字段或方法的引用)和称为常量池的常量。
JVM堆栈由框架组成 ,每个框架在调用方法时被压入堆栈,并在方法完成时从堆栈弹出(通过正常返回或抛出异常)。每个框架还包括:
- 一个局部变量数组,索引从0到其长度减1.长度由编译器计算。局部变量可以保存任何类型的值,除了
long
和double
它们占据两个局部变量的值。 - 一个操作数堆栈,用于存储中间值,这些中间值将充当指令的操作数,或者将参数推送到方法调用。
Bytecode进行了探索
有了JVM内部的概念,我们可以看一下从示例代码生成的一些基本字节码示例。Java类文件中的每个方法都有一个代码段,该代码段由一系列指令组成,每个指令都具有以下格式:
opcode (1 byte) operand1 (optional) operand2 (optional) ...
这是一个由一个字节的操作码和零个或多个操作数组成的指令,这些操作数包含要操作的数据。
在当前正在执行的方法的堆栈帧内,指令可以将值推送或弹出到操作数堆栈上,并且它可以潜在地加载或存储数组局部变量中的值。我们来看一个简单的例子:
public
static
void
main(String[] args) {
int
a =
1
;
int
b =
2
;
int
c = a + b;
}
|
为了在编译的类中打印生成的字节码(假设它在文件中Test.class
),我们可以运行该javap
工具:
javap -
v
Test.class
|
我们得到:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
...
|
我们可以看到该方法的方法签名main
,该描述符指示该方法采用Strings([Ljava/lang/String;
)数组并具有void返回类型(V
)。接下来是一组标志,将方法描述为public(ACC_PUBLIC
)和static(ACC_STATIC
)。
最重要的部分是Code
属性,它包含方法的指令以及诸如操作数堆栈的最大深度(在本例中为2)的信息,以及在此方法的框架中分配的局部变量的数量(4 in这个案例)。除了第一个(在索引0处)保存对args
参数的引用之外,所有局部变量都在上面的指令中引用。其他3个局部变量对应于变量a
,b
并且c
在源代码中。
地址0到8的指令将执行以下操作:
iconst_1
:将整数常量1推入操作数堆栈。
istore_1
:弹出顶部操作数(一个int值)并将其存储在索引1的局部变量中,该变量对应于变量a
。
iconst_2
:将整数常量2推入操作数堆栈。
istore_2
:弹出顶部操作数int值并将其存储在索引2的局部变量中,该变量对应于变量b
。
iload_1
:从索引为1的局部变量加载int值并将其推送到操作数堆栈。
iload_2
:从索引为1的局部变量加载int值并将其推送到操作数堆栈。
iadd
:从操作数堆栈中弹出前两个int值,添加它们并将结果推回操作数堆栈。
istore_3
:弹出顶部操作数int值并将其存储在索引3的局部变量中,该变量对应于变量c
。
return
:从void方法返回。
上述每条指令都只包含一个操作码,它完全决定了JVM要执行的操作。
方法调用
在上面的例子中,只有一种方法,主要方法。让我们假设我们需要对变量的值进行更精细的计算c
,并且我们决定将它放在一个名为的新方法中calc
:
public
static
void
main(String[] args) {
int
a =
1
;
int
b =
2
;
int
c = calc(a, b);
}
static
int
calc(
int
a,
int
b) {
return
(
int
) Math.sqrt(Math.pow(a,
2
) + Math.pow(b,
2
));
}
|
让我们看看生成的字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: invokestatic #2 // Method calc:(II)I
9: istore_3
10: return
static int calc(int, int);
descriptor: (II)I
flags: (0x0008) ACC_STATIC
Code:
stack=6, locals=2, args_size=2
0: iload_0
1: i2d
2: ldc2_w #3 // double 2.0d
5: invokestatic #5 // Method java/lang/Math.pow:(DD)D
8: iload_1
9: i2d
10: ldc2_w #3 // double 2.0d
13: invokestatic #5 // Method java/lang/Math.pow:(DD)D
16: dadd
17: invokestatic #6 // Method java/lang/Math.sqrt:(D)D
20: d2i
21: ireturn
|
主方法代码的唯一区别是iadd
,我们现在invokestatic
只是调用静态方法而不是使用指令calc
。需要注意的关键是操作数堆栈包含传递给方法的两个参数calc
。换句话说,调用方法通过以正确的顺序将它们推送到操作数堆栈来准备要调用的方法的所有参数。invokestatic
(或稍后将看到的类似的invoke *指令)随后会弹出这些参数,并为调用的方法创建一个新的框架,其中参数放在其局部变量数组中。
我们还注意到该invokestatic
指令通过查看从6跳到9的地址占用3个字节。这是因为与目前为止看到的所有指令不同,invokestatic
包括两个额外的字节来构造对要调用的方法的引用(除了操作码)。该引用由javap显示,因为#2
它是calc
从前面描述的常量池中解析的方法的符号引用。
其他新信息显然是calc
方法本身的代码。它首先将第一个整数参数加载到操作数堆栈(iload_0
)上。下一条指令i2d
通过应用扩展转换将其转换为double。生成的double替换操作数堆栈的顶部。
下一条指令将double常量2.0d
(取自常量池)推送到操作数堆栈。然后使用Math.pow
到目前为止准备的两个操作数值(第一个参数calc
和常量2.0d
)调用静态方法。当Math.pow
方法返回时,其结果将存储在其调用者的操作数堆栈中。这可以在下面说明。
应用相同的过程来计算Math.pow(b, 2)
:
下一条指令dadd弹出前两个中间结果,添加它们并将总和推回到顶部。最后,invokestatic调用Math.sqrt
结果总和,并使用narrowing conversion(d2i
)将结果从double转换为int 。结果int返回给main方法,后者将其存储回c
(istore_3
)。
实例创作
让我们修改示例并引入一个类Point
来封装XY坐标。
public
class
Test {
public
static
void
main(String[] args) {
Point a =
new
Point(
1
,
1
);
Point b =
new
Point(
5
,
3
);
int
c = a.area(b);
}
}
class
Point {
int
x, y;
Point(
int
x,
int
y) {
this
.x = x;
this
.y = y;
}
public
int
area(Point b) {
int
length = Math.abs(b.y -
this
.y);
int
width = Math.abs(b.x -
this
.x);
return
length * width;
}
}
|
该main
方法的编译字节码如下所示:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: new #2 // class test/Point
3: dup
4: iconst_1
5: iconst_1
6: invokespecial #3 // Method test/Point."<init>":(II)V
9: astore_1
10: new #2 // class test/Point
13: dup
14: iconst_5
15: iconst_3
16: invokespecial #3 // Method test/Point."<init>":(II)V
19: astore_2
20: aload_1
21: aload_2
22: invokevirtual #4 // Method test/Point.area:(Ltest/Point;)I
25: istore_3
26: return
|
这里遇到的新指令是new
,dup
和invokespecial
。与编程语言中的new运算符类似,该new
指令创建一个传递给它的操作数中指定类型的对象(它是对类的符号引用Point
)。对象的内存在堆上分配,对象的引用在操作数堆栈上被推送。
该dup
指令复制了顶部操作数堆栈值,这意味着现在我们有两个引用Point
堆栈顶部的对象。接下来的三条指令将构造函数的参数(用于初始化对象)推入操作数堆栈,然后调用一个特殊的初始化方法 ,该方法对应于构造函数。该
方法是字段
x
和y
初始化的地方。后方法结束,三甲操作数堆栈值被消耗,剩下的就是原始参考创建的对象(其是由现在已经成功地初始化)。
接下来astore_1
弹出Point
引用并分配给索引1处的局部变量(a
in astore_1
表示这是一个参考值)。
重复相同的过程以创建和初始化第二个Point
实例,该第二个实例被分配给变量b
。
最后一步从索引1和2的局部变量(分别使用aload_1
和aload_2
)加载对两个Point对象的引用,并调用area
using方法invokevirtual
,该方法根据对象的实际类型处理调用适当方法的调用。例如,如果变量a
包含SpecialPoint
扩展类型的实例Point
,并且子类型覆盖该area
方法,则调用overriden方法。在这种情况下,没有子类,因此只有一种area
方法可用。
请注意,即使该area
方法接受一个参数,堆栈顶部也有两个 Point
引用。第一个(pointA
来自变量a
)实际上是调用该方法的实例(this
在编程语言中也称为),它将在该area
方法的新帧的第一个局部变量中传递。另一个操作数value(pointB
)是area
方法的参数。
另一种方式
您不需要掌握对每条指令的理解以及确切的执行流程,以便根据手头的字节码了解程序的功能。例如,在我的情况下,我想检查代码是否使用Java 流来读取文件,以及流是否已正确关闭。现在给出下面的字节码,可以相对容易地确定确实使用了流,并且很可能它是作为try-with-resources语句的一部分被关闭的。
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=8, args_size=1
0: ldc #2 // class test/Test
2: ldc #3 // String input.txt
4: invokevirtual #4 // Method java/lang/Class.getResource:(Ljava/lang/String;)Ljava/net/URL;
7: invokevirtual #5 // Method java/net/URL.toURI:()Ljava/net/URI;
10: invokestatic #6 // Method java/nio/file/Paths.get:(Ljava/net/URI;)Ljava/nio/file/Path;
13: astore_1
14: new #7 // class java/lang/StringBuilder
17: dup
18: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
21: astore_2
22: aload_1
23: invokestatic #9 // Method java/nio/file/Files.lines:(Ljava/nio/file/Path;)Ljava/util/stream/Stream;
26: astore_3
27: aconst_null
28: astore 4
30: aload_3
31: aload_2
32: invokedynamic #10, 0 // InvokeDynamic #0:accept:(Ljava/lang/StringBuilder;)Ljava/util/function/Consumer;
37: invokeinterface #11, 2 // InterfaceMethod java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V
42: aload_3
43: ifnull 131
46: aload 4
48: ifnull 72
51: aload_3
52: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V
57: goto 131
60: astore 5
62: aload 4
64: aload 5
66: invokevirtual #14 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
69: goto 131
72: aload_3
73: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V
78: goto 131
81: astore 5
83: aload 5
85: astore 4
87: aload 5
89: athrow
90: astore 6
92: aload_3
93: ifnull 128
96: aload 4
98: ifnull 122
101: aload_3
102: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V
107: goto 128
110: astore 7
112: aload 4
114: aload 7
116: invokevirtual #14 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
119: goto 128
122: aload_3
123: invokeinterface #12, 1 // InterfaceMethod java/util/stream/Stream.close:()V
128: aload 6
130: athrow
131: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
134: aload_2
135: invokevirtual #16 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
138: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
141: return
...
|
我们看到调用java/util/stream/Stream
where的出现forEach
,之前是对InvokeDynamic
a的引用的调用Consumer
。然后我们看到一大块字节码调用Stream.close以及调用的分支Throwable.addSuppressed
。这是编译器为try-with-resources语句生成的基本代码。
以下是完整性的原始来源:
public
static
void
main(String[] args)
throws
Exception {
Path path = Paths.get(Test.
class
.getResource(
"input.txt"
).toURI());
StringBuilder data =
new
StringBuilder();
try
(Stream lines = Files.lines(path)) {
lines.forEach(line -> data.append(line).append(
"n"
));
}
System.out.println(data.toString());
}
|
结论
由于字节码指令集的简单性以及在生成指令时几乎没有编译器优化,反汇编类文件可能是检查应用程序代码更改的一种方法,而不需要源代码,如果需要的话。
最后
以上就是暴躁绿草为你收集整理的Java字节简介Java字节简介的全部内容,希望文章能够帮你解决Java字节简介Java字节简介所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。