美文网首页技术杂谈
Java字节码方法表

Java字节码方法表

作者: 香芋牛奶面包 | 来源:发表于2019-04-25 15:33 被阅读0次

引言

因为字段表和方法表的结构类似,所以我们直接分析Java字节码的方法表内容,理解了方法表,自然就理解了字段表

方法表

方法表在Class文件中的位置是在字段表之后的,具体的结构我们根据下表再来回顾一下

类型 名称 数量
u2 access_flas 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info 属性表 attributes_count

还是跟上篇文章一样,我们写一个简单的Java类


public class Test {

 public String sayHello() {

 String sayStr = "hello world";

 return sayStr;

 }

 public static void main(String[] args) {

 Test test = new Test();

 System.out.println(test.sayHello());

 }

}

使用javap翻译字节码文件


public class com.ymm.agent.Test

 minor version: 0

 major version: 52

 flags: (0x0021) ACC_PUBLIC, ACC_SUPER

 this_class: #3 // com/ymm/agent/Test

 super_class: #8  // java/lang/Object

 interfaces: 0, fields: 0, methods: 3, attributes: 1

Constant pool:

 #1 = Methodref #8.#27  // java/lang/Object."<init>":()V

 #2 = String  #28 // hello world

 #3 = Class #29 // com/ymm/agent/Test

 #4 = Methodref #3.#27  // com/ymm/agent/Test."<init>":()V

 #5 = Fieldref  #30.#31 // java/lang/System.out:Ljava/io/PrintStream;

 #6 = Methodref #3.#32  // com/ymm/agent/Test.sayHello:()Ljava/lang/String;

 #7 = Methodref #33.#34 // java/io/PrintStream.println:(Ljava/lang/String;)V

 #8 = Class #35 // java/lang/Object

 #9 = Utf8  <init>

 #10 = Utf8  ()V

 #11 = Utf8  Code

 #12 = Utf8  LineNumberTable

 #13 = Utf8  LocalVariableTable

 #14 = Utf8  this

 #15 = Utf8  Lcom/ymm/agent/Test;

 #16 = Utf8  sayHello

 #17 = Utf8  ()Ljava/lang/String;

 #18 = Utf8  sayStr

 #19 = Utf8  Ljava/lang/String;

 #20 = Utf8  main

 #21 = Utf8  ([Ljava/lang/String;)V

 #22 = Utf8  args

 #23 = Utf8  [Ljava/lang/String;

 #24 = Utf8  test

 #25 = Utf8  SourceFile

 #26 = Utf8  Test.java

 #27 = NameAndType #9:#10  // "<init>":()V

 #28 = Utf8  hello world

 #29 = Utf8  com/ymm/agent/Test

 #30 = Class #36 // java/lang/System

 #31 = NameAndType #37:#38 // out:Ljava/io/PrintStream;

 #32 = NameAndType #16:#17 // sayHello:()Ljava/lang/String;

 #33 = Class #39 // java/io/PrintStream

 #34 = NameAndType #40:#41 // println:(Ljava/lang/String;)V

 #35 = Utf8  java/lang/Object

 #36 = Utf8  java/lang/System

 #37 = Utf8  out

 #38 = Utf8  Ljava/io/PrintStream;

 #39 = Utf8  java/io/PrintStream

 #40 = Utf8  println

 #41 = Utf8  (Ljava/lang/String;)V

{

 public com.ymm.agent.Test();

 descriptor: ()V

 flags: (0x0001) ACC_PUBLIC

 Code:

 stack=1, locals=1, args_size=1

 0: aload_0

 1: invokespecial #1 // Method java/lang/Object."<init>":()V

 4: return

 LineNumberTable:

 line 9: 0

 LocalVariableTable:

 Start Length Slot Name  Signature

 0  5  0 this  Lcom/ymm/agent/Test;

 public java.lang.String sayHello();

 descriptor: ()Ljava/lang/String;

 flags: (0x0001) ACC_PUBLIC

 Code:

 stack=1, locals=2, args_size=1

 0: ldc  #2 // String hello world

 2: astore_1

 3: aload_1

 4: areturn

 LineNumberTable:

 line 12: 0

 line 13: 3

 LocalVariableTable:

 Start Length Slot Name  Signature

 0  5  0 this  Lcom/ymm/agent/Test;

 3  2  1 sayStr  Ljava/lang/String;

 public static void main(java.lang.String[]);

 descriptor: ([Ljava/lang/String;)V

 flags: (0x0009) ACC_PUBLIC, ACC_STATIC

 Code:

 stack=2, locals=2, args_size=1

 0: new  #3 // class com/ymm/agent/Test

 3: dup

 4: invokespecial #4 // Method "<init>":()V

 7: astore_1

 8: getstatic  #5 // Field java/lang/System.out:Ljava/io/PrintStream;

 11: aload_1

 12: invokevirtual #6 // Method sayHello:()Ljava/lang/String;

 15: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

 18: return

 LineNumberTable:

 line 17: 0

 line 18: 8

 line 19: 18

 LocalVariableTable:

 Start Length Slot Name  Signature

 0 19  0 args  [Ljava/lang/String;

 8 11  1 test  Lcom/ymm/agent/Test;

}

SourceFile: "Test.java"

通过上文的interfaces: 0, fields: 0, methods: 3, attributes: 1,我们可以知道该文件一共包含3个方法,分别对应着无参构造,sayHello,main三个方法

好了,现在让我们直接略过前面的一大推常量池项,

直接阅读我们自己写的sayHello方法的字节码内容


public java.lang.String sayHello();

 descriptor: ()Ljava/lang/String; /* 方法描述符 */

 flags: (0x0001) ACC_PUBLIC /* 访问标识 */

 Code: /* code属性  也就是我们写的具体代码翻译成的字节码指令内容 */

 stack=1, locals=2, args_size=1 /* 分别指操作数栈深度;本地变量所需的存储空间(slot为单位);参数个数 */

 0: ldc  #2 // String hello world /*将一个常量加载到操作数栈*/

 2: astore_1 /* 将一个数值从操作数栈存储到局部变量表 */

 3: aload_1 /* 将一个数值从局部变量表加载到操作数栈 */

 4: areturn /* 将栈顶第一个元素返回 */

 LineNumberTable: /* 字节码与java代码行数对应关系 一般用于调试 */

 line 12: 0

 line 13: 3

 LocalVariableTable: /* 局部变量表 */

 Start Length Slot Name  Signature

 0  5  0 this  Lcom/ymm/agent/Test;

 3  2  1 sayStr  Ljava/lang/String;

根据上表我们知道方法表的第一个内容的是access_flas,占一个字节。表中的access_flas其实就对应着上文中的 flags: (0x0001) ACC_PUBLIC,标识这个方法是public的,接下在的name_indexdescriptor_index,在上文中的体现分别对应着sayHello()Ljava/lang/String;

这里解释一下描述符的概念,在介绍Class文件结构的文章中有提到,这里再提一遍:

描述符的作用是用来描述字段的数据类型,方法的参数列表和返回值,根据描述符规则,基本数据类型(byte,char,int,long,float,double,short,boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用L加对象的全限定名来表示

最重要的内容其实还是在attribute_info属性表中的code内容,也就是我们写的具体代码翻译成的字节码指令内容,这块内容是我们阅读字节码文件的重中之重。下面我们就来阅读一下上文的字节码内容

  1. ldc #2 将常量池中索引为2的字符串加载到操作数栈顶

  2. astore_1 将一个数值从操作数栈存储到局部变量表

  3. aload_1 将一个数值从局部变量表加载到操作数栈

  4. areturn 将栈顶第一个元素返回

可以看到上面的四个步骤的操作其实都是基于栈的操作,这里提一下java虚拟机栈的栈帧结构

image.png

关于字节码子令集可以参考java字节码子令集

有趣的例子 ---

下面我们再来看一个有趣的例子,大家思考一下这个执行这个方法的返回值会是多少?


public class Test {

 public int inc() {

 int x;

 try {

 x = 1;

 return x;

 } catch (Exception e) {

 x = 2;

 return x;

 } finally {

 x = 3;

 }

 }

}

代码非常简单,我想大家应该也都知道正确答案,当没有出现异常的时候,返回值为1,出现异常的话则为2(当然这里不会抛异常)。可是如果我们在finally快里加句代码System.out.prinln("do it"),然后再执行这个方法,其实是可以看到do it被打印了,也就是说在执行return之前,finally快中的代码是被执行了的,那么这里就有一个有趣的问题了,为何返回仍然是2而不是3呢?

下面我们就从字节码文件中来找出答案


public int inc();

 descriptor: ()I

 flags: (0x0001) ACC_PUBLIC

 Code:

 stack=1, locals=5, args_size=1

 0: iconst_1 // 将int = 1的值压入栈顶

 1: istore_1 // 弹出栈顶元素,存入位置为1的局部变量表

 2: iload_1 // 从位置为1的局部变量中取出元素压入栈顶

 3: istore_2 // 弹出栈顶元素,存入位置2的局部变量中

 4: iconst_3 // 将int = 3的值压入栈顶 (这里执行finally块中的代码了)

 5: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

 6: iload_2 // 从位置为2的局部变量中取出元素压入栈顶

 7: ireturn // 返回栈顶元素2 (哈哈哈  看到没有,这里返回的是2,没有异常的话,这里方法就返回了)

 8: astore_2 // 将栈顶的异常引用,存入位置2的局部变量中  (这里就是异常捕获的代码了)

 9: iconst_2 // 将int = 2的值压入栈顶

 10: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

 11: iload_1 // 从位置为1的局部变量中取出元素压入栈顶

 12: istore_3 // 弹出栈顶元素,存入位置3的局部变量中

 13: iconst_3 // 将int = 3的值压入栈顶

 14: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

 15: iload_3 // 从位置为3的局部变量中取出元素压入栈顶

 16: ireturn // 返回栈顶元素 2

 17: astore 4 // 将栈顶异常引用存入位置为4的局部变量表中

 19: iconst_3 // 将int = 3的值压入栈顶

 20: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

 21: aload  4 将位置为4的局部变量引用压入栈顶

 23: athrow // 将栈顶的异常抛出

 Exception table: // 异常表

 from to target type

 0  4  8  Class java/lang/Exception

 0  4 17  any

 8 13 17  any

 17 19 17  any

如果你已经看懂了上面的字节码,你应该会豁朗开朗。原因其实就是在于这里开辟了两个局部变量,finally块中的代码也确实执行了,只是将变量存入了局部变量表中的另一个位置,并且通过这个例子,也可以发现,无论什么情况finally块中的代码都会执行。

尾言

好了,本文到这里就差不多结束了。对于字节码的探索个人觉的还是非常有意思的,之后应该也会探索更多有意思的东西

博客原文戳这里

相关文章

  • Java字节码方法表

    引言 因为字段表和方法表的结构类似,所以我们直接分析Java字节码的方法表内容,理解了方法表,自然就理解了字段表 ...

  • Jvm基础知识下篇

    一、java执行引擎工作原理:方法调用 1.进行方法调用 Java语言的原子指令是字节码,java方法是对字节码的...

  • 字节码引用检测原理与实战

    一、字节码与引用检测 1.1 Java字节码 本章中的字节码重点研究Java 字节码,Java字节码(Java b...

  • java字节码

      我们都知道Java字节码是JVM所使用的指令集。java字节码可以分为如下几类: 操作数栈   Java 方法...

  • java注解底层详解

    首先上参考文章:JAVA 注解的基本原理Java字节码方法表与属性表深度剖析这篇写的太好了,醍醐灌顶。 文章具体内...

  • 程序员练级攻略(2018):Java底层知识

    Java 字节码相关 首先,Java 最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。...

  • 12.JVM学习笔记_字节码知识点整理

    字节码知识点整理 对于Java类中的每个实例方法(非static方法),其在编译后所生成的字节码当中,方法参数的数...

  • 一些防止 Java 代码被反编译的方法

    由于Java字节码的抽象级别较高,因此它们较容易被反编译。本节介绍了几种常用的方法,用于保护Java字节码不被反编...

  • 浅谈 JVM 3:指令集及其执行

    上文我们聊了如何参考 JVM 规范 解读 Java 字节码。但对于字节码方法字段 Code 中的 JVM 指令 执...

  • Java并发机制的底层原理

    Java程序执行:Java代码→Java字节码→字节码被类加载器加载到JVM里,JVM执行字节码→转化为汇编指令在...

网友评论

    本文标题:Java字节码方法表

    本文链接:https://www.haomeiwen.com/subject/qvdzgqtx.html