美文网首页
class文件属性表解析

class文件属性表解析

作者: shysheng | 来源:发表于2018-05-13 11:21 被阅读0次

class文件结构解析一文中,我们介绍了class文件的构成,整个class文件一共包含3部分共16个属性:

  • 3个描述文件属性的数据项:魔数和主次版本号
  • 11个描述类属性的数据项:类、字段、方法等信息
  • 2个描述代码属性的数据项:属性表,描述方法体内的具体内容

其中文件属性和类属性在上一篇中已经有过介绍,本文将主要介绍一下属性表。在最新的JVM规范中,一共定义了21个属性,接下来我将对其中一些关键属性进行分析。

对于每一个属性,它的结构可以分为3部分:

  • 一个u2类型的属性名称(attribute_name_index):从常量池中引用的一个常量
  • 一个u4类型的属性长度(attribute_length):属性值所占用的字节数
  • attribute_length个u1类型的属性值:具体的属性值
1. Code属性

java源文件方法体中的代码经过编译后,最终存储在Code属性内,它的结构如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count
  • attribute_name_index是一个指向常量池中某一个常量的索引,取值固定为Code
  • attribute_length表示属性值的长度
  • max_stack表示操作数栈的最大深度,jvm运行时会根据这个值来分配栈帧中的操作数栈深度
  • max_locals表示局部变量表所需要的存储空间,单位为slot
  • code_length代表字节码指令长度
  • code代表具体的字节码指令,根据jvm规范,每个字节码指令占用一个字节,jvm可以自动识别该指令是否需要接收参数。
  • exception_table_length表示异常表占用的字节数
  • exception_table表示具体的异常表
  • Code属性本身还有自己的一些属性表,包括LineNumberTable、LocalVariableTable和StackMapTable,这些属性不是必须的,如果有的话,会在attributes_count和attributes中体现出来
2. LineNumberTable属性

LineNumberTable是Code属性中的一个子属性,用来描述java源文件行号与字节码文件偏移量之间的对应关系。当程序运行抛出异常时,异常堆栈中显示出错的行号就是根据这个对应关系来显示的,它的结构如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

其中line_number_info结构如下:

类型 名称 数量 含义
u2 start_pc 1 字节码偏移量
u2 line_number 1 java源文件行号
3. LocalVariableTable属性

LocalVariableTable也是Code属性中的一个子属性,用来描述栈帧的局部变量表中变量与java源码中变量的对应关系,其结构如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 llocal_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

其中local_variable_info结构如下:

类型 名称 数量 含义
u2 start_pc 1 变量生命周期开始时的字节码偏移量
u2 length 1 变量作用范围覆盖的字节数
u2 name_index 1 索引值,指向变量名称
u2 descriptor_index 1 索引值,指向变量描述符
u2 index 1 变量在栈帧中slot的位置

LineNumberTable和LocalVariableTable都不是运行时必须的,可以在javac中使用-g:none选项来取消生成这两个属性,取消前后反编译出来的文件将丢失这两个属性,如下图所示:

默认条件,有属性信息 取消后,信息丢失
4. 实例分析

我们还是继续以上一篇中的代码为例进行分析:
java源文件:

java源文件.png

利用javap得到的字节码内容(这里只给出了main方法的):

// 这一部分是描述方法的元数据,在上一篇已经分析过
public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC

// 这一部分描述方法体的具体内容
  Code:

    // 操作数栈最大深度为3,局部变量表最多需要一个slot,有一个入参
    // 如果是实例方法,还会增加一个this对象的引用作为隐形参数
    stack=3, locals=1, args_size=1

       // 具体的字节码指令,共占用28个字节
       // 获取System.out属性值,压入栈顶
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       // 创建StringBuilder实例,并将其引用压入栈顶
       3: new           #3                  // class java/lang/StringBuilder
       // 复制栈顶元素,并压入栈顶
       6: dup
       // 调用init()方法,弹出栈顶元素
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
       // 加载常量Hello,压入栈顶
      10: ldc           #5                  // String Hello
       // 调用StringBuilder的append()方法
      12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       // 获取name属性值
      15: getstatic     #7                  // Field name:Ljava/lang/String;
       // 调用StringBuilder的append()方法
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       // 调用StringBuilder的toString()方法
      21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       // 调用PrintStream的println()方法
      24: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return

    // java源码行号与字节码文件偏移量的对应关系
    LineNumberTable:
      line 14: 0
      line 15: 27

    // 栈帧中局部变量表变量与java源码变量的对应关系
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      28     0  args   [Ljava/lang/String;

再来看看main方法code属性对应的十六进制文件,按照code属性结构对其进行分析,可以发现其内容与javap得到的结果完全一致。

main方法的Code属性.png

main方法里只看到了加载常量Hello的操作,那么有人可能会问,静态常量name的属性值是在哪里加载的呢?实际上,这一步在cinit()方法中就完成了。

static {};
  descriptor: ()V
  flags: ACC_STATIC
  Code:
    // 类方法,不会传入this引用,故args_size=0
    stack=1, locals=0, args_size=0
       // 从常量池加载静态常量,压入栈顶
       0: ldc           #10                 // String JVM
       // 为静态属性name赋值
       2: putstatic     #7                  // Field name:Ljava/lang/String;
       5: return
    LineNumberTable:
      line 11: 0

通过查看方法的字节码,可以更直观的看出方法内部的执行逻辑。比如说对于字符串连接操作:

 "Hello " + name;

底层实际上是通过新建一个StringBuilder对象来实现的:

StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(name);
sb.toString();

事实上,很多问题都可以由此迎刃而解,比如说i++和++i到底有什么区别。以下4段代码,最终i的值分别是多少呢?稍有经验的程序员都可以轻松给出答案,但是其实现原理是什么呢,我们不妨从字节码角度来略探一二。

    public static void incr1() {
        int i = 0;
        i = i++;
    }

    public static void incr2() {
        int i = 0;
        i++;
    }

    public static void incr3() {
        int i = 0;
        i = ++i;
    }

    public static void incr4() {
        int i = 0;
        ++i;
    }
  public static void incr1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: iinc          0, 1
         6: istore_0
         7: return
      LineNumberTable:
        line 20: 0
        line 21: 2
        line 22: 7
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2       6     0     i   I

  public static void incr2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iinc          0, 1
         5: return
      LineNumberTable:
        line 26: 0
        line 27: 2
        line 28: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2       4     0     i   I

  public static void incr3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iinc          0, 1
         5: iload_0
         6: istore_0
         7: return
      LineNumberTable:
        line 32: 0
        line 33: 2
        line 34: 7
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2       6     0     i   I

  public static void incr4();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iinc          0, 1
         5: return
      LineNumberTable:
        line 38: 0
        line 39: 2
        line 40: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2       4     0     i   I

具体分析可以参考占小狼的文章从字节码角度分析 i++ 和 ++i 实现,图文并茂,清晰易懂。

相关文章

网友评论

      本文标题:class文件属性表解析

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