class文件属性表解析

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 实现,图文并茂,清晰易懂。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容