深入理解JVM字节码(一)

本文详细介绍了Java Class文件的结构,包括魔数、版本号、常量池、访问标志、类索引、超类索引、接口表、字段表、方发表和属性表。重点阐述了常量池的组成,如Integer、Float、Long、Double、Utf8、String、Class、NameAndType等信息,以及方法和字段的访问标记、描述符和属性。通过对Class文件的剖析,有助于理解Java字节码的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深入剖析Class文件结构

一、初探class文件

class文件使用javac编译.java文件后得到,可以直接采用十六进制工具查看这个文件。
十六进制查看器查看class文件

二、class文件结构剖析

  1. Java虚拟机规定使用u1u2u4三种数据结构来表示1、2、4字节无符号整数,相同类型的若干条数据集合表(table)的形式来存储。表是一个变长的结构,由代表长度的表头n和紧随的n个数据项组成。class文件采用类似C语言的结构体来存储数据。
    class文件结构
  2. class文件由十个部分组成:
    • 魔数 Magic Number
    • 版本号 Minor&Major Version
    • 常量池 Constant Pool
    • 类访问标记 Access Flag
    • 类索引 This Class
    • 超类索引 Super Class
    • 接口表索引 Interface
    • 字段表 Field
    • 方发表 Method
    • 属性表 Attribute

(一) 魔数

我们经常通过文件后缀来识别文件类型。比如看到一个.jpg的文件,就知道是图片。但是使用后缀识别很不靠谱,后缀可以被任意修改,那如何根据文件内容本身来识别文件的类型呢?使用魔数(Magic Number)实现。
文件格式的制定者可以自由的选择魔数值,只要魔数值还没有被广泛才用过且不会引起混淆即可。使用十六进制工具打开class文件,首先看到的就是魔数0xCAFEBABE(咖啡宝贝)。该魔数是JVM识别.class文件的标志,虚拟机在加载类文件之前会先检查这四个字节,如果不是0xCAFEBBE,就会抛出java.lang.ClassFormateError异常。

(二) 版本号

在魔数之后的四个字节分别表示副版本号(Minor Version)和主版本号(Major Version)。
Java8的主版本号是52(0x34),如果类文件的版本号高于JVM自身的版本号,加载类就会被直接抛出java.lang.UnsupportedClassVersionError异常。
每次Java发布大版本,主版本会加1.
Java主版本号对应

(三) 常量池

紧随版本号之后的是常量池数据区域。对于JVM字节码来讲,如果操作是很常用的数字,比如0,这些操作数是内嵌到字节码中的。如果是字符串常量和较大的整数等,class文件则会把这些操作数存储在常量池(Constant Pool)中,当使用这些操作数时,会根据常量池的索引位置来查找。
常量池结构如下:
常量池结构
常量池分为两部分:

  • 常量池大小(cp_info_count) 2字节无符号整数
    常量池是class文件中第一个出现的变长结构。假设常量池大小为n,常量池真正的有效索引是1 ~ n-1。0属于保留索引,可供特殊情况使用。
  • 常量池项cp_info集合:最多包含n-1个元素。为什么是最多呢?long和double类型的常量会占用两个索引位置,如果常量池包含了这两种类型的元素,实际的常量池项的元素个数比n-1小。

常量池构成图
常量池构成图
常量池中每个常量项的结构:
常量项结构
每个cp_info的第一个字节表示常量项的类型(tag),接下来的几个字节表示常量项的具体内容。
Java虚拟机目前一共定义了14中常量项tag类型,这些常量名都已CONSTANT开头,以info结尾。
14种常量项tag
如果想查看类文件的常量池,可以使用javap命令加上-v选项。

1. CONSTANT_Integer_info和CONSTANT_Float_info

CONSTANT_Integer_info和CONSTANT_Float_info这两种结构分别用来表四int和float类型的常量,两者的结构很类似,都用4个字节来表示具体的数值常量,它们的结构定义如下
Integer与Float的字节码结构定义
Java语言规范还定义了booleanbyteshortchar类型的变量,在常量池中都会被当作int来处理。例如:
int常亮表示

2.CONSTANT_Long_info与CONSTANT_Double_info

这两种结构分别用来表示longdouble类型的变量,二者都用8个字节表示具体的常量数值,它们的结构如下面的代码所示。
在这里插入图片描述
前面提到过,CONSTANT_Long_info和CONSTANT_Double_info占用两个常量池位置,可以看到常量池大小为22,常量a占用了 #3和 #4两个位置,下一个常量项Fieldref从索引值5开始
在这里插入图片描述

3. CONSTANT_Utf8_info

CONSTANT_Utf8_info存储了字符串的内容,结构如下
在这里插入图片描述
它由三部分构成:第一个字节是tag,值固定为1;tag之后的两个字节length并不是表示字符串有多少个字符,而是表示第三部分byte数组的长度;第三部分是采用MUTF-8编码的长度为length的字节数组。
加入存储字符串"hello",则存储结构为
在这里插入图片描述

4. CONSTANT_String_info

CONSTANT_String_info用来表示java.lang.String类型的常量对象。它与CONSTANT_Utf8_info的区别是CONSTANT_Utf8_info存储了字符串真正的内容,而CONSTANT_String_info并不包含字符串的内容,仅仅包含一个指向常量池中CONSTANT_Utf8_info常量类型的索引。
CONSTANT_String_info由两部分构成,第一个字节是tag,值为8,tag后面的两个字节是一个名为string_index的索引值,指向常量池中的CONSTANT_Utf8_info,这个CONSTANT_Utf8_info存储的才是真正的字符串常量内容。

5. CONSTANT_Class_info

该结构用来表示类或接口,它的结构与CONSTANT_String_info非常相似.
在这里插入图片描述
它由两部分组成,第一个字节是tag,值固定为7 , tag后面的两个字节name_index是一个常量池索引,指向CONSTANT_Utf8_info常量,这个字符串存储的是类或接口的全限定名。例如:
在这里插入图片描述

6. CONSTANT_NameAndType_info

CONSTANT_NameAndType_info结构用来表示字段或者方法,结构为;
在这里插入图片描述
CONSTANT_NameAndType_info结构由三部分组成,第一部分tag值固定为12,后面的两个部分name_indexdescriptor_index都指向常量池中的CONSTANT_Utf8_info的索引,name_index表示字段或方法的名字descriptor_index字段或方法的描述符,用来表示一个字段或方法的类型。例如方法:
在这里插入图片描述
对应的CONSTANT_NameAndType_info结构为
在这里插入图片描述

7. CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info

这三种结构比较类似
在这里插入图片描述
下面以CONSTANT_Methodref_info为例来进行讲解,它用来描述一个方法。它由三部分组成:第一部分是tag值,固定为10;第二部分是class_index,是一个指向CONSTANT_Class_info的常量池索引值,表示方法所在的类信息;第三部分是name_and_type_index,是一个指向CONSTANT_NameAndType_info的常量池索引值,表示方法的方法名、参数和返回值类型。例如以下代码:
在这里插入图片描述
testMethod对应的Methodrefclass_index为2,指向类名为“HelloWorldMain”的类,name_and_type_index为20,指向常量池中下标为20的NameAndType索引项,对应的方法名为“testMethod”,方法类型为“(ILjava/lang/String;)V”
在这里插入图片描述

8.CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info

从JDK1.7开始,为了更好地支持动态语言调用,新增了3种常量池类型(CONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info)。以CONSTANT_InvokeDynamic_info为例,CONSTANT_InvokeDynamic_info的主要作用是为invokedynamic指令提供启动引导方法,它的结构如下所示。
在这里插入图片描述
第一部分为tag,值固定为18;第二部分为bootstrap_method_attr_index,是指向引导方法表bootstrap_methods[]数组的索引。第三部分为name_and_type_index,是指向索引类常量池里的CONSTANT_NameAndType_info的索引,表示方法描述符。

(四) Access flags

紧随常量池之后的区域是访问标记(Access flags),用来标识一个类为finalabstract等,由两个字节表示,总共有16个标记位可供使用,目前只使用了其中的8个。
在这里插入图片描述
完整的访问标记符含义为:
在这里插入图片描述
这些访问标记并不是可以随意组合的,比如ACC_PUBLICACC_PRIVATEACC_PROTECTED不能同时设置,ACC_FINALACC_ABSTRACT也不能同时设置,否则会违背语义。

(五) this_class、super_name、interfaces

这三部分用来确定类的继承关系,this_class表示类的索引,super_name表示直接父类的索引,interfaces表示类或者接口的直接父接口。
this_class是一个指向常量池的索引,表示类或者接口的名字,用两字节表示。

(六) 字段表

紧随接口索引表之后的是字段表(fields),其中定义的字段会被存储到这个集合中,包括静态和非静态的字段,它的结构可以用下面的伪代码表示。
在这里插入图片描述
字段表也是一个变长的结构,fiedls_count表示field的数量,接下来的fields表示字段集合,共有fields_count个,每一个字段用field_info结构表示,稍后会进行介绍。

1.字段field_info结构

每个字段field_info的格式如下
在这里插入图片描述
字段结构分为4个部分:第一部分access_flags表示字段的访问标记,用来标识是publicprivate还是protected,是否是static,是否是final等;第二部分name_index用来表示字段名,指向常量池的字符串常量;第三部分descriptor_index是字段描述符的索引,指向常量池的字符串常量;最后的attributes_countattribute_info表示属性的个数和属性集合。

2. 字段访问标记

与类一样,字段也拥有访问标记,但字段的访问标记更丰富,共有9种。
在这里插入图片描述
如果在类中定义了字段public static final int DEFAULT_SIZE = 128,编译后DEFAULT_SIZE字段在类文件中存储的访问标记值为0 x0019,则它的访问标记为ACC_PUBLIC |ACC_STATIC | ACC_FINAL,表示它是一个public static final类型的变量。
同之前介绍的类访问标记一样,字段访问标记并不是可以随意组合的,比如ACC_FINALACC_VOLATILE也不能同时设置,否则会违背语义。

3. 字段描述符

字段描述符field descriptor用来表示某个field的类型,在JVM中定义一个int类型的字段时,类文件中存储的类型并不是字符串int,而是更精简的字母I。

根据类型不同,字段描述符分为三大类:

  • 原始类型,byte、int、char、float等这些简单类型使用一个字符来表示,比如J对应long类型,B对应Byte类型。
  • 引用类型使用L;的方式来表示,为了防止多个连续的引用类型描述符出现混淆,引用类型描述符最后都加了一个;作为结束,比如字符串类型String的描述符为Ljava/lang/String;
  • JVM使用一个前置的[表示数组类型,如intp[]类型的描述符为[I,字符数组String[]的描述符为[Ljava/lang/String;。而多维数组描述符只是多加了几个[而已,比如Object[][]的描述符为[[Ljava/lang/Object;

完整的字段类型描述符隐射表
在这里插入图片描述

4. 字段属性

与字段相关的属性包括ConstantValueSyntheticSignatureDeprecatedRuntime-Visible AnnotationsRuntimeInvisibleAnnotations这6个,比较常见的是ConstantValue属性,用来表示一个常量字段的值。

(七) 方发表

方发表的作用和字段表非常相似,类中定义的方法被存储在这里,方发也是一个变长结构,表结构
在这里插入图片描述
其中methods_count表示方法的数量,接下来的methods表示方法的集合,共有methods_count个,每一个方法用method_info结构表示。

1. 方法method_info结构

在这里插入图片描述
方法method_info结构分为四部分:第一部分access_flags表示方法的访问标记,用来标记是publicprivate还是protected,是否是static,是否是final等;接下来的name_indexdescriptor_index分别表示方法名和方法描述符的索引值,指向常量池的字符串常量;attributes_countattribute_info表示方法相关属性的个数和属性集合,包含了很多有用的信息,比如方法内部的字节码就存放在Code属性中。
在这里插入图片描述

2. 方法访问标记

方法的访问标记比类和字段的访问标记更丰富,一共12种。
在这里插入图片描述
例子:
在这里插入图片描述
生成的类文件中,foo方法的访问标记等于0x002aACC_PRIVATE |ACC_STATIC|ACC_SYNCHRONIZED),表示这是一个private static synchronized的方法。
在这里插入图片描述

3. 方法名与描述符

紧随方法访问标记的是方法名索引name_index,指向常量池中CONSTANT_Utf8_info类型的字符串常量,比如有这样一个方法定义private void foo(),编译器会生成一个类型为CONSTANT_Utf8_info的字符串常量项,里面存储了foo,方法名索引name_index指向了这个常量项。
方法描述符索引descriptor_index也是指向常量池中类型为CONSTANT_Utf8_info的字符串常量项。方法描述符用来表示一个方法所需的参数和返回值,格式如下:

(参数1类型 参数2类型 参数3类型 …) 返回值类型

比如,方法Object foo(int i, double d, Thread t)的描述符为(IDLjava/lang/Thread;) Ljava/lang/Object;,其中I表示第一个参数i的参数类型int,D表示第二个参数d的类型double,Ljava/lang/Thread;表示第三个参数t的类型Thread,Ljava/lang/Object;表示返回值类型Object在这里插入图片描述

4. 方法属性表

方法属性表是method_info结构的最后一部分。前面介绍了方法的访问标记和方法签名,还有一些重要的信息没有出现,如方法声明抛出的异常方法的字节码,方法是否被标记为deprecated等,属性表就是用来存储这些信息的。与方法相关的属性有很多,其中比较重要的是CodeExceptions属性,其中Code属性存放方法体的字节码指令Exceptions属性用于存储方法声明抛出的异常

(八) 属性表

在方法表之后的结构是class文件的最后一部分——属性表。属性出现的地方比较广泛,不只出现在字段和方法中,在顶层的class文件中也会出现。相比于常量池只有14种固定的类型,属性表的类型更加灵活,不同的虚拟机实现厂商可以自定义属性,属性表的结构如下所示。
在这里插入图片描述
与其他结构类似,属性表使用两个字节表示属性的个数attributes_count,接下来是若干个属性项的集合,可以看作是一个数组,数组的每一项都是一个属性项attribute_info,数组的大小为attributes_count。每个属性项的atttribute_info的结构如下
在这里插入图片描述attribute_name_index是指向常量池的索引,根据这个索引可以得到attribute的名字,接下来的两部分表示info数组的长度和具体byte数组的内容。
虚拟机预定义了20多种属性,下面介绍字段表相关的ConstantValue属性和方法表相关的Code属性。

1. ConstantValue属性

ConstantValue属性出现在字段field_info中,用来表示静态变量的初始值,它的结构如下所示。
在这里插入图片描述
其中attribute_name_index是指向常量池中值为ConstantValue的字符串常量项,attribute_length值固定为2,因为接下来的具体内容只会有两个字节大小。constantvalue_index指向常量池中具体的常量值索引,根据变量的类型不同,constantvalue_index指向不同的常量项。如果变量为long类型,则constantvalue_index指向CONSTANT_Long_info类型的常量项。

2. Code属性

Code属性是类文件中最重要的组成部分,它包含方法的字节码,除nativeabstract方法以外,每个method都有且仅有一个Code属性,它的结构如下在这里插入图片描述
1) 属性名索引(attribute_name_index),指向常量池中CONSTANT_Utf8_info常量,表示属性的名字,比如这里对应的常量池的字符串常量"Code"。
2)属性长度(attribute_length)
3)max_stack表示操作数栈的最大深度,方法执行的任意期间操作数栈的深度都不会超过这个值。它的计算规则是:有入栈的指令stack增加,有出栈的指令stack减少,在整个过程中stack的最大值就是max_stack的值,增加和减少的值一般都是1,但也有例外:LongDOUBLE相关的指令入栈stack会增加2,VOID相关的指令则为0.
4)max_locals表示局部变量表的大小,它的值并不等于方法中所有局部变量的数量之和。当一个局部作用域结束,它内部的局部变量占用的位置就可以被接下来的局部变量复用了
5)code_lengthcode用来表示字节码相关的信息。其中,code_length表示字节码指令长度;code是一个长度为code_length字节的数组,存储真正的字节码指令。
6)exception_table_lengthexception_table用来表示代码内部的异常表信息,如我们熟知的try-catch语法就会生成对应的异常表exception_table_length表示接下来exception_table数组的长度,每个异常项包含四个部分,可以用下面的结构表示。
在这里插入图片描述
其中start_pcend_pchandler_pc都是指向code字节数组的索引值,start_pcend_pc表示异常处理器覆盖的字节码开始和结束的位置,是左闭右开区间[start_pc, end_pc),包含start_pc,不包含end_pchandler_pc表示异常处理handlercode字节数组的起始位置,异常被捕获以后该跳转到何处继续执行。catch_type表示需要处理的catch的异常类型是什么,它用两个字节表示,指向常量池中类型为CONSTANT_Class_info的常量项。如果catch_type等于0,则表示可处理任意异常,可用来实现finally语义。
当JVM执行到这个方法 [start_pc, end_pc)范围内的字节码发生异常时,如果发生的异常是这个catch_type对应的异常类或者它的子类,则跳转到code字节数组handler_pc处继续处理。
7)attributes_countattributes[]用来表示Code属性相关的附属属性,Java虚拟机规定Code属性只能包含这四种可选属性:LineNumberTableLocalVariableTableLocalVariableTypeTableStackMapTable

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值