class文件结构解析

从学习Java语言的第一天起,我们就被告知与其他语言相比,Java的一大特点在于它的平台无关性,即Write Once, Run Everywhere. 而构成平台无关性的基石就在于所有JVM都采用了字节码作为它们的程序存储格式,因此今天主要就分析一下class文件的结构。

知识准备

在详细分析class文件结构之前,我们需要了解一些基本概念:

  • class文件以8字节为基本单位来进行存储,中间没有任何分隔符;
  • 当数据项需要占用的空间大于8字节时,会按照高位在前的方式来进行分割;
  • class文件只有两种数据类型:无符号数、表;
  • 无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数;
  • 表是由多个无符号数或者其它表作为数据项构成的符合数据类型,表名习惯性都以 _info 结尾。

因此本质上整个class文件就是一张表,它由以下数据项构成:

类型 名称 数量 描述
u4 magic 1 魔数
u2 minor_version 1 次版本号
u2 major_version 1 主版本号
u2 constant_pool_count 1 常量个数
cp_info constant_pool constant_pool_count - 1 具体常量
u2 access_flags 1 访问标志
u2 this_class 1 类索引
u2 super_class 1 父类索引
u2 interfaces_count 1 接口索引
u2 interfaces interfaces_count 具体接口
u2 fields_count 1 字段个数
field_info fields fields_count 具体字段
u2 methods_count 1 方法个数
method_info methods methods_count 具体方法
u2 attributes_count 1 属性个数
attribute_info attributes attributes_count 具体属性

可以看到,这16种数据项大致可以分为3类:

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

接下来我们就逐一来看看这些数据项的含义。整个分析过程我们将以下面这段代码对应的class文件为基础:

public class JavaTest {

    private static String name = "JVM";

    public static void main(String[] args) {
        System.out.println("Hello " + name);
    }
}
图1 JavaTest class文件结构.png

1.魔数与版本

每个class文件的头4个字节称为魔数,用于确定这个文件是否能被虚拟机所接受。class文件的魔数值为CAFEBABE。

第5、6字节为次版本号,7、8字节为主版本号。Java的主版本号从45开始,JDK1.1之后每个大版本发布,主版本号加1。高版本的jdk能前向兼容之前版本的class文件,但不能运行以后版本的class文件。

从图1可以看到,次版本号为0000,主版本号为0031,这说明该class文件可以被1.5及以后版本的jdk运行。

2.常量池

紧接着主版本号之后的是常量池入口,由于常量池中常量数量不固定,因此入口使用第一个u2类型的数据代表常量池计数值,该计数器从1开始。图1中常量池计数值为0034,代表常量池中一共有51个常量。

常量池中每一个常量都是一个表,jdk1.7之后一共有14种类型的常量,他们对应着14个不同结构的表,但这14个表都有一个共同特点:那就是表开始的第一位是一个u1类型的标志位,代表当前常量属于哪种常量类型。其取值和含义如下表所示:

类型 标志 描述
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_InterfaceMehtodref_info 11 接口方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 方法句柄
CONSTANT_MethodType_info 16 方法类型
CONSTANT_InvokeDynamic_info 18 动态方法调用点

这14种常量的结构如下表所示:

常量池变量结构

有了这些基础,我们继续分析前面提到的class文件:

image.png

第一个u1类型的变量代表常量类型为0A,对应的表为CONSTANT_Methodref_info,表示方法引用,紧接着一个u2类型的变量000C,它表示声明该方法的类描述符为常量池汇中的第12个常量,第二个u2类型的变量001D表示指向该方法名称及类型的描述符为常量池中的第29个常量。
按照同样的方式,下图给出了前面14个常量的字节码,其中前面12个都是指向了常量池中的其它常量,第13、14个常量是两个类型为1(即UTF-8编码的常量),对应的英文字符分别为name、Ljava/lang/String.

前14个常量划分.png
第13个常量.png
第14个常量.png

剩下其他常量的划分方式是类似的,事实上,jdk已经为我们提供了专门用于分析class文件的工具javap,利用javap -v JavaTest.class得到常量池中的52个常量如下,可以看到,前面14个常量的划分与我们之前分析的完全一致。

bogon:Downloads shiyangsheng$ javap -v JavaTest.class
Classfile /Users/shiyangsheng/Downloads/JavaTest.class
  Last modified 2018-3-17; size 842 bytes
  MD5 checksum fbb2370c6b7413a0636806a0e492224a
  Compiled from "JavaTest.java"
public class com.youzan.shys.advice.JavaTest
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#29        // java/lang/Object."<init>":()V
   #2 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #32            // java/lang/StringBuilder
   #4 = Methodref          #3.#29         // java/lang/StringBuilder."<init>":()V
   #5 = String             #33            // Hello
   #6 = Methodref          #3.#34         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Fieldref           #11.#35        // com/youzan/shys/advice/JavaTest.name:Ljava/lang/String;
   #8 = Methodref          #3.#36         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = String             #39            // JVM
  #11 = Class              #40            // com/youzan/shys/advice/JavaTest
  #12 = Class              #41            // java/lang/Object
  #13 = Utf8               name
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/youzan/shys/advice/JavaTest;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               JavaTest.java
  #29 = NameAndType        #15:#16        // "<init>":()V
  #30 = Class              #42            // java/lang/System
  #31 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #32 = Utf8               java/lang/StringBuilder
  #33 = Utf8               Hello
  #34 = NameAndType        #45:#46        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #35 = NameAndType        #13:#14        // name:Ljava/lang/String;
  #36 = NameAndType        #47:#48        // toString:()Ljava/lang/String;
  #37 = Class              #49            // java/io/PrintStream
  #38 = NameAndType        #50:#51        // println:(Ljava/lang/String;)V
  #39 = Utf8               JVM
  #40 = Utf8               com/youzan/shys/advice/JavaTest
  #41 = Utf8               java/lang/Object
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               append
  #46 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #47 = Utf8               toString
  #48 = Utf8               ()Ljava/lang/String;
  #49 = Utf8               java/io/PrintStream
  #50 = Utf8               println
  #51 = Utf8               (Ljava/lang/String;)V

由此可见,常量池在class文件中占据了绝大部分内容(中间用红框框出来的就是常量池内容):

常量池内容.png

3.访问标志

紧接着常量池之后的两个字节表示访问标志,主要是用来标记类或者接口层次的一些属性。目标之定义了16个标志位中的8位,没有使用到的一律为0。 具体标志位如下表:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否为final类型
ACC_SUPER 0x0020 是否允许使用invokespcial字节码指令的新语义,jdk1.0.2之后编译出来的类,此标志都为真
ACC_INTERFACE 0x0200 是否为接口
ACC_ABSTRACT 0x0400 是否为abstract类型(对接口和抽象类来说,此标志都为真)
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 是否是注解
ACC_ENUM 0x4000 是否是枚举

显然,对JavaTest类而言,只有ACC_PUBLIC、ACC_SUPER两个标志应该为真,因此access_flags=0x0021.

4.类索引、父类索引和接口索引集合

在访问标志之后,有3个用来确定一个类的继承关系的数据,按先后顺序分别是:

  • 类索引:用于确定类的全限定名
  • 父类索引:用于确定父类的全限定名
  • 接口索引:用于描述类实现了哪些接口
    它们在class文件中的位置如下:
image.png

可见,类索引为11,父类索引为12,接口索引集合大小为0,根据前面得到的常量池,可以知道第11、12个常量为:

...
#11 = Class              #40            // com/youzan/shys/advice/JavaTest
#12 = Class              #41            // java/lang/Object
...
#40 = Utf8               com/youzan/shys/advice/JavaTest
#41 = Utf8               java/lang/Object
...

5.字段表集合

在接口索引之后是字段表集合,字段表用来描述接口或者类中声明的变量。它包括类级变量和实例级变量,但是不包括局部变量以及从父类和接口中继承而来的字段。字段表的格式如下:

类型 名称 数量 含义
u2 access_flags 1 字段修饰符
u2 name_index 1 字段和方法简单名称在常量池中的引用
u2 descriptor_index 1 字段和方法描述符在常量池中的引用
u2 attributes_count 1 描述字段额外信息属性的个数
attribute_info attributes attributes_count 具体描述字段的额外信息属性
5.1字段修饰符

字段修饰符与类中的访问标志很类似,用来描述字段的一些属性:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 是否为public类型
ACC_PRIVATE 0x0002 是否为private类型
ACC_PROTECTED 0x0004 是否为protected类型
ACC_STATIC 0x0008 是否为static类型
ACC_FINAL 0x0010 是否为final类型
ACC_VOLATILE 0x0040 是否volatile类型
ACC_TRANSIENT 0x0080 是否transient类型
ACC_SYNTHETIC 0x1000 是否由编译器自动产生
ACC_ENUM 0x4000 是否enum类型
5.2全限定名

把类全路径中的.替换为/,同时在最后加入一个;即可。

5.3简单名称

简单名称指的是没有类型和修饰符的字段或者方法名称。

5.4描述符

描述符用来描述字段的数据类型、方法的参数列表和返回值。其中基本类型字段的描述符用一个大写字母来表示,而对象类型则用字符L加上对象类型的全限定名来表示。具体如下表:

描述符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 基本类型void
L 对象类型,如Ljava/lang/Object

对于数组类型,每一个维度都是用一个前置的“[”来描述,如java.lang.String[][]类型的二位数组将被记录为[[java/lang/String;

描述方法时,将按照先参数列表、后返回值的顺序来描述。其中参数列表严格按照参数的顺序放在一组小括号()之内。例如方法java.lang.String.toString()的描述符为()Ljava/lang/String;

了解了这几个概念之后,我们回到JavaTest的class文件:

image.png
  • fields_count=0x0001表明这个类只有一个字段表数据;
  • access_flags=0x000A表明ACC_PRIVATE与ACC_STATIC标志位为1真,其它标志位为0;
  • name_index=0x000D表明字段简单名称为常量池中的第13个常量,也就是name;
  • descriptor=0x000E表明字段描述符为常量池中的第14个常量,也就是Ljava/lang/String;
  • attributes_count=0x0000表明字段额外属性个数为0;
    由此可以反过来得到该类的一个属性为 private static String name;
6.方法表集合

对方法描述的方式与对字段描述的方式基本一致,方法表的结构也与字段表的结构完全一致,不同之处在于方法的访问标志与字段的访问标志有所区别。例如volatile与transient不能修饰方法,但是方法却有synchronized、native、strictfp和abstract等属性。其具体访问标志如下:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 是否为public类型
ACC_PRIVATE 0x0002 是否为final类型
ACC_PROTECTED 0x0004 是否为protected类型
ACC_STATIC 0x0008 是否为static类型
ACC_FINAL 0x0010 是否为final类型
ACC_SYNCHRONIZED 0x0020 是否synchronized类型
ACC_BRIDGE 0x0040 是否桥接方法
ACC_VARARGS 0x0080 是否接收不定参数
ACC_NATIVE 0x0100 是否native方法
ACC_ABSTRACT 0x0400 是否abstract
ACC_STRICTFP 0x0800 是否strictfp
ACC_SYNTHETIC 0x1000 是否由编译器自动产生

让我们继续回到class文件:

6.1 第一个方法
第一个方法表结构.png
  • methods_count=0x0003表明该类有3个方法;
  • 第一个方法的access_flags=0x0001表明只有ACC_PUBLIC标志位为真;
  • name_index=0x000F表明方法简单名称为常量池中的第15个常量,也就是<init>;
  • descriptor=0x0010表明方法修饰符为常量池中的第16个常量,也就是()V;
  • attributes_count=0x0001表明第一个方法有一个额外属性,且索引值就是其后的0x0011,也就是常量池中的第17个常量Code。
  • Code属性是该方法的具体字节码描述。

由此得到第一个方法为public void init(),这个方法是编译器自动添加的实例构造器方法。

Code属性也是class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(方法体里的代码)和元数据(描述类、字段、方法的其他信息)两部分,那么Code属性描述的就是代码的信息,其它所有数据都用于描述元数据。由于Code数据极其重要也相对复杂,我将在另外一篇文章中单独介绍,这里直接给出init()方法的Code属性在class文件中的表示(简单来说,前面4个字节0000 002F表示属性值的长度,也就是47个字节,也就是说后续47个字节都是Code属性的内容):

init方法Code属性
6.2 第二个方法

第一个方法的Code属性后面紧跟着的是第二个方法的描述,同样的分析方法,不难得出第二个方法为public static void main(String[]);其Code属性值的长度为0000 004A,也就是74个字节。

第二个方法表结构.png
main方法Code属性.png
6.3 第三个方法

同样,很容易得到第三个方法为static clinit void();这个方法是编译器自动添加的类构造器方法,其Code属性值的长度为0000 001E,也就是30个字节。

第三个方法表结构.png
clinit方法Code属性.png

实际上,这个分析与javap得到的结果也是一致的。

{
  public com.youzan.shys.advice.JavaTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/youzan/shys/advice/JavaTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String Hello
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: getstatic     #7                  // Field name:Ljava/lang/String;
        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
      LineNumberTable:
        line 14: 0
        line 15: 27
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      28     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #10                 // String JVM
         2: putstatic     #7                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 11: 0
}

7.属性表集合

属性表集合用于描述某些场景的专有信息,它一共有21个属性,属性表结构如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 info attribute_length

由于涉及属性太多,这里也不再展开,只是简单说明下之类的属性代表什么意思。从class文件可以看出,该类的属性表集合只有一个元素,001B表示常量池中的第27个常量,也就是SourceFile,001C表示常量池中的第28个常量,也就是JavaTest.java,也就是说,SourceFile属性记录了生成这个class文件的源码文件的名称。

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

推荐阅读更多精彩内容

  • Java代码必须要被编译成class文件后,虚拟机才能够加载运行,要搞清楚Java的类加载机制,首先必须要理解Cl...
    云飞扬1阅读 8,897评论 2 61
  • 字节码查看工具:WinHex 前言 Java虚拟机实现语言无关性的基石就是Class文件Java虚拟机提供的语言无...
    zlcook阅读 7,043评论 4 18
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 多少年來,你一直努力取悅別人、取悅你想要取悅的人、取悅這個世界,又要多少年後,你才懂得取悅自己?
    心交朋友钱交丶狗阅读 49评论 0 1
  • 也许这个世界,真的什么也不缺,但是唯独缺人!
    抹茶酱木子兮阅读 137评论 0 0