类文件结构

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

概述 #

  • Java中,JVM可以理解的代码就叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机
  • Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时效率极高,且由于字节码并不针对一种特定的机器。因此,Java程序无需重新编译便可在多种不通操作系统的计算机运行
  • Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译.class文件最终运行在 Java 虚拟机之上。.class文件的二进制格式可以使用 WinHexopen in new window 查看。

ly-20241212141957797

.class文件是不同语言在Java虚拟机之间的重要桥梁,同时也是支持Java跨平台很重要的一个原因

Class文件结构总结 #

根据Java虚拟机规范,Class文件通过ClassFile定义,有点类似C语言的结构体

ClassFile的结构如下:

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类可以有多个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
} 

ly-20241212141958065

通过IDEA插件jclasslib查看,可以直观看到Class 文件结构

img

使用jclasslib不光能直观地查看某个类对应的字节码文件,还可以查看类的基本信息常量池接口属性函数等信息
下面介绍一下Class文件结构涉及到的一些组件

魔数(Magic Number) #

    u4             magic; //Class 文件的标志

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件

程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义

这里前两个字节是cafe 英[ˈkæfeɪ],后两个字节 babe 英[beɪb]

JAVA为 CA FE BA BE,十六进制(一个英文字母[这里说的是字母,不是英文中文之分]代表4位,即2个英文字母为1字节)

Class文件版本号(Minor&Major Version) #

    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号

前4个字节存储Class 文件的版本号第5位第6位次版本号第7位第8位主版本号。 比如Java1.8 为00 00 00 34

JDK1.8 = 52
JDK1.7 = 51
JDK1.6 = 50
JDK1.5 = 49
JDK1.4 = 48

如图,下图是在java8中编译的,使用javap -v 查看 每当Java发布大版本(比如Java8 ,Java9 )的时候,主版本号都会+1

ly-20241212141958350

注:高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致

常量池(Constant Pool) #

    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池 

主次版本号之后的是常量池,常量池实际数量为constant_pool_count -1

常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”

常量池主要包括两大常量字面量符号引用

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

    注意,非常量是不会在这里的, ly-20241212141958491

    没有找到3

  2. 符号引用则属于编译原理方面的概念,包括下面三类常量

    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

常量池中的每一项常量都是一个表,这14种表有一个共同特点:开始第一位是一个u1类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型

ly-20241212141958637

.class 文件可以通过javap -v class类名 指令来看一下其常量池中的信息(javap -v class类名-> temp.txt :将结果输出到 temp.txt 文件)。

访问标志(Access Flag) #

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

类访问和属性修饰符

【这里好像漏了一个0x0002 ,private 】ly-20241212141958784

img

上图转自: https://www.cnblogs.com/qdhxhz/p/10676337.html

其实是所有值相加,所以对于 public interface A ,是0x601 ,即 0x200 + 0x400 + 0x001

对于 public final class MyEntity extends MyInterface0x310x0001 + 0x0010 + 0x0020

再举个例子:

package top.snailclimb.bean;
public class Employee {
   ...
}

通过 javap -v class类名指令来看一下类的访问标志
查看类的访问标志

当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合 #

    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口 
  • 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个除了 java.lang.Object 之外,所有的 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0
  • 接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements (如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。

字段表集合 (Fields) #

    u2             fields_count;//Class 文件的字段的个数
    field_info     fields[fields_count];//一个类可以有多个字段 
  • 字段表(filed info)用于描述接口中声明的变量

  • 字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量 filed info(字段表)的结构:
    ly-20241212141959204

    1. access_flag:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可否被序列化(transient修饰符)、可变性(final)、可见性(volatile修饰符,是否强制从主内存读写)
    2. name_index:对常量池的引用,表示的字段的名称
    3. descriptor_index:对常量池的引用,表示字段和方法的描述符
    4. attributes_count:一个字段还会拥有额外的属性,attributes_count 存放属性的个数
    5. attributes[attriutes_count]: 存放具体属性具体内容

    上述这些信息中,各个修饰符都是布尔值要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。

    字段的 access_flag 的取值

方法表集合(Methods) #

    u2             methods_count;//Class 文件的方法的数量
    method_info    methods[methods_count];//一个类可以有个多个方法
  • methods_count 表示方法的数量,而 method_info 表示方法表。-

  • Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志名称索引描述符索引属性表集合几项。

  • method_info(方法表的)结构
    方法表的结构 方法表的 access_flag 取值: 方法表的 access_flag 取值

    注意:因为volatile修饰符和transient修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronizednativeabstract等关键字修饰方法,所以也就多了这些关键字对应的标志。

属性表集合(Attributes) #

如上,字段和方法都拥有属性 属性大概就是这种 ly-20241212141959772

   u2             attributes_count;//此类的属性表中的属性数
   attribute_info attributes[attributes_count];//属性表集合
  • 在 Class 文件,字段表方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息
  • 与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性