深入浅出JVM之Class文件结构

一、语言无关性

jvm并不是只支持java语言,其他的语言如groovy,scala,ruby等,只要能够生产class文件,都可以在jvm上运行。最新版本的jvm上甚至还增加了一些新的指令,这些指令在java语言中无法使用,而在动态语言如groovy中才能使用。所以class文件才是jvm运行的基石。

二、文件结构

用二进制文件打开工具Notepad++或Winhex打开一个class文件,可以看到class文件的头两行的16进制内容如下:

1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16
CA FE BA BE 00 00 00 33 00 20 07 00 02 01 00 07
54 65 73 74 49 6E 74 07 00 04 01 00 10 6A 61 76
61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 03
73 69 64 01 00 01 49 01 00 0D 43 6F 6E 73 74 61
6E 74 56 61 6C 75 65 03 00 00 00 63 01 00 03 75

该class文件对应的java代码如下:

public class TestInt {
	public static final int sid=99;
	private static final int uid=199;
	public  String sname="this is a test";
	public static void main(String[] args) {
	}
}
2.1 class文件头信息

magic u4:先看头4个字节-0xCAFEBABE,这个是class文件的标识,以这4个字节的二进制内容来标识该文件是不是class文件

其后的四个字节分别是minor_version和major_version,上面的例子中这四个字节的16进制内容为:0x00000033,按两个字节换算成十进制就是0和51,也就是minor_version=0,major_version=51,jdk1.7编译出来的class文件默认的major_version就是51.

2.2 常量池:

常量池是class文件中最为重要的部分,因为其他的部分如方法、字段都会引用常量池的内容。

constant_pool_count u2,指定常量的个数,由于常量池中的计数是从1开始,没有第0个常量,但是在计算总数的时候加上了0序号,所有constant_pool_count比实际的常量池数量多1个。在上面的例子中,第1行第9,10两个字节的内容0x0020表示的常量池的个数,转换为10进制也就是有32,实际应该有31个常量。

通过javap -v TestInt.class命令可以查看到类中的常量信息,可以看到常量池中确实有31个常量。

Classfile /D:/workspace/eclipse/JavaDemo/bin/TestInt.class
  Last modified 2019-12-3; size 519 bytes
  MD5 checksum d304064d5e896b194468c0f4e1ae7535
  Compiled from "TestInt.java"
public class TestInt
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // TestInt
   #2 = Utf8               TestInt
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               sid
   #6 = Utf8               I
   #7 = Utf8               ConstantValue
   #8 = Integer            99
   #9 = Utf8               uid
  #10 = Integer            199
  #11 = Utf8               sname
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Methodref          #3.#17         // java/lang/Object."<init>":()V
  #17 = NameAndType        #13:#14        // "<init>":()V
  #18 = String             #19            // this is a test
  #19 = Utf8               this is a test
  #20 = Fieldref           #1.#21         // TestInt.sname:Ljava/lang/String;
  #21 = NameAndType        #11:#12        // sname:Ljava/lang/String;
  #22 = Utf8               LineNumberTable
  #23 = Utf8               LocalVariableTable
  #24 = Utf8               this
  #25 = Utf8               LTestInt;
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               SourceFile
  #31 = Utf8               TestInt.java
  ....

constant_pool cp_info:再接下来就是常量池的实际内容。在本例中是从第1行第11个字节开始。从该字节开始就是每个常量池的内容,比如第11个字节的内容是0x07,他表示的是该常量的类型是 CONSTANT_Class_info,通过查询常量池的数据接口可以得知该类型的数据结构如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;              //CONSTANT_Utf8_info
}

该数据结构中u1是指占用1个字节,u2代表占用2个字节。所以在后面的两个字节,也就是第1行的第12,13个字节表示的是name_index,也就是类名实际内容在常量池中的索引。本例中类名是第2个常量。

第1行第14个字节开始是第2个常量,该常量的tag是0x01,查常量表,它是一个 CONSTANT_Utf8,是一个utf8类型的常量,该常量的数据结构如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;                  //直接数
    u1 bytes[length];           //改进版的UTF8
}

其中tag=0x01,length=0x0007,紧接着后面是该常量的内容数据,在该例中从第2行开始,一共有7个字节:0x54657374496E74,转换成ASCII码就是TestInt。

接下来是第3个常量,从第2行第8个字节开始,tag是0x07,该类型和第1个常量是一样的,所以该字节后面的两个字节的内容也是表示该常量值的索引,在本例中是0x0004,也就是第4个常量。

然后是第4个常量,从第2行第11个字节开始,tag是0x01,这也是一个CONSTANT_Utf8,所有后面的两个字节是该变量的长度:0x0010,该常量值的内容是16个字节

从第3行第14个字节开始分别是第5、第6,第7个常量,这几个常量都是CONSTANT_Utf8

从第5行第8个字节开始是第8个变量,该变量的tag是0x03,它的类型是CONSTANT_Integer_info,该类型的数据结构如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;                   //直接数
}

所以在tag后面直接跟着该变量的值,在本例中是0x00000063,也就是代码中定义的99。

对于String类型的变量,如果前面有final关键字,则该String变量的值会直接跟在String的变量名后面,如果没有final关键字,则该String变量的值会放在Fieldref中,根据Fieldref中的值来找到对应的变量名称。另外,在定义String变量的时候,如果使用+合并不同的字符串内容,编译的时候是直接合并成一个String字符串,不会生成多个String对象。

其他的常量都是依次类推,根据常量类型的数据结构来判断其所占的字节数和内容。

2.3 类信息

常量池结束后是类的访问标识:access flag u2,占两个字节。它是由不同的标识通过或运算符计算出来了,本例中该内容是0x0021,它是由public:0x0001和super:0x0020通过或运算计算得来的:0x0001|0x0020=0x0021

访问标识后的内容如下:

this_class u2:指向当前类,值为常量池中的索引

super_class u2:指向父类,值为常量池中的索引

interface_count u2:指向接口数量

interfacec:如果接口数量不为0,则后面会跟每个接口在常量池中的索引。

2.4 类字段信息

field_count:字段的数量,也就是该类中直接定义的变量数

fields:后面紧跟着field_count个field_info,field_info描述了该字段的访问标识和其在常量池中的索引以及一些其他的属性

field_info:

–access_flags u2

–name_index u2

–descriptor_index u2

–attributes_count u2

–attribute_info attributes[attributes_count]

字段的access_flags:

与类的访问标识是类似的,通过两个字节一共16位2进制数来标识访问的类型。本例中的一个字段的访问标识是0x0019,该变量的访问标识是public static final,对应的标识是0x0001、0x0008和0x0010,通过或计算:0x0001|0x0008|0x0010=0x0019.

name_index:指向常量池中的变量名的索引

descriptor_index:标识字段的类型,指向常量池中的索引

attribute_info:指向字段的属性信息,其结构是2个字节的属性索引,4个字节的属性长度,然后其后是属性长度个字节的内容是属性值的常量池索引。本例中0x0001表示有1个属性,0x0007是该属性在常量池的索引,对应的值是ConstantValue,0x00000002表示属性值的长度为2,紧接着后面的2个字节0x0008是属性值的索引,对应常量池中的第8个常量,这里是99。

2.5 类方法信息

接下来是方法的内容:

首先是方法数量,刚开始2个字节是方法的数量,这里是0x0002,表示有两个方法。

后面是具体的方法信息-method_info,其表结构如下:

–access_flags u2:占用2个字节,内容与字段的访问标识类似,不过有一些方法独有的访问标识,比如ACC_VARARGS-可变参数。

–name_index u2:占用2个字节,值对应的是常量池中的常量

–descriptor_index u2:占2个字节,值对应的是常量池中的常量,方法描述符包含方法的返回值以及参数。

–attributes_count u2:占2个字节,表示有多少个属性信息

–attribute_info attributes[attributes_count]

method的attributes比较复杂,下面是几种比较常用的method的attribute:

名称使用者描述
Deprecatedfield method声明字段、方法、类被废弃
ConstantValuefieldfinal常量
Codemethod方法的字节码和其他数据
Exceptionsmethod方法的异常,这里的异常是方法通过throw抛出的异常
LineNumberTableCode_Attribute方法行号和字节码映射,它是Code属性的子属性
LocalVaribleTableCode_Attribute方法局部变量表描述,它是Code属性的子属性
SourceFileClass file源文件名
Syntheticfield method编译器产生的方法或字段,这个是编译器产生的,源码中看不到

上面每一项都有自己的数据结构,这时如果还是通过二进制文件来查看会比较费劲,这时就需要借助工具能够更好的查看class的文件结构,这里推荐工具:jclasslib,这是一个开源软件,可以方便的查看到class的文件结构。

method的attribute的数据结构参考附录,查看的方式和其他的属性是一样的。一般class文件的最后一个属性是SourceFile。

class的文件结构是程序在jvm中运行的基础,将来可能会出现不同的语言,但是他都是要生成同样的class文件,然后才能够正常在jvm上运行,所以了解calss的文件结构对于掌握jvm上程序的运行很有帮助。

附:
附1.常量池数据结构类型
type数值16进制Class版本JavaSE版本
CONSTANT_Class70x0745.31.0.2
CONSTANT_Fieldref90x0945.31.0.2
CONSTANT_Methodref100x0a45.31.0.2
CONSTANT_InterfaceMethodref110x0b45.31.0.2
CONSTANT_String80x0845.31.0.2
CONSTANT_Integer30x0345.31.0.2
CONSTANT_Float40x0445.31.0.2
CONSTANT_Long50x0545.31.0.2
CONSTANT_Double60x0645.31.0.2
CONSTANT_NameAndType120x0c45.31.0.2
CONSTANT_Utf810x0145.31.0.2
CONSTANT_MethodHandle150x0f51.07
CONSTANT_MethodType160x1051.07
CONSTANT_InvokeDynamic180x1251.07
CONSTANT_Module190x1353.09
CONSTANT_Package200x1453.09

数据结构

  • CONSTANT_Class_info
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;              //CONSTANT_Utf8_info
}
  • CONSTANT_Fieldref_info
CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;             //CONSTANT_Class_info
    u2 name_and_type_index;     //CONSTANT_NameAndType_info
}
  • CONSTANT_Methodref_info
CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;             //CONSTANT_Class_info
    u2 name_and_type_index;     //CONSTANT_NameAndType_info
}
  • CONSTANT_InterfaceMethodref_info
CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;             //CONSTANT_Class_info
    u2 name_and_type_index;     //CONSTANT_NameAndType_info
}
  • CONSTANT_String_info
CONSTANT_String_info {
    u1 tag;
    u2 string_index;            //CONSTANT_Utf8_info
}
  • CONSTANT_Integer_info
CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;                   //直接数
}
  • CONSTANT_Float_info
CONSTANT_Float_info {
    u1 tag;
    u4 bytes;                   //直接数
}
  • CONSTANT_Long_info
CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;              //直接数
    u4 low_bytes;               //直接数
}
  • CONSTANT_Double_info
CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;              //直接数
    u4 low_bytes;               //直接数
}
  • CONSTANT_NameAndType_info
CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;              //CONSTANT_Utf8_info
    u2 descriptor_index;        //CONSTANT_Utf8_info
}
  • CONSTANT_Utf8_info
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;                  //直接数
    u1 bytes[length];           //改进版的UTF8
}
附2.类的访问标识:
标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可以设置,接口不能设置该标志
ACC_SUPER0x0020是否允许使用invokespecial字节码指令(查了一下该命令的作用为"调用超类的构造方法,实例的构造方法,私有方法"),JDK1.2以后的编译器编译出来的class文件该标志都为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否被声明为abstract类型,对于接口和抽象类来说此标志为真,其他类为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码生成
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
附3.字段的访问标识:
Flag NameValueInterpretation
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static.
ACC_FINAL0x0010final
ACC_VOLATILE0x0040volatile
ACC_TRANSIENT0x0080transient
ACC_SYNTHETIC0x1000synthetic; 没有源码,编译器生成
ACC_ENUM0x4000枚举类型
附4.attribute_info数据结构
Attribute_info {
   attribute_name_index u2
   attribute_length u4
   info[attribute_length] u1
}
附5.方法的access_flag:
Flag NameValueInterpretation
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_SYNCHRONIZED0x0020synchronized
ACC_BRIDGE0x0040编译器产生 桥接方法
ACC_VARARGS0x0080可变参数
ACC_NATIVE0x0100native
ACC_ABSTRACT0x0400abstract
ACC_STRICT0x0800strictfp
ACC_SYNTHETIC0x1000不在源码中,由编译器产生
附6.方法的attribute:
Deprecated {
   attribute_name_index u2
   attribute_length u4
}
ConstantValue {
   attribute_name_index u2
   attribute_length u4      //默认为2
   constantvalue_index u2
}
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;  //异常表,这里的异常是try-catch里的异常
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;         //属性数量,这里的属性是LineNumberTable和LocalVariableTable
    attribute_info attributes[attributes_count];
}
LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;         //这里指的是code中的偏移量,对应的是具体要执行的字节码
        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;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}
Exceptions {       //这里的Exception是方法上throws的异常,可以有多个异常
    attribute_name_index u2 
    attribute_length u4 
    number_of_exceptions u2 
    exception_index_table[number_of_exceptions] u2 
}
SourceFile {
   attribute_name_index u2
   attribute_length u4   //固定为2
   soucefile_index u2
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值