美文网首页
粗谈Java虚拟机2_Class文件分析

粗谈Java虚拟机2_Class文件分析

作者: 杨杰C | 来源:发表于2019-08-13 13:44 被阅读0次

1. 前言

class文件作为 JVM 的可执行文件,在可读性方面比 C语言 等直接编译成平台可执行文件的语言强太多,反编译class文件往往能够得到不错的效果。而一个类无论代码的多少,在结构上都大同小异。在源码级,类的结构由上而下大致为:当前类的包名路径、引用类的包名路径、当前类的信息(类名、父类、接口)、变量、方法、内部类/内部接口/枚举/注解等。Javac编译器也按照该顺序来编译源码。

class_info.png

同时将代码中所使用到的引用类包名路径、变量命名和赋予的值,方法名等信息,均都存储在常量池中。当需要使用时,以索引下标的方式指向常量池中的位置。分析class文件没有任何的技术难度,明白各个数据项的结构,分析起来将会很轻松。


本文以下列源码为例,带大家一起来分析 class 文件 :

public class Person implements Serializable{
    
    private static final long serialVersionUID = 1L;
    
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    
}

编译为 class 文件后:


Person_class.png

2. 魔数和Class编译版本

  • 魔数,CA FE BA BE

    固定值,标识文件是否是 class 文件。

  • 版本,00 00 00 34

    编译JDK版本,minor_version 和 major_version 一起确定class文件格式的版本,前2个字节小版本,后两个字节大版本。


    jdk_version.png

3. 常量池

常量池中存储 class文件 中的各种符号引用和字面量,是Class文件的资源仓库,有多全面呢?源码一眼望去,除了各种的关键字和方法体中的字节码指令,其余全部存储在常量池当中,当需要使用时,通过索引下标的方式来引用。常量类型:

Tag 类型 描述
ox1 CONSTANT_Uft8 Utf-8编码的字符串
ox3 CONSTANT_Integer 整型字面量
ox4 CONSTANT_Float 浮点型字面量
ox5 CONSTANT_Long 长整型字面量
ox6 CONSTANT_Double 双精度字面量
ox7 CONSTANT_Class 类或接口的符号引用
ox8 CONSTANT_String 字符串类型字面量
ox9 CONSTANT_Fieldref 字段的符号应用
ox10 CONSTANT_Methodref 类中方法的符号引用
ox11 CONSTANT_InterfaceMathodref 接口中方法的符号引用
ox12 CONSTANT_NameAndType 字段或方法的部分符号引用
ox15 CONSTANT_MethodHand 表示方法句柄
ox16 CONSTANT_MethType 标识方法类型
ox18 CONSTANT_InvokedDaynamic 表示一个动态方法调用点

微信截图_20190729095157.png

前两个字节 0x001F 为常量池大小,共有 31 项常量。第 1 项常量 tag 为 07 查上图得知为 class_info 类型,class_info 结构如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

长度为 u2 大小的 name_index 必须指向一个 CONSTANT_Utf8_info 类型的常量。此处 02 指向第二项常量。第 2 项常量 tag 为 01 查表得知为 CONSTANT_Utf8_info 类型常量,结构如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

对应的字节为:01 00 10 + 后面依次16个字节(linked/TestClass ) 点击查看更多类型,使用 JDK 自带的 javap 工具,可查看常量池信息:

constant_pool.png

4. 类信息

4.1 访问标志

常量池结束后,紧接着就是类或接口的信息。前两个字节代表类或接口的访问标志,被哪些关键字修饰:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 是否声明为public
ACC_FINAL 0x0010 是否声明为final
ACC_SUPER 0x0020 是否使用1.2版本编译器(支持使用新的invokespecial指令)
ACC_INTERFACE 0x0200 接口类型
ACC_ABSTRACT 0x0400 抽象类_类型
ACC_SYNTHETIC 0x0100 编译器合成,并非用户代码编写
ACC_ANNOTATION 0x2000 注解类型
ACC_ENUM 0x4000 枚举类型

通过ACC_INTERFACE 标志来区分 Class 是类还是接口。如果设置 ACC_INTERFACE ,则 ACC_ABSTRACT、ACC_SUPER、ACC_ENUM 不能设置。

acc_super标志是否使用invokespecial指令,在调用构造方法或使用super.xxx()显示调用父类方法时,会使用该指令。1.2之前invokespecial对方法的调用都是静态绑定的。java1.2 的时候增加了动态绑定的功能。

当前类被声明为 public类型,使用1.2以上编译器。ACC_PUBLICACC_SUPER为真,0x0001 | 0x0020 = 0x0021。

class_access.png
4.2 类索引、父类索引和接口索引

接下来分别是类、父类、接口信息,直接引用常量池中的符号引用,类的全限命名。结构上类信息、父类信息都用一个u2大小字节引用常量池中的索引。由于接口是可以多实现的,所以先用u2字节大小记录实现了多少个接口,再依次在后面数几个u2大小字节。

class_interface_info.png
类型 索引 常量池
类索引 0x0001 linked/Person
父类索引 0x0003 java/lang/Object

Person 类实现了 Serializable 接口,所以 interface_count 等于 1,紧接着 0005 代表引用常量池索引为 5 的常量。得知为 java/io/Serializable

5. 字段表

字段表值包含成员变量,常量会存在常量池当中。

字段表结构:

类型 名称 数量 说明
u2 access_flags 1 修饰符/访问标志
u2 name_index 1 变量命名
u2 descriptor 1 数据类型
u2 attributes_count 1 属性表长度
attribute_info attributes attributes_count 属性表

修饰符/访问标志:

标志名称 标志值 含义 标志名称 标志值 含义
ACC_PUBLIC 0X0001 字段是否public ACC_VOLATILE 0x0040 字段是否volatile
ACC_PRIVATE 0x0002 字段是否private ACC_TRANSIENT 0x0080 字段是否transient
ACC_PROTECTED 0x0004 字段是否protected ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生
ACC_STATIC 0x0008 字段是否static ACC_ENUM 0x4000 字段是否enum
ACC_FINAL 0x0010 字段是否final

字段类型:
除开 八大基本类型 和 引用类型,还有数组类型,方法还有 Void 类型。

描述符 类型 描述符 类型
B byte J long
C char S short
D double Z boolean
F float V void
I int L 引用类型
[ 数组纬度
filed_table.png

0x0002 字段表数量,只有二个字段

第一个字段:

  • 访问标志 0x001A :private static final 0x02 | 0x08 | 0x10 等于 0x1A
  • 变量名 0x0007 :serialVersionUID
  • 变量类型 0x0008 :对应常量池中的 J,转换为 long 类型。
  • 一个属性 0x0001 :详见* 7.1ConstantValue属性分析

第二个字段:

  • 访问标志 0x0002:private
  • 变量名 0x000C:name
  • 变量类型 0x000D: Ljava/lang/String;
  • 没有属性 0x0000;

6. 方法表

方法的结构有各种修饰符、返回值类型、方法名、参数列表、再就是方法体内的代码。结构如下:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
微信截图_20190729104127.png
  • 0x0003 3个方法

第一个方法:默认构造函数

  • 0x0001 访问标志:public
  • 0x000E 方法名:init
  • 0x000F 返回值类型:V()
  • 0x0001 一个属性:00 10 在常量池中Code属性,详见 7.2Code属性分析

7. 属性表

字段和方法表中都可以携带自己的属性,JVM 的属性表类型较多,本篇文章只泛泛而谈出现的两种(更多属性):

7.1 ConstantValue属性

同时被 staticfinal 关键字修饰的字段,会以 ConstantValue 属性表的结构存储在Class文件种,也就是平时大家说的常量。并且做为类或接口的一部分,存储在运行时常量池当中。格式如下:

ConstantValue_attribute {
    u2 attribute_name_index; //属性表名称位于常量池中的位置
    u4 attribute_length;    //属性表长度大小
    u2 constantvalue_index; //常量值位于常量池中的位置
}

接着上面的Class文件分析:

  • 00 09 对应常量池中的 ConstantValue
  • 00 00 00 02 :两个字节长度
  • 00 0A :1L
7.2 Code属性

方法体中的代码,经Javac编译器处理后存放在方法的Code属性表中。抽象方法和接口不存在Code属性。Code属性结构如下∶

Code_attribute {
 u2 attribute_name_index;
 u4 attribute_length;
 u2 max_stack;
 u2 max_locals;
 u4 code_length;
 u1 code[code_length];
 u2 exception_table_length;
 { 
    u2 start_pc;
    u2 end_pc;
    u2 handler_pc;
    u2 catch_type;
 } 
 exception_table[exception_table_length];
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

对应的字节码为:


微信截图_20190729111806.png

使用 javap 命令得出的:


微信截图_20190729104624.png
  • 0x0010,Code
  • 0x0001,操作数栈大小
  • 0x0001,局部变量表大小
  • 0x2A,从表中查询对应的指令为aload_0,该指令将局部变量表的第一个(索引为0)变量推送至栈顶。
  • 0xB7,从表中查询对应的指令为invokespecial,方法调用指令,必须声明一个方法命名,可以是构造方法、父类的构造方法、私有方法。后面u2大小,指向常量池中一个CONSTANT_Methodref_info类型的项。
  • 0x0011,invokespecial 所执行的方法,对应常量池中的<init>方法的符号引用。
  • 0x00B1,从表中查询对应的指令为return,当前方法返回 void。
  • 0x0000,不存在异常表

code_legth 虽然是一个u4大小的值,理论上可以存储0xffffffff(40多亿)大小的字节码指令,但是虚拟机规范中明确规定限制了一个方法不超过65535条字节码指令,实际只使用了u2(0xffff)大小。

点击查看字节码指令

相关文章

网友评论

      本文标题:粗谈Java虚拟机2_Class文件分析

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