前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【JVM进阶之路】十一:Class文件结构

【JVM进阶之路】十一:Class文件结构

作者头像
三分恶
发布2021-04-23 11:19:44
发布2021-04-23 11:19:44
32200
代码可运行
举报
文章被收录于专栏:三分恶的专栏三分恶的专栏
运行总次数:0
代码可运行

Java虚拟机和Class文件是Java实现系统无关性的基石。

Class文件是JVM实现语言无关性的基石。

Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。

每一个 Class 文件对应于一个如下所示的 ClassFile 结构体:

代码语言:javascript
代码运行次数:0
复制
ClassFile {
 u4 magic;
 u2 minor_version;
 u2 major_version;
 u2 constant_pool_count;
 cp_info constant_pool[constant_pool_count-1];
 u2 access_flags;
 u2 this_class;
 u2 super_class;
 u2 interfaces_count;
 u2 interfaces[interfaces_count];
 u2 fields_count;
 field_info fields[fields_count];
 u2 methods_count;
 method_info methods[methods_count];
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

简单看一下各项的含义:

由于 Class 文件结构没有任何分隔符,所以无论是每个数据项的的顺序还是数量,都是严格限定的,哪个字节代表什么含义,长度多少,先后顺序如何,都是不允许改变的。

接下来我们来具体学习每项的含义。

1、魔数

这是基本上每个Java开发人员的第一个Java程序:

代码语言:javascript
代码运行次数:0
复制
public class HelloWorld {
    public static void main(String[] args) {

        System.out.println("Hello World");
    }
}

我使用的是Idea工具,运行,target目录下会生成对应的class文件,为了查看文件的十六进制信息,我们可以安装一个插件HexView

安装完成之后,选择class文件,右键 HexView,打开后的十六进制如下:

第一行中有一串特殊的字符 cafebabe,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 ClassFormatError

这段字节很有意思——咖啡宝贝,Java原来不止是咖啡,还是宝贝?

2、版本号

紧跟着魔数后面的四个字节 0000 0031 存储的是 class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。

Java的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加1(JDK1.0JDK1.1使用了45.045.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。

0000 0031 对应的十进制为49,是JDK8的内部版本号。

3、常量池

紧接着主、次版本号之后的是常量池入口。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的。

如图所示,常量池容量为十六进制数0x0022,即十进制的34,这就代表常量池中有33项常量,索引值范围为1~33。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。

常量池中主要存放两大类常量:字面量(Literal)符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

这17类常量结构只有一个相同之处,表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型。

17种常量类型所代表的具体含义如表所示:

类型

标志

描述

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_MethodType_info

16

表示方法类型

CONSTANT_Dynamic_info

17

表示一个动态计算常量

CONSTANT_InvokeDynamic_info

18

表示一个动态方法调用点

CONSTANT_Moudle_info

19

表示一个模块

CONSTANT_Package_info

20

表示一个模块中开放或者导出的包

常量池非常繁琐,17种常量类型各自有着完全独立的数据结构,彼此之间没有什么共性和联系。

我们直接看一下常量池中的17种数据类型的结构总表:

4、访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。

具体的标志位以及标志的含义如表:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

是否为 Public 类型

ACC_FINAL

0x0010

是否被声明为 final,只有类可以设置

ACC_SUPER

0x0020

是否允许使用 invokespecial 字节码指令的新语义

ACC_INTERFACE

0x0200

标志这是一个接口

ACC_ABSTRACT

0x0400

是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假

ACC_SYNTHETIC

0x1000

标志这个类并非由用户代码产生

ACC_ANNOTATION

0x2000

标志这是一个注解

ACC_ENUM

0x4000

标志这是一个枚举

access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一 律为零。

5、类索引、父类索引与接口索引集合

这三者为什么放在一起呢?因为这三者用来确定类的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(后的接口顺序从左到右排列在接口索引集合中。

6、字段表集合

接口索引结束后,接着是字段表(field_info),它用于描述接口或者类中声明的变量——这里的字段(Field)只包括类级变量以及实例级变量,不包括在方法内部声明的局部变量。

描述的主要信息包括:

  ①、字段的作用域(public,protected,private修饰)

  ②、是类级变量还是实例级变量(static修饰)

  ③、是否可变(final修饰)

  ④、并发可见性(volatile修饰,是否强制从主从读写)

  ⑤、是否可序列化(transient修饰)

  ⑥、字段数据类型(8种基本数据类型,对象,数组等引用类型)

  ⑦、字段名称

字段表的结构如下:

类型

名称

数量

u2

access_flags

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

access_flags是该字段的的访问标志,它和类中的访问标志很类似,用以描述该字段的权限类型:private、protected、public;并发可见性:volatile;可变性:final;

访问标志详情如下图所示:

由于Java语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

7、方法表集合

方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如表所示:

有区别的部分只有方法访问标志 access_flag, 因为volatile关键字和transient关键字不能修饰方法。

方法表标志位及其取值如下:

8、属性表集合

接下来终于到了最后一项:属性表集合。

前面提到的Class文件、字段表、方法表都可以携带自己的属性表集合,就是引用的这里。

属性表集合中的属性如下所示:

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。


参考:

【1】:《深入理解Java虚拟机:JVM高级特性与最佳实践》第三版

【2】:《Java虚拟机规范(Java_SE_7)》

【3】:jvms/se8

【4】:Java虚拟机详解(九)------类文件结构

【5】:一把小刀,直插 class 文件的小心脏

【6】:JVM系列(二)-字节码文件结构(基础篇)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-04-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、魔数
  • 2、版本号
  • 3、常量池
  • 4、访问标志
  • 5、类索引、父类索引与接口索引集合
  • 6、字段表集合
  • 7、方法表集合
  • 8、属性表集合
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档