深入理解Class文件结构

深入理解Class文件结构

概述

我们都知道编写的Java的源码会先编译成Class文件,java虚拟机再将Class文件解释编译成对应平台的机器指令,所以能够解析Class文件的数据结构是非常有必要的。

先编写一段java源码,定义一个类并实现一个接口,类内部定义了一个成员变量、一个类变量和一个sum方法。

package com.changyy.jvm;

public class ClassTest implements IClassTest {
    
    private int n = 10;
    
    private static int m = 5;
    
    public int sum(int p) {
        return n + p;
    }
}
package com.changyy.jvm;

public interface IClassTest {
}

查看编译后的Class文件,可以使用notepad++的HEX-Editor插件、IDEA的BinEd插件等工具。

image

Class文件只有两种数据类型:“无符号数”和“表”

  • 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表:是由多个无符号数或者其他作为数据项构成的复合数据类型,表一般以“_info”结尾。

Class文件结构表

类型 名称 说明 长度
u4 magic 魔数,识别Class文件格式 4个字节
u2 minor_version 次版本号 2个字节
u2 major_version 主版本号 2个字节
u2 constant_pool_count 常量池容量计数值 2个字节
cp_info constant_pool 常量池 n个字节
u2 access_flags 访问标志 2个字节
u2 this_class 类索引 2个字节
u2 super_class 父类索引 2个字节
u2 interfaces_count 接口计数器 2个字节
u2 interfaces 接口索引集合 2个字节
u2 fields_count 字段个数 2个字节
field_info fields 字段集合 n个字节
u2 methods_count 方法计数器 2个字节
method_info methods 方法集合 n个字节
u2 attributes_count 属性计数器 2个字节
attribute_info attributes 属性集合 n个字节

接下来开始分析字节码

魔数

Class文件的前4个字节称为魔数(0xCAFEBABE、咖啡宝贝),作用是确定这个文件是否能被java虚拟机所接受的Class文件.

image

版本号

按照Class文件结构表接下来是两个字节的次版本号和两个字节的主版本号,次版本号0x0000 -> 0,主版本号 0x0031 ->52(十六进制转十进制),版本号52.0也就是对应jdk1.8。

[图片上传失败...(image-ae6eaa-1604406038568)]

常量池

接下来是常量池容器计数值,两个字节0x001d -> 29 ,代表常量池中有28项常量,索引值范围位1~28。为什么不是从0开始,是因为它把第0项常量空出来了。这是为了在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

[图片上传失败...(image-ea57e1-1604406038568)]

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

  1. 字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。

  2. 符号引用总结起来则包括了下面三类常量:

    • 被模块导出或者开放的包

    • 类和接口的全限定名

    • 字段的名称和描述符

    • 方法的名称和描述符

    • 方法句柄和方法类型

    • 动态调用点和动态常量

全限定名:com/changyy/jvm/ClassTest就是类的全限定名,

字段方法的名称:Java代码中的sum和n、m就是方法、字段的简单名称

字段的描述符:

[图片上传失败...(image-19aefc-1604406038568)]

方法的描述符:

[图片上传失败...(image-f4a192-1604406038568)]

常量池的项目类型

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或者导出的包

通过上图接下来逐一解析常量池,共28项常量

[图片上传失败...(image-c7f9ca-1604406038568)]

读取下一个字节标志位tag用于区分常量类型 0x0a->10,在类型查找标志是10的,可知这个常量属于CONSTANT_Methodref_info类型,此类型的常量代表类中方法的符号引用,CONSTANT_Methodref_info的结构如下:

[图片上传失败...(image-f236ad-1604406038568)]

tag占一个字节、class_index占两个字节、name_and_type占两个字节,class_index:0x0005 -> 5 指向常量池的第五项常量,name_and_type_index:0x0017->23 指向常量池的第二十三项常量。

[图片上传失败...(image-e4cd15-1604406038568)]

使用JDK的一个工具 javap来输出ClassTest.class的字节码内容,可以用来验证解析字节码是否正确。

javap -v ClassTest.class

其中的常量池部分

Constant pool:
   #1 = Methodref          #5.#23         // java/lang/Object."":()V
   #2 = Fieldref           #4.#24         // com/changyy/jvm/ClassTest.n:I
   #3 = Fieldref           #4.#25         // com/changyy/jvm/ClassTest.m:I
   #4 = Class              #26            // com/changyy/jvm/ClassTest
   #5 = Class              #27            // java/lang/Object
   #6 = Class              #28            // com/changyy/jvm/IClassTest
   #7 = Utf8               n
   #8 = Utf8               I
   #9 = Utf8               m
  #10 = Utf8               
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/changyy/jvm/ClassTest;
  #17 = Utf8               sum
  #18 = Utf8               (I)I
  #19 = Utf8               p
  #20 = Utf8               
  #21 = Utf8               SourceFile
  #22 = Utf8               ClassTest.java
  #23 = NameAndType        #10:#11        // "":()V
  #24 = NameAndType        #7:#8          // n:I
  #25 = NameAndType        #9:#8          // m:I
  #26 = Utf8               com/changyy/jvm/ClassTest
  #27 = Utf8               java/lang/Object
  #28 = Utf8               com/changyy/jvm/IClassTest

从这里可以看出第一项常量确实是CONSTANT_Methodref_info,索引值也都对应。

接着解析,下1个字节是tag: 0x09 -> 9 类型是CONSTANT_Fieldref_info的常量,表示字段的符号引用,CONSTANT_Fieldref_info的结构如下:

[图片上传失败...(image-d918c-1604406038568)]

tag占一个字节、class_index占两个字节、name_and_type占两个字节,class_index:0x0004 -> 4 指向常量池的第四项常量,name_and_type_index:0x0018->24 指向常量池的第二十四项常量,可以通过javap出的常量池进行验证。

[图片上传失败...(image-70ea85-1604406038568)]

继续1个字节是 tag:0x09 -> 9 还是CONSTANT_Fieldref_info类型的常量class_index:0x0004 -> 4 指向常量池的第四项常量,name_and_type_index:0x0019->25 指向常量池的第二十五项常量,

[图片上传失败...(image-d35e19-1604406038568)]

继续1个字节是tag:0x07 -> 7 类型是CONSTANT_Class_info的常量,表示类或接口的符号引用。结构如下:

[图片上传失败...(image-bd3c68-1604406038568)]

共占3个字节,tag:0x07,name_index:0x001a -> 26 指向常量池中第二十六项常量

[图片上传失败...(image-96a41f-1604406038568)]

继续一个字节 tag:0x07 -> 7还是CONSTANT_Class_info类型常量,name_index:0x001b -> 27 指向常量池中第二十七项常量

[图片上传失败...(image-7fa154-1604406038568)]

继续一个字节 tag:0x07 -> 7还是CONSTANT_Class_info类型常量,name_index:0x001c -> 28 指向常量池中第二十八项常量

[图片上传失败...(image-510d8b-1604406038568)]

继续一个字节 tag:0x01 -> 1 类型是CONSTANT_Utf8_info的常量,表示UTF-8编码的字符串,结构如下:

[图片上传失败...(image-80fd5d-1604406038568)]

共占4个字节,length:0x0001 -> 1 bytes:6e转成utf-8是 n

[图片上传失败...(image-5d21c9-1604406038568)]
由于常量池项太多,不一一解析了,可以参考文末的思维导图一步步解析


image

访问标志

access_flags用两个字节来表示,其标识了类或者接口的访问信息,结构如下:

image

0x0001|0x0020=0x0021 是一个public类型的类

[图片上传失败...(image-d1c06d-1604406038568)]

类索引、父类索引

类索引(this_class)和父类索引(super_class)都是两个字节的数据

0x0004 -> 4 指向常量池中类型为CONSTANT_Class_info的类描述符 第四项常量

0x0005 -> 5 指向常量池中类型为CONSTANT_Class_info的类描述符 第五项常量

[图片上传失败...(image-e2f9b3-1604406038568)]

接口索引集合

接口计数器:两个字节,0x0001 - > 1 表示实现了一个接口

[图片上传失败...(image-2aba3c-1604406038568)]

接口集合:接口数量是1个,那就读取两个字节,指向常量池中类型为CONSTANT_CLass_info的接口描述符。 0x0006 -> 6 常量池中第6项常量

[图片上传失败...(image-8c2938-1604406038568)]

字段表集合

首先是fields_count字段数量,占两个字节,0x0002 -> 2 表示有两个字段
[图片上传失败...(image-85d82c-1604406038568)]
字段表结构如下

[图片上传失败...(image-3f41b8-1604406038568)]

image

字段修饰符access_flag 0x0002 -> 2,表示字段的访问标志是private

image

name_index:0x0007 -> 7 指向常量池类型为CONSTANT_Utf8_info的字段简单名称,常量池中第7项 也就是变量 n

descriptor_index:0x0008 -> 指向常量池类型为CONSTANT_Utf8_info的字段描述符,常量池中第8项 也就是变量 I

[图片上传失败...(image-a680fa-1604406038568)]

attributes_count:0x0000 -> 0 表示没有属性

接下来第二个字段 access_flag:0x000a == 0x0002 | 0x0008 访问修饰符是private + static

[图片上传失败...(image-3310d4-1604406038568)]

name_index:0x0009 -> 9 指向常量池类型为CONSTANT_Utf8_info的字段简单名称,常量池中第9项 也就是变量 m

descriptor_index:0x0008 -> 指向常量池类型为CONSTANT_Utf8_info的字段描述符,常量池中第8项 也就是变量 I

attributes_count:0x0000 -> 0 表示没有属性

方法表集合

首先是两个字节表示方法数量 0x0003 -> 3 表示3个方法

[图片上传失败...(image-e4cde6-1604406038568)]

方法表methods结构如下:

[图片上传失败...(image-8833c6-1604406038568)]

第一个方法 [图片上传失败...(image-5dcb2-1604406038568)]

access_flags:0x0001 表示方法的访问修饰符是public

image

name_index:0x000a ->10 指向常量池类型为CONSTANT_Utf8_info的方法的简单名称,常量池中第10项常量

descriptor_index:0x000b -> 11 指向常量池类型为CONSTANT_Utf8_info的方法的描述符,常量池中第11项常量 ()V,方法描述符结构如下:

[图片上传失败...(image-b11fea-1604406038568)]

attributes_count 0x0001 -> 1 有一个属性

attributes属性集合,attribute_name_index:0x000c -> 12 指向常量池中第12项常量,Code属性

[图片上传失败...(image-78b264-1604406038568)]

Code属性结构如下:

image

attribute_length:4个字节 0x00000039 -> 57 ,由于属性名称索引和属性长度一共6个字节,所以属性值的长度是整个属性表的长度减去6个字节

max_statck:0x0002 ->2 表示操作数栈深度最大值是2

max_locals:0x0001 ->1 表示局部变量表所需1个Slot

code_length:0x0000000b -> 11 表示字节码的长度是11

[图片上传失败...(image-7e4695-1604406038568)]
接下来是code 读取11个字节

image

字节码指令可以去oracle官网 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html 查找,0x2a表示aload_0,这部分可以通过javap反编译查看

  public com.changyy.jvm.ClassTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: bipush        10
         7: putfield      #2                  // Field n:I
        10: return
      LineNumberTable:
        line 8: 0
        line 10: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/changyy/jvm/ClassTest;

接下来是exception_table_length 异常表长度 0x0000 -> 0

[图片上传失败...(image-f64cbb-1604406038568)]
attributes_count:0x0002 -> 2 2个属性
[图片上传失败...(image-632053-1604406038568)]

第一个属性attribute_name_index:0x000d ->13 指向常量池中第13项常量 LineNumberTable

image

LineNumberTable属性:描述Java源代码与字节码行号(字节码的偏移量)之间的对应关系,结构如下:

[图片上传失败...(image-cf9789-1604406038568)]

attribute_length:0x0000000a -> 10

line_number_table_length:0x0002 -> 2 长度为2

[图片上传失败...(image-119b9b-1604406038568)]
第一个line_number_info, start_pc 0x0000 -> 0 ,line_number 0x0008 -> 8 字节码第0行对应Java源代码第8行

第二个line_number_info, start_pc 0x0004 ->4,line_number 0x000a -> 10 字节码第4行对应Java源代码第10行

[图片上传失败...(image-2c17a9-1604406038568)]

第一个属性attribute_name_index:0x000e ->14 指向常量池中第14项常量 LocalVariableTable

[图片上传失败...(image-549f52-1604406038568)]

LocalVariableTable属性:用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,结构如下:

image

[图片上传失败...(image-b73e4f-1604406038568)]

attribute_lenght:0x000000c -> 12 覆盖长度12,

local_variable_table_length : 0x0001 -> 1 表示一个local_variable_table

[图片上传失败...(image-959926-1604406038568)]

start_pc: 0x0000 ->0 ; length: 0x000b -> 11; name_index: 0x000f ->15;descriptor_index: 0x0010->16;index: 0x0000 ->0;解释如上图。

后面二个方法就不再解析了,可以通过后面思维导图自行解析

image

关注微信公众号“程序员二胖” 回复 002 获取Class文件结构思维导图原图。

image

你可能感兴趣的:(深入理解Class文件结构)