【云+社区年度征文】怎么理解JVM虚拟中的Class文件?

1、什么是JVM虚拟机

1.1概念

Java作为一门编程语言能够获得如此广泛的认可,除了它有结构严谨,面向对象的编程语言之外,它还具备一个非常突出的特性:一次编写到处运行,即编写的程序可以摆脱硬件平台束缚,它提供了一种相对安全的内存管理和访问机制,避免了绝大部分内存泄漏和指针越界问题。

谈到jvm,就离不开与jdk和jre的对比,那么它们之间到底有什么区别和联系呢?

1.2 jdk/jre/jvm

我们先看这样一幅架构图,

image-20201111225543541

从集合关系上看,jdk>jre>jvm,除了范围上的区别,我们应该了解的是它们所包含的功能上的差别及各自发挥的作用。

  • jdk

jdk的全称是Java Development kit(java开发工具包),我们可以把程序设计语言java虚拟机java类库这三部分统称为jdk,jdk是用于支持java程序开发的最小环境。Developer可以很容易的使用里面的方法以减少代码量,里面同时包含jre和一些开发的小工具(如编译工具javac),同时包含了jre。

  • jre

jre的全称是Java Running Environment(java运行时环境 ),可以把java类库API中的javaSE的API子集java虚拟机这两部分统称为JRE,JRE是支持java程序运行的标准环境

  • jvm

jvm的全称java virtual machine(java 虚拟机),它只认识XXX.class文件,虚拟机可以识别这种文件的字节码指令并调用操作系统上的API,正是这个原因,java才可以跨平台使用

1.3代码的执行

不管怎么说,jvm终究是一个软件,那么它是怎样屏蔽底层的操作系统硬件CPU指令层的细节呢?我们以Java程序为例来分析它的执行流程。

image-20201111231310216

图中Test.java文件是按照java语法规则编写的源文件,是一种高级语言,.java文件经javac编译后就生成字节码文件,字节码文件是用于给java虚拟机执行用的,该文件的格式规范受到java虚拟机的定义。而jvm的目的就是将字节码文件Test.class翻译为操作系统及硬件的指令,便于在不同的操作系统上执行。

NOTE: jvm虚拟机并不是仅仅只针对java语言,像一些其它编程语言如GroovyScalaKotlin也可以在jvm虚拟机上运行上,这些语言仅仅需要实现一个编译器,通过该编译器把源代码文件编译成JVM能识别的字节码文件即可。

image-20201111233843081

实现语言无关性的基础是虚拟机和字节码的存储格式,Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与Class文件这种特定的二进制文件格式所关联。

2、类文件结构

Class文件是Java语言保持良好兼容性的关键,那么Class文件的结构是什么呢,存储那些内容呢?

事实上,Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格的按照顺序紧凑地排列在文件之中,中间没有添加任何分割符,这使得整个Class文件存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

《Java虚拟机规范》规定了Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只包含两种数据类型,即无符号数

  • 无符号数

无符号数属于基本数据类型,可以用来描述数字索引引用数量值或按照UTF-8编码构成的字符串值

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名的都以_info结尾。

class文件通过固定的数据结构排列顺序并且每种数据结构指定了占用的字节长度来紧凑的在组成了完整的可读文件,jvm只需要从文件开始的地方一步一步的读取能够完全的解析出这个类文件的内容。

来感受一下字节码文件长啥样!

image-20201112225239963

懵逼了吧,这都是些啥玩意,但是理解这些十六进制数字对我们来说是非常有必要的,如果我们能充分理解每一个字节码文件的细节,自己就可以反编译出Java源文件。在下面的学习中,我们将一步步拆解class文件。

2.1魔数与版本

image-20201114163423250

在class文件中,前4个字节被称为魔数,它能够唯一确定class文件能否被虚拟机接受。其实,魔数还广泛应用在GIF、JPEG等文件头中。

紧接着魔数的4个字节存储的是Class文件的版本号,第5和第6个字节是次版本号,第7和第8个字节是主版本号。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布的主版本号加1(JDK1.0~1.1使用了45.0~45.3的版本号),《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生变化,虚拟机也必须拒绝执行超过其版本号的Class文件,所以高版本的JDK能向下兼容以前版本的Class文件,但是不能运行以后版本的Class文件。

2.2常量池

在魔数、版本号之后,下一个位置存储的就是常量池,常量池可以认为是Class文件里的资源仓库,它是Class文件结构中与其它项目关联最多的数据。常量池的前两个字节占有的位置称为常量池计数器(constant_pool_cont),它记录着常量池的组成元素常量池项(cp_info)的个数。

image-20201113230501592

常量池计数器是从1开始的,而不是从0开始的,即如果常量池计数器的值constant_pool_count=22,则后面的cp_info的个数就为21,这是因为在指定class文件规范的时候,将第0项常量空出来是为了满足某些指向常量池的索引值的数据在特定的情况下表达”不引用任何一个常量池项“,这种情况下可以将索引值设置为0来表示。

常量池中主要存放两大类常量:字面量符号引用,字面量可以理解为Java语言层面上的的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则包括类和结构的全限定名称字段的名称和描述符方法的名称和描述符等。

image-20201114164604431

Class文件存储了方法字段等各种类信息,但是它仅仅是存储了而已,它是不能反映出方法、字段等信息在内存中的布局。这是因为Java语言并不像C++语言有链接的概念,但是Java语言在虚拟机加载时会进行动态的连接,虚拟机将会从常量池中获得对应的符号引用,再在类创建时或运行时进行解析翻译到具体的内存地址之中。

2.3访问标志

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

image-20201114190537393

标志值与标志名称的对应关系如下:

标志值

标志名称

0x0001

ACC_PUBLIC

0x0010

ACC_FINAL

0x0020

ACC_SUPER

0x0100

ACC_INTERFACE

0x0200

ACC_ABSTRACT

0x1000

ACC_SYNTHETIC

0x2000

ACC_ANNOTATION

0x4000

ACC_ENUM

标志名称就是限定访问信息的,如ACC_PUBLIC表示为是否为public类型,ACC_FINAL表示为是否被声明为final,其它的标志类似。

2.4索引

访问标志结束后,紧接就是索引,包括类索引父类索引接口索引集合,Class文件可以由这三项数据来确定该类型的继承关系。我们先了解下这三类索引的各有什么作用

  • 类索引

类索引用于确定这个类的全限定名,通过类的全限定名找到这个类,所以类索引的作用就是为找出class文件所描述的这个类叫什么名字。

  • 父类索引

父类索引用于确定这个类的父类的全限定名,有Java语言不支持多重继承,所以除了Object外,其它类的父类索引只有一个。

  • 接口索引的集合

它是用来描述这个类实现哪些接口,由于接口是多实现的,所以这些实现的接口将会按顺序排列在索引集合中。接口索引的集合在入口处会有一个计数器,它用来表示集合中索引的数量,如果该类没有实现接口,则该计数器为0。

image-20201114193107542

Note:类索引、父类索引和接口索引集合指向常量池中的符号引用。

2.5字段表集合

字段表集合用于描述接口或者类中声明的变量,它有若干个字段表组成,字段表集合的就类似一个数组的结构,jvm在编译类的时候,会将类中的定义的字段的个数统计到字段计数器中,然后将每一个字段信息以结构的形式组成起来放在字段计数器之后。其结构如下图:

image-20201114195418427

特别需要注意的是,这里的字段包括类变量以及实例变量,但是不包括方法内部的声明的局部变量。

我们在思考这样一个问题,字段表存储的是那些信息,这些信息是什么呢,事实上,字段表存储的就是字段信息,我们整理如下

  • 修饰符(public、protected、private)
  • 实例变量还是类变量(被static修饰)
  • 可变性(final)
  • 并发可见性(volatile)
  • 是否可被序列化(transient)
  • 字段数据类型(基本类型、对象、数组)
  • 字段名称

既然字段有那么多信息,他的存储的形式是怎样的呢?事实上,字段的存储和我们写字段的形式是一样的,不懂?那我们就回顾下!

image-20201114202040451

在字节码,JVM定义了filed_info结构体来描述字段,它的形式也很简单,就是一个结构体,

代码语言:javascript
复制
Field_info{
 access_flags;
 name_index;
 descriptor_index;
 attribute_count;
 attributes;
}

access_flags是访问标志,与前面讲解的访问标志功能是类似的,紧接着access_flags标志的是name_indexdescriptor_index,它们是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。简单名称就是指没有类型和参数修饰的方法或者字段名称;字段和方法描述符指的是基本类型的头一个大写字母,如基本数据类型是byte,则方法描述修饰符是B。attribute_count表示的属性计数器,attributes包含三部分内容(属性名称索引、属性的长度和常量值索引)

image-20201114204710428

2.6方法表集合

方法表集合结构同字段表集合的结构是一样的,我们这里主要讲解它们之间的区别,剩下都可以按照属性表集合来学习。

  • 区别一

对于方法来说,volatile关键字和transient关键字是不修饰方法的,所以访问标志中不会有相应的标志;但是synchronized、native、stricftp和abstract关键字是可以修饰方法的,所以在会有相应的访问标志。

  • 区别二

与字段相比,方法内是有代码的,那么方法内的代码存储到哪里去了呢?事实上,对于方法里的Java代码,经Javac编译器编译成字节码的指令后,存放在方法属性表集合中会有一个名为Code的属性里面。

总结

Class文件的主要结构都说完了,我们从宏观的角度看看Class文件到底是什么样,话不多说,来看图

image-20201114212113378

参考文献

周志华.深入理解JVM虚拟机

https://blog.csdn.net/luanlouis/article/details/41046443