jvm 类文件结构学习

本文以代码示例来学习 java 类文件的结构,其中对类文件结构的学习均来自周志明先生所著的 《深入理解 Java 虚拟机》一书,在此表示诚挚的感谢。

代码如下:

 1 package com.reycg.jvm;
 2
 3 public class ReferenceCountingGC {
 4
 5     public Object instance = null;
 6
 7     public static void testGC() {
 8         ReferenceCountingGC objA = new ReferenceCountingGC();
 9         ReferenceCountingGC objB = new ReferenceCountingGC();
10
11         objA.instance = objB;
12         objB.instance = objA;
13
14         objA = null;
15         objB = null;
16
17         System.gc();
18     }
19
20
21     public static void main(String[] args) {
22         ReferenceCountingGC.testGC();
23     }
24
25 }

使用  winHex 打开对应的 class 文件,如下图显示

下面对这些数据进行分析

Magic Number 魔数

每个 Class 文件的头 4 个字节称为 magic number,它的作用就是确定这个文件是否是一个能够被 jvm 接受的 Class 文件。

0xCAFEBABE 咖啡宝贝,这个充满魔性的代号就是 class 文件的 magic number。

Class 文件的版本

紧接着 magic number 的四个字节就是 Class 文件的版本号,其中前两个字节表示次版本号,后两个字节表示主版本号。

00 00 00 33 就表示 JDK  1.7.0 版本。

常量池

上图中发灰的部分,也就是紧紧挨着主次版本号后的区域就是常量池的入口。

常量池可以理解为 Class 文件中的资源仓库。它在 class 文件中一般占用的空间最大。

常量池入口出放置了一个 u2 类型的数据,u2 也就是两个字节的意思,表示常量池容量的计数值。

从图中可看出数值为 0x0023,从 Data interpreter 中可看出十进制为 35。

与 Java 习惯语言不一致的是,这个容量计数是从 1 开始,而不是从 0 开始的,因此这就表示常量池中包含 34 个常量。

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

  • 字面量比较接近 Java 语言中的常量概念,包括

    • 文本字符串
    • 声明为 final 的常量值等
  • 符号引用则属于编译原理方面的概念
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符  

class 文件中不会保存各个方法,字段的最终的内存信息,而是在 jvm 加载 class 文件的时候才会进行动态连接,翻译成具体的内存地址。

常量池的每一项常量都是一个表,在 JDK 1.7 中共有 14 种结构各不相同的结构数据。在 《深入理解 java 虚拟机》一书中有详细的介绍,此处不再赘述。

需要说明的是每个类型结构数据表结构,最开始都是一个 u1 类型的标志位 tag,可以根据这个 tag 直接对应到到底是哪个常量类型。

观察上图, 07 则表示 tag 标志位,通过它则可知道这个常量属于 CONSTANT_Class_info 类型,该类型表示一个类或者接口的符号引用。

CONSTANT_Class_info 的结构如下

类型  名称 数量
u1 tag 1
u2 name_index 1

该结构的 name_index 是一个索引值,指向常量池中一个 Constant_Utf8_info 类型常量,该常量则代表了这个类或者接口的全限定名称。

此处 name_index 则为 0x0002 也就是指向常量池中的第 2 项常量。

继续查找第二项常量,如上图,所有的灰色区域都是第二项常量的范围。

从 tag 为 01 上可得到该常量为 CONSTANT_Utf8_info 型常量。该常量描述为 UTF-8 编码的字符串。

该常量的结构为

类型 名称 数量
u1   tag 1
u2 length 1
u1     bytes length

length 值则表示这个 UTF-8 编码的字符串长度是多少个字节。它后面会紧跟着长度为 length 字节的连续数据,该段连续数据是一个使用 UTF-8 缩略编码表示的字符串。

由于 length 值是 u2 类型的数据,因此它能表示的最大长度为 65535,因此字符串的最大长度也就是 65535 (64k),如果 Java 中定义了超过 64k 的英文字符的变量或者方法名,就会无法编译。

截止到现在,我们已经分析了常量池中 34 个常量中的两个,其余的 32 个常量都可以通过类似的方法计算出来。后面的常量方式也都可以通过这种方式计算出来,此处就不再一一分析。

实际上有现成的软件可以帮助我们完成这一过程, Oracle 公司专门有一个用于分析 Class 文件字节码的工具 javap。关于 javap 的 help 文档如下所示

下图列出了使用 javap 工具的 -verbose 参数输出的 ReferenceCountingGc.class 文件字节码内容,注意此处省略了常量池之外的其他信息。

访问标志

在常量池结束后,后面的两个字节就表示访问标志 access_flags, 这个标志用来标识一些 class 或者接口层次的访问信息。

通过页首的 java 代码可以看出 ReferenceCountingGC 的访问标志为 public class,其中

  • public 标志名称为 ACC_PUBLIC, 值为 0x0001
  • ACC_SUPER 标志在 JDK 编译出来后就必须为真,其值为 0x0020

其他的标志都为假,因此它的 access_flags 为 0x0001|0x0020=0x0021. 从上图看出确实为 0x0021

类索引,父类索引与接口索引集合

Class 文件由类索引,父类索引和接口索引,这 3 项数据来确定该 class 的继承关系,其中

  • this_class 类索引

    • 确定这个类的全限定名
    • u2 类型
  • super_class 父类索引
    • u2 类型
    • 确定这个类的父类的全限定名
  • interface 接口索引集
    • 是一组 u2 类型的数据集合
    • implements 语句对应,从左至右依次排列

访问标志后的 3个 u2 数据依次为 0x0001, 0x0003, 0x0000。也就是说

  • 类索引 0x0001
  • 父类索引 0x0003
  • 接口索引的大小 0x0000

通过查询 javap 命令计算出的常量池,找出对应的 class 和父 class 的常量

从图中对应得出

  • 类索引指向 #1 //  com/reycg/jvm/ReferenceCountingGC
  • 父类索引  #3     ////  java/lang/Object

字段表集合 field_info

字段表用来描述接口或者 class 中声明的变量。

字段 field 则包括

  • 类级变量
  • 实例级变量

此处需要总结下在 Java 中描述一个字段需要包括的信息

  • 作用域 (public, private, protected 修饰符)
  • 实例变量还是类变量 static 修饰符
  • 可变性 final
  • 并发可见性 volatile 修饰符,也就是是否强制从主内存读写
  • 是否可以被序列化 transient 修饰符
  • 字段类型数据(基本类型,对象,数组)
  • 字段名称

根据上面的分析,下表就列出了字段表的最终格式

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 description_index 1
u2 attribute_count 1
attribute_info attributes attributes_count

access_flags 就是访问修饰符,有些是强制的,有些不是强制的,依据具体情况而定。

name_index 和 description_index,都是对常量池的引用,但两者有所区别

  • name_index 表示字段的简单名称
  • description_index 表示字段和方法的描述符

简单名称,描述符以及限定名这 3 个字符串的概念

  • 全限定名,如 com/reycg/jvm/ReferenceCountingGC
  • 简单名称,指没有类型和参数修饰的方法或者字段名称,比如 testGC
  • 方法和字段的描述符
    • 作用是用来描述字段的数据类型,方法的参数列表和返回值
    • 方法的参数列表中包含
      • 数量
      • 类型
      • 顺序

描述符标识含义如下表所示

标识字符 含义
B 基本类型 byte
C 基本类型char
D 基本类型 double

F

 基本类型 float
I 基本类型 int

J

基本类型 long
S 基本类型 long
Z 基本类型 boolean
V 特殊类型 void
L 对象类型 如 Ljava/lang/object

对于数组来说,每个维度都会使用一个前置的 "[" 字符来描述,如一维数组 java.lang.String[] 将会被记录为 "[java/lang/String"。

用描述符来描述方法时,会按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号内,如

  • void inc()  ---> ()V
  • java.lang.String toString()   --> ()Ljava/lang/String
  • int indexOf(String str, int fromIndex)   -->  indexOf:(Ljava/lang/String;)I

现在结合前面字段表的最终格式表格,继续看 winhex class 数据

  • 红色框中 u2 类型数据 0x0001 表示字段表的数量 field count ,说明这个 class 只有一个字段表数据
  • 后面会紧跟着对这个字段表数据进行描述
    • 0x0001 表示 access_flags public
    • 0x0005 表示 name_index ,从前面的常量表中可查到 #5 是一个 utf8 字符串 instance
    • 0x0006 表示 description_index, 指向常量表中的 #6 表示 L/java/lang/object
    • 0x0000 表示 attributes_count 其值为 0, 那么后面也就不需要 attribute_info 字段了

由此可以推断出源代码定义字段为

private Object instance;

方法表集合

如果对字段表集合能够做到很好的理解,要理解方法表集合也就非常容易了。方法表的结构与字段表的结构非常类似,如下表所示。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

字段表集合和方法表集合最大的区别在于访问标志和属性表集合的可选项有所区别。

继续分析 class 文件

  • 0x0003 表示集合中有两个方法
  • 第一个方法集合数据
    • access_flags 0x0001 : 方法为 public
    • name_index   0x0007 : 指向常量表  #7   = Utf8       <init>
    • descriptor_index  0x0008  #8 = Utf8               ()V
    • attributes_count 0x0001 表示该方法的属性表集合中有一项属性
    • attributes 属性名称索引 0x0009 对应常量是 Code,说明该属性是方法的字节码描述

有上面分析,第一个方法集合描述的是 <init> 方法,它是由编译器自动添加的方法,叫做实例构造器方法,会在编译器优化章节中涉及到。

属性表集合

属性表这个概念在前面已经出现了数次,字段表,class 文件,方法表都可以携带自己的属性表集合,用来描述某些场景下专有的信息。

一个符合负责的属性表应该满足下表中所定义的结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info 1

在 java 虚拟机规范 Java SE 7 版中,预定义的属性有 21 项,下文会结合着 class 字节码对其中一些属性的关键部分进行讲解。

1. Code 属性

Java 程序方法体中的代码经过 Javac 编译器处理后,最终会变成字节码指令存储在 Code 属性内。

Code 属性表的结构如下图所示

类   型 名  称 数  量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attribute_count 1
attribute_info attributes attributes_count

结合下图 class 文件内容进行分析

上面讲到,方法 <init> 的 attribute 是 Code, 0x0009。

attribute_length 表示属性值的长度 0x0000003c  转换成十进制就是 60 字节

max_stack 0x0002 表示操作数栈 Operand Stacks 深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。

max_locals 0x0001 表示局部变量表所需要的存储空间。max_locals 的单位是 Slot。

Slot 是虚拟机为局部变量分配内存使用的最小单位。对于 byte, char, float,int, short,boolean 和 returnAddress 等长度不超过 32 位的类型数据,每个局部变量占用一个 slot,而 double 和 long 这两种 64 位的数据类型则需要两个 Slot 来存放。

code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令。code_length 表示字节码长度 0x0000000A,表示 10 个字节。后面的 10 个字节,也就是蓝色方框中的数据就是 code 字节码。

code 是用来存储字节码指令的一系列字节流,叫做字节码指令。其中每个指令就是一个 u1 类型的单字节,当 jvm 读取到 code 中的一个字节码时,就会找到对应的字节码代表什么指令,并且可以知道该指令是否需要跟随参数,以及参数应当如何理解。

刚才分析到了 init 方法的 code 字节码,知道占10个字节,下面我们就操刀对这10个字节的 code进行操作分析

winhex class 数据如下

这里再补充一下,下面查询的字节码指令对应关系是从《深入理解 jvm 虚拟机》 附录B 得出。

jvm 翻译这些字节的过程如下

  1. 读入 2A,查表得到对应的指令为 a_load0, 表示将第 0 个 slot 中为 reference 类型的本地变量推送到操作数栈
  2. 读入 B7 ,对应指令 invokespecial 调用超类构造方法,实例初始化方法,私有方法。解释下就是将刚才推送的 reference 类型的本地变量 作为方法参数,调用此对象的实例构造器方法。invokespecial 指令本身有一个 u2 类型的参数,来说明具体调用哪个方法
  3. 读入 0x000A 就是 invokespecial 的参数 #10 从常量池中可以查到 #10 表示
    1. Methodref          #3.#11         //  java/lang/Object."<init>":()V
    2. 也就是实例构造器 <init> 方法的符号引用
  4. 读入 2A,a_load0
  5. 读入 01, aconst_null 将 null 推送到栈顶
  6. 读入 B5,对应指令为 putfield 为指定的类的实例域赋值,putfield 后面跟一个 u2 的参数
  7. 读入 00 0C,为 putfield 的参数,到常量表里对应得到 #13
    1. #13 = NameAndType        #5:#6          //  instance:Ljava/lang/Object;
    2. 对应到代码,就是 public Object instance = null;
  8. 读入 B1 查表得到对应的指令为 return,含义是返回此方法,并且返回值为 void

由此当前整个方法结束。

现在来看下使用 javap 命令将 Class 文件中的 init 方法计算出来得到的结果。

从图中可看出,与我们的计算结果能够精确的吻合在一起。

这里有个问题 locals = 1, args_size =1 表示参数为 1, 但是在 init 方法体中并没有定义任何局部变量和参数。这是为什么呢?

这是因为在任何实例方法里面,都可以通过 "this" 关键字访问到此方法所属的对象。 javac 编译器在编译的时候,会把对 this 关键字的访问转变为对一个普通方法参数的访问,然后再 jvm 调用此参数时自动传入此参数。

而如果方法声明为 static 类型就不会将 this 传入,看代码中的 testGC 就可以得到验证。

未完待续。。。

时间: 2024-10-11 06:57:28

jvm 类文件结构学习的相关文章

深入理解JVM读书笔记: Class类文件结构

Class文件是一组以8位字节为基础单位的二进制流.采用一种类似于C语言结构体的微结构来存储数据,只有两种数据类型:无符号数和表.其中无符号数数据基本的数据类型,以u1.u2.u4.u8表示1.2.4.8字节的无符号数,用于描述数字.索引引用.数量值或者UTF-8编码字符串:表则是由无符号树和其他表的复合数据类型,以_info后缀.整个Class文件本质上就是一张表: 解析Class文件各个数据项含义: 魔数 头4个字节为魔数Magic Number,唯一作用是识别文件是否能被虚拟机接受. 版本

《JVM》(四)Class类文件结构,对象的创建

Class类文件结构 class文件是一组以8字节为单位的二进制流,只有两种数据类型:无符号数(基本数据类型),表(复合数据类型) 魔数 版本号 常量池(占class空间最大的数据之一,从1开始计数) 1.字面量 :接近于java层面的常量概念,如字符串,声明为final的常量 2.符号引用:类和接口的全限定名,字段和方法的描述符 字段描述符:描述字段数据类型 方法描述符:描述方法参数列表和返回值 访问标志 类索引,父类索引,接口索引集合 字段表集合(描述接口或类中声名的变量,不包括方法中的局部

JVM总结(三):类文件结构

这一节我们来总结一下类文件结构方面的知识.目录如下: 类文件结构 字节码的意义 Class类文件的结构 Class类文件的存储形式 Class文件的格式 Class类文件结构详解 举例详解 一.写程序 二.查看生成的相应的Class文件的16进制形式 三.深入解析 类文件结构 字节码的意义 为什么存在字节码?  字节码是构成Java平台无关性的基石.实现语言无关性的基础是虚拟机和字节码存储格式.  Java语言中的各种变量.关键字和运算符的语义最终是由多条字节码命令组成,因此字节码命令所能提供的

(转)《深入理解java虚拟机》学习笔记5——Java Class类文件结构

Java语言从诞生之时就宣称一次编写,到处运行的跨平台特性,其实现原理是源码文件并没有直接编译成机器指令,而是编译成Java虚拟机可以识别和运行的字节码文件(Class类文件,*.class),字节码文件是一种平台无关的中间编译结果,字节码文件由java虚拟机读取,解析和执行,java虚拟机屏蔽了不同操作系统和硬件平台的差异性. 如今的java虚拟机已经称为一种通用平台,不但能够运行java语言,Groovy,JRuby,Jython等一大批动态语言也可以直接在Java虚拟机上运行,其原理也是这

java类文件结构(字节码文件)

[0]README 0.1)本文部分文字描述转自 "深入理解jvm",旨在学习类文件结构  的基础知识: 0.2)本文荔枝以及荔枝的分析均为原创: 0.3)下面的截图中有附注t*编号,不关乎博文内容: [1]类文件概述 1)各种不同平台的虚拟机与所有平台都统一使用存储格式--字节码,他是构成平台无关性的基石: 2)时至今日,商业机构和开源机构已经在 java语言外发展出一大批在 jvm 上运行的语言,如 Groovy, JRuby, Jython,Scala等: 3)实现语言无关性的基

深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)

周志明的<深入理解Java虚拟机>很好很强大,阅读起来颇有点费劲,尤其是当你跟随作者的思路一直探究下去,开始会让你弄不清方向,难免有些你说的啥子的感觉.但知识不得不学,于是天天看,反复看,就慢慢的理解了.我其实不想说这种硬磨的方法有多好,我甚至不推荐,我建议大家阅读这本书时,由浅入深,有舍有得,先从宏观去理解去阅读,再慢慢深入,有条不紊的看下去.具体来说,当你看书的某一部分时,先看这部分的章节名,了解这部分这一章在讲什么,然后再看某一章,我拿"类文件结构"这一章来说,我必须

不知道Java类文件结构的同学,看这篇文章就够了

一.前言 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步.经过多年的发展,目前的计算机仍然只能识别0和1,但是由于近10年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的.平台中立的格式作为程序编译后的存储格式. 二.class类文件结构 Class文件是一组以8位字节为基础单位的二进制流,各项数据严

深入理解Java虚拟机(类文件结构)

深入理解Java虚拟机(类文件结构) 欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 之前在阅读 ASM 文档时,对于已编译类的结构.方法描述符.访问标志.ACC_PUBLIC.ACC_PRIVATE.各种字节码指令等等许多概念听起来都是云山雾罩.一知半解,原因就在于对类文件结构和类加载机制不够了解.直到后来细读了<深入理解 Java 虚拟机>中虚拟机执行子系统的相关内容,才建立了清晰的认知.如果你也和我一样,不了解类结构和类加载,但是工作中又涉及到字节码相关内容,相信后面两篇文章

Java类文件结构

Java类文件结构 阅读目录 一.概述 二.Class类文件的结构 三.字节码指令 四.参考资料 回到顶部 一.概述 实现语言无关性的基础是虚拟机和字节码存储格式.Java虚拟机不和包括Java在内的任何语言绑定,只与"Class文件"这种特定的二进制文件所关联,Class文件中包含了Java虚拟机指令集合符号表以及若干其它辅助信息.Java虚拟机作为一个通用的.机器无关的执行平台,任何其他语言都可以将其作为语言的产品交付媒介. 回到顶部 二.Class类文件的结构 Class文件是一