HuKai's Blog HuKai's Blog
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)

HuKai

梦想成为全栈的保安
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)
  • Java基础

  • Java并发编程

  • JVM

    • 初识JVM
    • 类文件结构详解上篇
      • 无关性的基石
        • 平台无关性
        • 语言无关性
      • Class类文件结构
      • 魔数与Class文件的版本
      • 常量池constant_pool
        • 常量池的数量
        • 常量池的结构
        • 常量类型的结构
      • 访问标志access_flag
      • 类索引、父类索引与接口索引集合
      • 字段表集合
        • 字段修饰符access_flags
        • 简单名称name_index
        • 描述符descriptor_index
      • 方法表集合
    • 类文件结构详解下篇
    • 虚拟机类加载机制
    • JVM内存结构
    • JVM对象模型
    • GC基础
    • GC算法与收集器
    • GC日志详解
  • Java新特性

  • Spring5

  • SpringMVC

  • SpringBoot

  • MyBatis

  • Java
  • JVM
HuKai
2022-02-13
目录

类文件结构详解上篇

# 无关性的基石

# 平台无关性

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式-字节码(ByteCode),是构成平台无关性的基石。

无论哪种平台的虚拟机都可以载入和执行间一种平台无关的字节码,从而实现“Write Once. Run Anywhere”。

# 语言无关性

或许大部分程序员都还认为Java虚拟机执行Java程序是一件理所当然和天经地义的事情。但在Java发展之初,设计者就曾经考虑过并实现了让其他语言运行在Java虚拟机之上的可能性,也刻意把Java的规范拆分成了Java语言规范及Java 虚拟机规范。

时至今日,商业机构和开源机构已经在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Clojure、Groovy、JRuby、Jython、 Scala等。

实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。虚拟机并不关注Class的来源是何种语言。

# Class类文件结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符

注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类成接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。

根据Java虚拟机规范的规定,Class文件格式采用一种类似;C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表:

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-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个描述代码属性的数据项;

Class的结构不像XML等描述语言,它没有任何分割符号。所以在上表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格限定的,哪个字节代表佧么含义,长度是多少,先后顺序如何,都不允许改变。

接下来我们就逐一来看看这些数据项的含义。学习过程中我们会用到下面三种工具:

  • javap。jdk自带的class文件反编译工具
  • Notepad++的HEX-Editor插件,以16进制的方式查看class文件
  • JBE(Java Bytecode Editor),一款非常好用的字节码编辑器。或者是IDEA的jclasslib Bytecode viewer插件

# 魔数与Class文件的版本

每一个class文件的头4个字节称为魔数,它唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

很多文件存储标准中都使用魔数来进行身份识别。譬如图片格式gif、jpeg等。使用魔数而不是拓展名来进行识别主要是基于安全方面的考虑,因为文件扩展名能够任意修改。

Class文件的魔数值为:OxCAFEBABE,如图:

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

使用javap解析class文件,也可以得到版本号信息:

0000转10进制=0 0034转10进制=52,我们发现javap的解析结果和十六进制文件是对应的。

下表列出了主流JDK版本编译器输出的默认和可支持的Class:

编译器版本 十六进制主版本号 十进制主版本号
JDK 1.1 00 2D 45
JDK 1.2 00 2E 46
JDK 1.3 00 2F 47
JDK 1.4 00 30 48
JDK 1.5 00 31 49
JDK 1.6 00 32 50
JDK 1.7 00 33 51
JDK 1.8 00 34 52

相关异常为:Unsupported major.minor version 51.0,文件的编译版本是JDK1.7,但是JRE版本小于1.7。

# 常量池constant_pool

紧接着版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据项,也是占用Class文件空间最大的数据项目之一。

# 常量池的数量

由于常量池中的常量数量不固定,所以在常量池的入口需要放置一项u2类型的数据,代表容量池容量计数值constant_pool_count。

上图中002f转十进制=47。但是对于常量池的数量需要明确一点,常量池的数量是constant_pool_count-1,为什么减1,是因为索引0表示class中的数据项不引用任何常量池中的常量。所以上图中常量池的数量为47-1=46。

# 常量池的结构

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

字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用則属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中每一项常量都是一个表cp_info,cp_info开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。

使用javap -v命令解析字节码内容可以查看常量池:

cp_info可以细分为多种类型,标志=tag,15、16、18的常量项是用来支持动态语言调用的(jdk1.7时才加入的)详细如下:

# 常量类型的结构

之所以说常量池是最烦琐的数据,是因为这14种常量类型各自均有自己的结构,下面列举了11种常用的常量类型的结构:

看下图class文件的常量池的第一项常量,它的标志位是0x07,十进制为7,查询上表的标志列发现这个常量属于CONSTANT_Class_info类型,此类型的常量代表一个类或者接口的符号引用。0x0002=2 代表指向#2常量项的索引。

蓝色部分为#2常量项,标志位为0x01代表这个常量属于CONSTANT_Utf8_info,是一个字符串。查询它的结构我们发现标志位的后两位是字符串占用的字节数0x0022=34,所以后34位为字符串内容:com/mimaxueyuan/jvm/aentity/Person。

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。

# 访问标志access_flag

在常量池结束之后,紧接着的两个字节代表访问标志(access _flags),这个标志用于标识一些类或者接口层次的访问信息。

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

标志名称 标志值 含 义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 JDK1.0.2以后这个标志都为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

accessflags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。你可以把accessflags看成一个长度为16的布尔类型数组:

举个栗子,我们现在有个枚举的修饰符为:public enum AccessFlagDemo3,使用javap查看字节码信息如下:

使用十六进制方式查看class文件,可以发现访问标志位为0x4031,转化为2进制为:0100 0000 0011 0001‬‬,结合上图发现ACC_PUBLIC,ACC_FINAL,ACC_SUPER,ACC_ENUM标志位均为真。为什么没有声明为final却出现了呢?因为枚举类默认都是不可变的。

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

类索引this_class和父类索引super_class都是一个u2类型的数据。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类时全限定名。

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

类索引、父类索引按順序排列在访问标志之后,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。如图:


接口索引集合(interfaces)是一组u2类型的数据的集合,它按顺序排列在类索引、父类索引之后。对于接口索引集合,入口的第一项u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。其结构如下图:

interface_count表示当前类所实现的接口的数量或者当前接口所继承的超接口的数量。

interfaces, 他可以看做是一个数组,其中的每个数组项是一个索引, 指向常量池中的一个CONSTANT_Class_info。

注意:只有当前类直接实现的接口才会被统计,如果当前类继承了另一个类, 而另一个类又实现了一个接口,那么这个接口不会统计在当前类的。

# 字段表集合

在接口索引集合之后是字段表集合。与接口索引集合类似,入口的第一项u2类型的数据为字段计数器(fields_count),描述的是当前的类中定义的字段的个数。

需要注意的是这里包括静态字段,但不包括局部变量和从父类继承的字段。如果当前class文件是由一个接口生成的,那么这里的fields_count描述的是接口中定义的字段,我们知道,接口中定义的字段默认都是静态的。此外要说明的是,编译器可能会自动生成字段,也就是说,class文件中的字段的数量可能多于源文件中定义的字段的数量。举例来说,编译器会为内部类增加一个字段,这个字段是指向外围类的对象的引用。

fields, 可以把它看做一个数组,数组中的每一项是一个field_info 。这个数组中一共有fields_count个field_info,每个field_info都是对一个字段的描述。field_info使用伪代码结构表示如下:

field_info {
	u2 access_flags; 
	u2 name_index; 
	u2 descriptor_index; 
	u2 attributes_count; 
	attribute_info attributes[attributes_count];
}
1
2
3
4
5
6
7
类型 名称 数量 含义
u2 access_flags 1 字段修饰符
u2 name_index 1 字段和方法简单名称在常量池中的引用
u2 descriptor_index 1 字段和方法描述符在常量池中的引用
u2 attributes_count 1 描述字段额外信息属性的个数
attribute_info attributes attributes_count 具体描述字段的额外信息属性

# 字段修饰符access_flags

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

标志名称 标志值 描述
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类型

学过Java基础都有些常识,比如接口中的字段必然是public static final修饰的,同理接口字节码之中的字段必然有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标识。

# 简单名称name_index

假如一个类的类全名为:com.hukai.demo.FieldDemo。则它的全限定名为:com/hukai/demo/FieldDemo;。.仅仅是把类全名中的"."替换成了"/",同时在最后加入一个;而已。

简单名称指的是没有类型和修饰符的字段或者方法名称。这个类中的inc()方法和m字段的简单名称分別是“inc”和“m”。

# 描述符descriptor_index

相对于全限定名和简单名称来说.方法和字段的描述符就要复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

根据描述符规则,基本数据类以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,如下表:

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

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

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

在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。具体内容后续讲解。

了解了这几个概念之后,我们来看一个实例:

  • 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;

# 方法表集合

对方法描述的方式与对字段描述的方式基本一致,如下图:

methods_count描述的是当前的类中定义的方法的个数, 注意, 这里包括静态方法, 但不包括从父类继承的方法,除非这个方法在子类被重写了。 如果当前class文件是由一个接口生成的,那么这里的methods_count描述的是接口中定义的抽象方法的数量。

方法表的结构也与字段表的结构完全一致,不同之处在于方法的访问标志与字段的访问标志有所区别。例如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 是否由编译器自动产生

下面我们来看一个实例,现在有这样一个类:

package com.hukai.demo.bytecode;

public class MethodDemo {
	
	private static String name;
	
	static{
		name = "Impluvious";
	}

	public String getName() {
		return name;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用JBE工具打开class文件,可以查看到类中定义的方法:

我们明明只定义了一个方法,为什么class文件中有三个方法呢?

因为编译器可能会在编译时向class文件增加额外的方法, 也就是说, class文件中的方法的数量可能多于源文件中由用户定义的方法。 举例来说:如果当前类没有定义构造方法,那么编译器会增加一个无参数的构造函数init;如果当前类或接口中定义了静态变量, 并且使用初始化表达式为其赋值,或者定义了static静态代码块, 那么编译器在编译的时候会默认增加一个静态初始化方法clinit。

使用javap解析class文件,得到的结果也是一致的。

{
  private static java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #10                 // String Impluvious
         2: putstatic     #12                 // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 8: 0
        line 9: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public com.hukai.demo.bytecode.MethodDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #17                 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 12: 0
        line 13: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hukai/demo/bytecode/MethodDemo;

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #12                 // Field name:Ljava/lang/String;
         3: areturn
      LineNumberTable:
        line 16: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/hukai/demo/bytecode/MethodDemo;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
编辑 (opens new window)
#JVM
上次更新: 2022/03/20, 11:17:00
初识JVM
类文件结构详解下篇

← 初识JVM 类文件结构详解下篇→

最近更新
01
MyBatisPlus
03-20
02
MyBatis源码剖析-延迟加载
03-20
03
MyBatis源码剖析-二级缓存
03-20
更多文章>
Theme by Vdoing | Copyright © 2021-2022 HuKai | 赣ICP备17016768号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式