为了便于理解,以TestHelloWorld
类中有一个hello
方法为例,学习字节码和源代码之间的关联性。
TestHelloWorld类的hello方法代码如下:
public String hello(String content) {
String str = "Hello:";
return str + content;
}
hello
方法是一个非静态方法,返回值是String
,hello
方法有一个String
类型的参数。
编译后的栈指令如下:
{
"opcodes": [
"ldc #3 <Hello:>",
"astore_2",
"new #4 <java/lang/StringBuilder>",
"dup",
"invokespecial #5 <java/lang/StringBuilder.<init>>",
"aload_2",
"invokevirtual #6 <java/lang/StringBuilder.append>",
"aload_1",
"invokevirtual #6 <java/lang/StringBuilder.append>",
"invokevirtual #7 <java/lang/StringBuilder.toString>",
"areturn"
]
}
hello方法字节码解析
虽然hello
方法的代码非常简单,但是翻译成指令后就会变得比较难以理解了,有很多细节是隐藏在编译细节中的,比如return str + content;
是一个简单的两个字符串相加的操作,但实际上javac
编译时会创建一个StringBuilder
对象,然后调用append
方法来实现str
字符串和content
字符串相加的。
hello方法字节码解析:
ldc
表示的是将int, float或String型常量值从常量池中推送至栈顶,而ldc #3
表示的是将常量池中的第三个索引位置压入栈顶,也就是Hello:
;astore_2
表示的是将栈顶的值存入到局部变量表
的第二个位置,局部变量表
的索引位置是从0开始的,因为hello
方法是一个非静态方法,所以索引0表示的是this
对象(如果是static
方法那么就意味着没有this
对象,索引0就应该表示第一个参数)。索引1表示的是hello
方法的第一个参数,也就是String content
。如果在方法体中想创建一个新的对象,那么就必须计算这个变量在局部变量表中的索引位置,否则无法存储对象。还有一个需要特别注意的点是long
和double
是宽类型(wide type
)需要占用两个索引位置。astore_2
实际上表达的是将栈顶的对象压入到局部变量表中,等价于String arg2 = new String("Hello:")
;new #4
表示的是创建java/lang/StringBuilder
类实例;dup
表示复制栈顶数值并将复制值压入栈顶,即StringBuilder
对象;invokespecial #5
,invokespecial
表示的是调用超类构造方法,实例初始化方法,私有方法,即调用StringBuilder
类的构造方法(<init>
),#5
在常量池中是一个CONSTANT_METHOD_REF
类型的对象,用于表示一个类方法引用,invokespecial #5
实际上是在调用的StringBuilder
的构造方法,等价于:new StringBuilder()
;aload_2
表示的是加载局部变量表中的第二个变量,也就是读取astore_2
存入的值,即Hello:
。invokevirtual #6
表示的是调用StringBuilder
类的append
方法,等价于:sb.append("Hello:")
;aload_1
表示的是将局部变量表中的第一个变量压入栈顶,也就是将hello
方法的第一个参数content
的值压入栈顶;invokevirtual #6
,再次调用StringBuilder
类的append
方法,等价于:sb.append(content)
;invokevirtual #7
,调用StringBuilder
类的toString
方法,等价于:sb.toString()
;areturn
表示的是返回一个引用类型对象,需要注意的是如果不同的数据类型需要使用正确的return指令;
hello
方法的逻辑非常简单,如果只是看源代码的情况下我们可以秒懂该方法的执行流程和逻辑,但是如果我们从字节码层来看就会显得非常复杂不便于阅读;从第3步到第10步实际上只是在做源代码中的str + content
字符串相加操作而已。正是因为直接阅读虚拟机的指令对我们是一种非常不好的体验,所以才会有根据字节码逆向生成Java源代码的需求,通过反编译工具我们能够非常好的阅读程序逻辑,从而省去阅读字节码和指令的压力。但是反编译工具不是万能的,某些时候在解析指令的时候可能会报错,甚至是崩溃,所以为了更好的分析类业务逻辑以及学习ASM
字节码库,我们需要尽可能的掌握字节码解析和虚拟机指令解析的原理。