为了便于理解,以TestHelloWorld类中有一个hello方法为例,学习字节码和源代码之间的关联性。
TestHelloWorld类的hello方法代码如下:

  1. public String hello(String content) {
  2. String str = "Hello:";
  3. return str + content;
  4. }

hello方法是一个非静态方法,返回值是Stringhello方法有一个String类型的参数。
编译后的栈指令如下:

  1. {
  2. "opcodes": [
  3. "ldc #3 <Hello:>",
  4. "astore_2",
  5. "new #4 <java/lang/StringBuilder>",
  6. "dup",
  7. "invokespecial #5 <java/lang/StringBuilder.<init>>",
  8. "aload_2",
  9. "invokevirtual #6 <java/lang/StringBuilder.append>",
  10. "aload_1",
  11. "invokevirtual #6 <java/lang/StringBuilder.append>",
  12. "invokevirtual #7 <java/lang/StringBuilder.toString>",
  13. "areturn"
  14. ]
  15. }

hello方法字节码解析

虽然hello方法的代码非常简单,但是翻译成指令后就会变得比较难以理解了,有很多细节是隐藏在编译细节中的,比如return str + content;是一个简单的两个字符串相加的操作,但实际上javac编译时会创建一个StringBuilder对象,然后调用append方法来实现str字符串和content字符串相加的。

hello方法字节码解析:

  1. ldc表示的是将int, float或String型常量值从常量池中推送至栈顶,而ldc #3表示的是将常量池中的第三个索引位置压入栈顶,也就是Hello:
  2. astore_2表示的是将栈顶的值存入到局部变量表的第二个位置,局部变量表的索引位置是从0开始的,因为hello方法是一个非静态方法,所以索引0表示的是this对象(如果是static方法那么就意味着没有this对象,索引0就应该表示第一个参数)。索引1表示的是hello方法的第一个参数,也就是String content。如果在方法体中想创建一个新的对象,那么就必须计算这个变量在局部变量表中的索引位置,否则无法存储对象。还有一个需要特别注意的点是longdouble是宽类型(wide type)需要占用两个索引位置。astore_2实际上表达的是将栈顶的对象压入到局部变量表中,等价于String arg2 = new String("Hello:")
  3. new #4表示的是创建java/lang/StringBuilder类实例;
  4. dup表示复制栈顶数值并将复制值压入栈顶,即StringBuilder对象;
  5. invokespecial #5invokespecial表示的是调用超类构造方法,实例初始化方法,私有方法,即调用StringBuilder类的构造方法(<init>),#5在常量池中是一个CONSTANT_METHOD_REF类型的对象,用于表示一个类方法引用,invokespecial #5实际上是在调用的StringBuilder的构造方法,等价于:new StringBuilder()
  6. aload_2表示的是加载局部变量表中的第二个变量,也就是读取astore_2存入的值,即Hello:
  7. invokevirtual #6表示的是调用StringBuilder类的append方法,等价于:sb.append("Hello:")
  8. aload_1表示的是将局部变量表中的第一个变量压入栈顶,也就是将hello方法的第一个参数content的值压入栈顶;
  9. invokevirtual #6,再次调用StringBuilder类的append方法,等价于:sb.append(content)
  10. invokevirtual #7,调用StringBuilder类的toString方法,等价于:sb.toString()
  11. areturn表示的是返回一个引用类型对象,需要注意的是如果不同的数据类型需要使用正确的return指令;

hello方法的逻辑非常简单,如果只是看源代码的情况下我们可以秒懂该方法的执行流程和逻辑,但是如果我们从字节码层来看就会显得非常复杂不便于阅读;从第3步到第10步实际上只是在做源代码中的str + content字符串相加操作而已。正是因为直接阅读虚拟机的指令对我们是一种非常不好的体验,所以才会有根据字节码逆向生成Java源代码的需求,通过反编译工具我们能够非常好的阅读程序逻辑,从而省去阅读字节码和指令的压力。但是反编译工具不是万能的,某些时候在解析指令的时候可能会报错,甚至是崩溃,所以为了更好的分析类业务逻辑以及学习ASM字节码库,我们需要尽可能的掌握字节码解析和虚拟机指令解析的原理。