JVM篇-字节码文件解析

知识点:

魔数CAFEBABE:每一个Java Class文件都是以0x CAFEBABE开头的。Java这么做的原因就是为了快速判断一个文件是不是有可能为class文件,以及这个class文件有没有受损(文件受损,文件开头受损的可能性最大)。

大端模式(高尾端):数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端模式(低尾端):数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
图解大端小端

常量池

  • class文件中的常量池:静态的
  • 运行时常量池:动态的,可用HSDB查看
  • 字符串常量池:StringTable

一个类中最多能实现65535个接口。

class文件的数据组织格式解读

class文件数据格式

字节码文件组成解析图

一个字节码文件由图中展示部分组成,其中u后面数字表示这个部分所占字节码个数

源文件

public class ClassStructureTest {

    public static int a = 0;
    public static void main(String[] args) {

    }
}

使用hexdump命令(如果有Git工具,可以在git窗口使用xxd命令解析)解析结果(括号是自己做的标记):

000000  ca fe ba be 00 00 00 34 00 1a 0a 00 04 00 16 09
000010  00 03 00 17 07 00 18 07 00 19 01 00 01 61 01 00
000020  01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
000030  56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
000040  75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
000050  61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
000060  00 04 74 68 69 73 01 00 28 4c 63 6e 2f 69 64 77
000070  61 72 66 2f 6c 75 62 61 6e 2f 6a 76 6d 2f 43 6c
000080  61 73 73 53 74 72 75 63 74 75 72 65 54 65 73 74
000090  3b 01 00 04 6d 61 69 6e 01 00 16 28 5b 4c 6a 61
0000a0  76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29
0000b0  56 01 00 04 61 72 67 73 01 00 13 5b 4c 6a 61 76
0000c0  61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 01 00
0000d0  10 4d 65 74 68 6f 64 50 61 72 61 6d 65 74 65 72
0000e0  73 01 00 08 3c 63 6c 69 6e 69 74 3e 01 00 0a 53
0000f0  6f 75 72 63 65 46 69 6c 65 01 00 17 43 6c 61 73
000100  73 53 74 72 75 63 74 75 72 65 54 65 73 74 2e 6a
000110  61 76 61 0c 00 07 00 08 0c 00 05 00 06 01 00 26
000120  63 6e 2f 69 64 77 61 72 66 2f 6c 75 62 61 6e 2f
000130  6a 76 6d 2f 43 6c 61 73 73 53 74 72 75 63 74 75
000140  72 65 54 65 73 74 01 00 10 6a 61 76 61 2f 6c 61
000150  6e 67 2f 4f 62 6a 65 63 74(constant pool) 00 21 00 03 00 04 00
000160  00 00 01 00 09 00 05 00 06 00 00 00 03 00 01 00
000170  07 00 08 00 01 00 09(code) 00 00 00 2f 00 01 00 01 00
000180  00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 00
000190  00 06 00 01 00 00 00 09 00 0b 00 00 00 0c 00 01
0001a0  00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 0f
0001b0  00 02 00 09 00 00 00 2b 00 00 00 01 00 00 00 01
0001c0  b1 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00
0001d0  10 00 0b 00 00 00 0c 00 01 00 00 00 01 00 10 00
0001e0  11 00 00 00 12 00 00 00 05 01 00 10 00 00 00 08
0001f0  00 13 00 08 00 01 00 09 00 00 00 1d 00 01 00 00
000200  00 00 00 05 03 b3 00 02 b1 00 00 00 01 00 0a 00
000210  00 00 06 00 01 00 00 00 0b (attribute)00 01 00 14 00 00 00
000220  02 00 15
魔数

4个字节=>ca fe ba be

次版本号(minor)

两个字节=>00 00
java主次版本号关系
JDK各版本号关系

主版本号(major)

两个字节=>00 34,对应JDK版本1.8

常量池个数

两个字节=>00 1a,表示有26个,常量池计数从1开始(网传0被null占用)。 真正的常量池个数为字节码文件中的常量池个数减1(javap中打印出来只有25个,#1-#25)。

常量池解析

class文件结构中常量池14种数据类型结构表

上图常量池表结构中可以发现第一个字节为tag。0a(十进制:10)从常量池表结构中查找,tag为10的类型为Methodref。Methodref中下一个结构为index占2个字节,值为:00 04(十进制:4)。下一个结构`index也是两个字节,值为:00 16(十进制:22)。javap中的常量池第一行的结果(#4,#22)印证了这一结论。而此时#4是未知的,还未加载,这时候只能存一个符号引用,这也是为什么java中有符号引用转直接引用的原因。

类的访问控制权限

两个字节=>00 21。由于java修饰符的多样性,并没有穷举了每一种组合,而是每一个访问修饰符定义了一个值,0x0021是0x0020和0x0001的并集,表示ACC_PUBLIC与ACC_SUPER。表示该类是public的并能调用父类的。
类访问和属性修饰符标志

类名

两个字节
00 03(十进制:3),寻找常量池中#3的元素。
常量池中#3显示的类名

父类名

两个字节=>00 04(十进制:4),寻找常量池中#4的元素。
常量池中#4显示的父类名

实现的接口数

两个字节=>00 00

实现的接口

如果实现的接口数是0,则字节码文件中不出现此部分。举例中未定义,故而不出现此部分。

成员属性数量

两个字节=>00 01

成员属性:
field_info {
	u2 access_flags;	//属性修饰符
	u2 name_index;		//常量池中的属性名索引
	u2 descriptor_index;	//常量池中的字段索引
	u2 attributes_count;	//属性数量
	attribute_info attributes[attributes_count];//常量池的属性索引,结构是Attribute结构
}
attribute_info {
    u2 attribute_name_index;	//常量池中的属性名索引
    u4 attribute_length;	//属性长度
    u1 info[attribute_length];	//属性信息
}

开始解析:

  • access_flags:00 09,类访问和属性修饰符标志并未出现0009,与上述修饰符解释一致,应为public加上static,字节码文件也能印证这一点,如下图所示。
  • name_index:00 05,寻找常量池第五个元素。
  • descriptor_index:00 06,寻找常量池第六个元素,如下图字段描述符为I,由字段符描述表查询可知表示int。
    字段描述符
  • attributes_count:00 00,表示长度为0。
  • attribute_info:attributes_count为0,字节码中没有此区域。
成员方法数量

两个字节=>00 03,表示有三个,显式的方法只有一个main()方法,其实还有两个隐式的方法就是<clinit>()方法和<init>()方法,详情可参考文章:JVM-类加载机制
成员方法预览

成员方法
method_info {
    u2             access_flags;	//访问标志
    u2             name_index;		//方法名索引
    u2             descriptor_index;	//属性计数器
    u2             attributes_count;	//属性信息(Code、Exception)
    attribute_info attributes[attributes_count];//方法体Code
}

开始解析:

  • access_flags:00 01,通过类访问和属性修饰符标志表可知为ACC_PUBLIC。
  • name_index:00 07,寻找常量池第七个元素,可知此方法为<init>()方法。
    <init>()方法
  • descriptor_index:00 08,寻找常量池第八个元素,由字段符描述表可查()V表示方法。
  • attributes_count:00 01,表示长度为1。
  • attribute_info:00 09,截图可知为Code,表示方法体。

方法体(Code)解析
方法体中的代码,经Javac编译器处理后存放在方法的Code属性表中。抽象方法和接口不存在Code属性。

Code_attribute {
 u2 attribute_name_index;	//对应常量池的索引,CONSTANT_Utf8_info结构,表示字符串“Code”
 u4 attribute_length;		//属性长度
 u2 max_stack;			//操作数栈最大深度(栈帧中操作数栈的长度由编译期决定)
 u2 max_locals;			//局部变量最大槽数
 u4 code_length;		//字节码长度
 u1 code[code_length];		//方法体、字节码指令
 u2 exception_table_length;	
 { 
    u2 start_pc;		//起始PC
    u2 end_pc;			//结束PC
    u2 handler_pc;		//跳转PC
    u2 catch_type;		//捕获类型
 } 
 exception_table[exception_table_length];
 u2 attributes_count;		//Code属性的属性数量
 attribute_info attributes[attributes_count];//Code属性的属性信息
}

Code属性的属性分为LineNumberTable(灰色地带:行号)和LocalVariableTable(局部变量表)。

LineNumberTable_attribute {
    u2 attribute_name_index;	//属性名索引,对应常量池的索引
    u4 attribute_length;	//属性长度
    u2 line_number_table_length;
    { 
        u2 start_pc;		//起始PC
        u2 line_number;		//行号
    } line_number_table[line_number_table_length];
}
LocalVariableTable_attribute {
    u2 attribute_name_index;		//属性名索引,对应常量池的索引
    u4 attribute_length;		//属性长度
    u2 local_variable_table_length;
    { 
        u2 start_pc;			//起始PC
        u2 length;			//长度
        u2 name_index;			//名字索引,对应常量池的索引
        u2 descriptor_index;		//描述符索引,对应常量池的索引
        u2 index;			//序号
    } local_variable_table[local_variable_table_length];
}

注意:属性和方法上都有exception信息,方法上throws Exception和方法内try/catch所生成的字节码文件是不一样的。
开始解析Code

  • attribute_name_index:00 09

  • attribute_length:00 00 00 2f
    属性名和长度

  • max_stack:00 01,

  • max_locals:00 01,

  • code_length: 00 00 00 05,

  • code:code_length为5字节,往后取5个字节即为code=>2a b7 00 01 b1
    通过查询JVM指令,可以知道以上指令代表的含义。
    2a:将第一个引用类型本地变量推送至栈顶。
    b7:调用超类构造方法,实例初始化方法,私有方法。
    00:什么都不做(方法体为空)。
    01:将 null 推送至栈顶。
    b1:从当前方法返回void。

  • exception_table_length:00 00

  • exception_table:exception_table_length为0不存在此区域

  • attributes_count:00 02

  • attribute_info:

开始解析LineNumberTable
LineNumberTable
此图可直接印证下面值的正确性。

  • attribute_name_index:00 0a,可见常量池中第十个为:LineNumberTable。
  • attribute_length:00 00 00 06
  • line_number_table_length:00 01
  • start_pc:00 00
  • line_number:00 09

开始解析LocalVariableTable
LocalVariableTable
此图可直接印证下面值的正确性。

  • attribute_name_index:00 0b
  • attribute_length:00 00 00 0c
  • local_variable_table_length:00 01
  • start_pc:00 00
  • length:00 05
  • name_index:00 0c
  • descriptor_index:00 0d
  • index:00 00

字段符描述表
描述举例:

  1. ()V 表示方法
  2. V 表示void
  3. B 表示byte
  4. C 表示char
  5. D 表示double
  6. Ljava/lang/String; 表示引用类型String
  7. [B 表示byte数组
  8. [Ljava/lang/String; 表示String数组

方法描述符规则:(数据类型的描述符)返回值的描述符。

解析方法举例
    descriptor: ([[Ljava/lang/String;Ljava/lang/Integer;LTest;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=4, args_size=4
         0: ldc           #4                  // String
         2: areturn
      LineNumberTable:
        line 17: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   LClassLoaderTest;
            0       3     1   arr   [[Ljava/lang/String;
            0       3     2     a   Ljava/lang/Integer;
            0       3     3  test   LTest;

则还原为方法是:

public java.lang.String testMethod(java.lang.String[][] arr, java.lang.Integer a, Test test); 
类属性数量

两字字节=>00 01

类属性
SourceFile_attribute {
    u2 attribute_name_index;        // 属性名索引,对应常量池的索引
    u4 attribute_length;            // 属性长度
    u2 sourcefile_index;            // 源文件名索引,对应常量池的索引
}

开始解析
SourceFile

  • attribute_name_index:00 14=>SourceFile
  • attribute_length:00 00 00 02
  • info:00 15=>ClassStructureTest.java
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值