从零开始的JVM学习--类文件结构
简单介绍
什么是字节码?
什么是字节码?
JVM
可以理解的代码就叫「字节码」(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。
字节码有什么用 ?
通过「字节码」的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效
由于「字节码」并不针对一种特定的机器,因此 Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
JVM的“无关性”
平台无关性
任何操作系统都能运行 Java 代码
语言无关性
JVM
能运行除 Java 以外的其他代码。JVM
只认识 .class
文件,它不关心是何种语言生成了 .class
文件,只要 .class
文件符合 JVM
的规范就能运行。
Clojure
(Lisp
语言的一种方言)、Groovy
、Scala
等语言都是运行在 JVM
之上。尽管编译器不同,但是最终编译产物都是「字节码」,这样它们就最终能够运行在JVM
上。
「字节码」(.class
文件)是不同的语言在 JVM
之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。
类文件结构
Class 文件到底有什么内容?
「Class 文件」是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全都是连续的 0/1。
更详细的说「Class文件」是由8个字节为基础的字节流构成的,这些字节流之间都严格按照规定的顺序排列,并且字节之间不存在任何空隙,对于超过8个字节的数据,将按照Big-Endian
的顺序存储的,也就是说高位字节存储在低的地址上面,而低位字节存储到高地址上面。
这种存储也是「Class 文件」要跨平台的关键:
因为 PowerPC
架构的处理采用 Big-Endian
的存储顺序,而x86系列的处理器则采用Little-Endian
的存储顺序。因此为了「Class 文件」在各中处理器架构下保持统一的存储顺序,虚拟机规范必须对起进行统一。
「Class 文件」中的所有内容被分为两种类型:
-
无符号数
「无符号数」表示 Class 文件中的值。可以用来描述数字,索引引用以及字符串等。
这些值没有任何类型,但有不同的长度。u1、u2、u4、u8 分别代表 1/2/4/8 字节的「无符号数」。
-
表
由多个「无符号数」或者其他「表」作为数据项构成的复合数据类型。习惯以
_info
结尾,用于描述有层次关系的符合结构的数据。「Class 文件」本质上就是一张「表」。
如何自己查看Class 文件内容
创建HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
编译Java程序生成.class
文件
javac HelloWorld.java
使用JDK自带的javap
命令:
javap -verbose HelloWorld
执行结果:
Classfile /E:/Note Files/interview/code/jvm/src/main/java/com/dyh/classfile/HelloWorld.class
Last modified 2022-10-13; size 444 bytes
MD5 checksum a30e5af20cb16700ab23bb59280844aa
Compiled from "HelloWorld.java"
public class com.dyh.classfile.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/dyh/classfile/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/dyh/classfile/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "HelloWorld.java"
-
javap命令是什么?它做了什么?
javap
是JDK自带的反解析工具。它的作用就是根据Class 字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。有些信息(如本地变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等)需要在使用
javac
编译成Class 文件时,指定参数才能输出(比如,你直接javac xx.java
,就不会在生成对应的局部变量表等信息,如果你使用javac -g xx.java
就可以生成所有相关信息了)。
Class 文件的结构
已经知道了「Class 文件」的内容,而内容之间的组织编排又让「Class 文件」拥有了结构。
「Class 文件」结构采用类似C语言的结构体的方式来存储数据,大致的结构如下:
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];//属性表集合
}
接下来我们将依次介绍这些结构中的内容。在学习下面的章节之前,我们可以用一个16进制编辑器打开
Hello World.class
,方便我们理解各个结构的位置和内容。
魔数(Magic Number)
什么是魔数?
u4 magic; //Class 文件的标志
每个 Class 文件的头 4 个字节称为「魔数」(Magic Number)
Class文件的「魔数」的默认值就是0xCAFEBABE
。
魔数有什么用?
「魔数」的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
它就是JVM
识别Class文件的标志,JVM
会在验证阶段检查 Class文件是否以上面说的「魔数默认值」开头,如果不是则会抛出 ClassFormatError
。
程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义。(比如CAFEBABE
,直译过来就是“咖啡宝贝”,很有Java的风格)
版本号(Minor&Major Version)
什么是Class的版本号?
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
紧接着魔数的四个字节存储的是 Class 文件的版本号:
第 5 和第 6 位是「次版本号」,第 7 和第 8 位是「主版本号」。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。Java的版本号从45开始。
可以使用 javap -v
命令来快速查看 Class 文件的版本号信息。
其中十进制52(45+7)对应的是JDK8的版本号。
高版本的JVM
可以执行低版本编译器生成的 Class 文件,但是低版本的JVM
不能执行高版本编译器生成的 Class 文件。
所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。
常量池(Constant Pool)
什么是常量池?
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
紧接着主次版本号之后的是常量池数量和常量池,常量池的数量是 constant_pool_count-1
(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项” )。
从这里我们知道0x2B(43),为常量池长度,而常量池的数量就是42了。根据「常量池项」的长度进行叠加,最终就可以得到常量池的大小。
.class
文件可以通过javap -v class类名
指令来看一下其常量池中的信息
javap -v HelloWorld >> temp.txt
我们这里将命令结果放到temp.txt
中进行查看
常量池存储什么?
常量池主要存放两大常量:「字面量」和「符号引用」。
在开始学习这章内容之前,我们修改一下HelloWorld.java
:
public class HelloWorld {
private int basicTypeData=2;
public final static int finalConstant=0x10;
public void modifyBasicTypeData(){
final int offset=3;
basicTypeData+=offset;
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
然后再次编译程序,打印常量池信息。接着我们就可以开始继续本章的内容了。
-
字面量
「字面量」比较接近于 Java 语言层面的的常量概念:
-
文本字符串
也就是我们的“Hello World”
#4 = String #26 // Hello World! #26 = Utf8 Hello World!
-
声明为 final 的常量值(包括静态变量,实例变量,局部变量)
#10 = Utf8 finalConstant #11 = Utf8 ConstantValue #12 = Integer 16
存在于常量池的「字面量」,指的是数据的值,也就是"Hello World"和0x10(16)。
通过上面对常量池的观察可知这两个「字面量」是确实存在于常量池的。
而对于基本类型数据,也就是上面的
private int basicTypeData=2;
,常量池中只保留了它的字段描述符I
和字段的名称basicTypeData
,他们的「字面量」不会存在于常量池。而方法中的变量即便是
final
修饰的,常量值和标识符也没有出现在常量池中,因为它并不属于类。 -
-
符号引用
「符号引用」则涉及编译原理方面的概念。包括下面三类常量:
-
类和接口的全限定名
将类名中原来的"."替换为"/"得到,主要用于在运行时解析得到类的直接引用上去。
#8 = Class #34 // com/dyh/classfile/HelloWorld #34 = Utf8 com/dyh/classfile/HelloWorld
-
字段的名称和描述符
字段也就是类或者接口中声明的变量,包括类级别变量和实例级的变量
// private int basicTypeData=2; #2 = Fieldref #6.#23 // com/dyh/classfile/HelloWorld.basicTypeData:I #6 = Class #29 // com/dyh/classfile/HelloWorld #29 = Utf8 com/dyh/classfile/HelloWorld #23 = NameAndType #8:#9 // basicTypeData:I #8 = Utf8 basicTypeData #9 = Utf8 I
-
方法的名称和描述符
#17 = Utf8 modifyBasicTypeData
-
常量项的类型表
类型 | 标志值(占 1 byte) | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 表示方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Moudle_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
常见常量项内容
常量池中每一项常量都是一个表,这些表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型
对于 CONSTANT_Class_info
(此类型的常量代表一个类或者接口的符号引用),它的二维表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
-
tag
是标志位,用于区分常量类型; -
name_index
是一个索引值,它指向常量池中一个CONSTANT_Utf8_info
类型常量(此常量代表这个类(或接口)的全限定名)这里
name_index
值若为 0x001C,也即是指向了常量池中的第28项常量。
对于 CONSTANT_Utf8_info
型常量的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
tag
是当前常量的类型(同上);length
表示这个字符串的长度;bytes
是这个字符串的内容(采用缩略的 UTF8 编码)
Class文件常量池和运行时常量池的关系
Class 文件中的「常量池」在编译时确定,其中包括「符号引用」和「字面量」(文本字符串,被声明为final的变量的值)
运行时,JVM
从中读取数据到方法区的「运行时常量池」,「运行时常量池」可以在运行时添加常量
常量在编译期放入到类文件的「常量池」中,运行时放入到方法区的「运行时常量池」中(比如我们上面的例子中,方法中的常量就没有在类文件的常量池中出现)
字符串常量池(String Table)
需要注意的是「字符串常量池」和它的上一级标题「常量池」是不一样的概念。千万不要混在一起。这里之所以介绍字符串常量池也是为了帮助区分这两者的概念。
什么是字符串常量池?
为了减少在JVM
中创建的字符串的数量,虚拟机维护了一个字符串常量池(StringTable)
字符串常量池在哪?
-
JDK1.6及以前,「字符串常量池」存放在永久代。
-
JDK1.7中「字符串常量池」的位置调整到堆内。并且字符串常量池的逻辑进行了很大的改动。
- 所有的字符串都保存在堆(Heap)中,和其他普通对象一 样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
- 「字符串常量池」概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在JDK1.7中使用
String.intern()
。
-
JDK1.8「字符串常量池」仍然在堆里。
String
在JDK1.8及以前内部定义了final char [] value
用于存储字符串数据。JDK1.9时改为
byte[]
通过一段面试题程序理解字符串常量池
String s1 = "pixel-revolve";
String s2 = "pixel-revolve";
String a = "pixel";
String s3 = new String(a + "-revolve");
String s4 = new String(a + "-revolve");
System.out.println(s1 == s2); // 【1】 true
System.out.println(s2 == s3); // 【2】 false
System.out.println(s3 == s4); // 【3】 false
s3.intern();
System.out.println(s2 == s3); // 【4】 false
s3 = s3.intern();
System.out.println(s2 == s3); // 【5】 true
s4 = s4.intern();
System.out.println(s3 == s4); // 【6】 true
-
s1 == s2 返回 ture,因为都是字面量声明,全都指向字符串常量池中同一字符串。
字面量声明变量
我们用字面量声明创建了字符串常量"pixel",它会被放到字符串常量池中然后再返回给我们赋值的对象。
然后声明一个内容相同的字符串s2,会发现字符串常量池中已经存在了,那直接指向常量池中的地址即可。
-
【2】: s2 == s3 返回 false,因为 new String() 是在堆中新建对象,所以和常量池的常量不相同。
new String()的方式声明变量
与直接的字面量声明相对应的是
new String()
的方式(一般是不会用这种方式的,除非有逻辑的需要)String a = "pixel"; String s3 = new String(a + "-revolve");
使用这种方式创建字符串变量会有两种情况:
-
字符串常量池之前已经存在相同字符串
比如在使用 new 之前,已经用字面量声明的方式声明了一个变量,此时字符串常量池中已经存在了相同内容的字符串常量
- 首先会在堆中创建一个 s3 变量的对象引用;
- 然后将这个对象引用指向字符串常量池中的已经存在的常量;
-
字符串常量池中不存在相同内容的常量
直接在堆中创建一个字符串对象然后返回给变量
new String()
实际上不论常量池中有没有该字符串,都是要在堆上新建对象的,新建出来的对象,当然不会和别的对象相等。在得知了这个特征以后【3】就不难理解了。
-
-
【3】: s3 == s4 返回 false,都是在堆中新建对象,所以是两个对象,肯定不相同。
-
【4】: s2 == s3 返回 false,前面虽然调用了 intern() ,但是没有返回,不起作用。
intern()池化
Q: 如何手动将字符串放到字符串常量池中?
A: 使用
intern()
方法-
intern() 做了什么?
同样的分成两种情况:
-
如果当前字符串内容存在于字符串常量池(存在的条件是使用
equals()
方法为ture
,也就是内容是一样的)那直接返回此字符串在常量池的引用;这个时候堆中的字符串“pixel-revolve”在字符串常量池中也已经存在了。然后我们调用
s3=s3.intern();
将池化的结果返回给s3再次判断 s1 == s3 ,就会返回 true,因为它们都指向了字符串常量池的同一个字符串。
这么一来【6】的情况就不难理解了
-
如果之前不在字符串常量池中,那么在常量池创建一个引用并且指向堆中已存在的字符串,然后返回常量池中的地址。
然后我们使用
s3.intern()
。使用了intern()
之后在常量池新增了一个对象,但是 并没有 将字符串复制一份到常量池,而是直接指向了之前已经存在于堆中的字符串对象。因为在 JDK 1.7 之后,字符串常量池不一定就是存字符串对象的,还有可能存储的是一个指向堆中地址的引用(下图是只调用了
s3.intern()
,并没有返回给一个变量。其中字符串常量池(0x88)指向堆中字符串对象(0x99)就是intern()
的过程。)这么一来【4】就不难理解了。
我们将
s3.intern()
的结果返回给 s3 时,s3 才真正的指向字符串常量池。这么一来【5】就不难理解了
-
-
-
-
【5】: s2 == s3 返回 ture,前面调用了 intern() ,并且返回给了 s3 ,此时 s2、s3 都直接指向常量池的同一个字符串。
-
【6】: s3 == s4 返回 true,和 s3 相同,都指向了常量池同一个字符串。
访问标志(Access Flags)
什么是访问标志?
u2 access_flags;//Class 的访问标记
在常量池结束之后,紧接着的两个字节代表「访问标志」。
「访问标志」用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等。
我们这边javap
命令对我们HelloWorld
程序显示的内容如下:
而看到我们的16进制文件:
发现该标志值为0x0021,也就是ACC_PUBLIC
,ACC_SUPER
标志值的叠加。
具体的标志位和标志值含义
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 Public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
当前类、父类、接口索引集合
什么是 当前类、父类、接口索引集合
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
) 后的接口顺序从左到右排列在接口索引集合中。
「类索引」和「父类索引」用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info
的类描述符常量。
通过这个类描述符常量总的索引值可以找到定义在 CONSTANT_Utf8_info
类型的常量中的全限定名字符串。
我们看到16进制文件:
这告诉我们该类的索引在常量池0005的位置,父类的索引在常量池0006的位置,而接口索引的数量为0(即该类没有实现接口)
这些信息都可以根据javap命令的输出内容进行对照:
字段表集合(Fields)
什么是字段表集合?
u2 fields_count;//Class 文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
「字段表」(field info
)用于描述接口或类中声明的变量。
字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
我们编译并查看上面修改过的HelloWorld
程序:
这里告知我们该文件字段个数为2,刚好就是下面的这两个:
字段表结构
每一个字段表只表示一个成员变量,本类中的所有成员变量构成了字段表集合。这里列出了每个成员变量的内容:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 字段的访问标志,与类稍有不同。字段的作用域(public ,private ,protected 修饰符),是实例变量还是类变量(static 修饰符),可否被序列化(transient 修饰符),可变性(final ),可见性(volatile 修饰符,是否强制从主内存读写)。 |
u2 | name_index | 1 | 字段名字的索引。对常量池的引用,表示字段的名称。 |
u2 | descriptor_index | 1 | 对常量池的引用,表示字段和方法的描述符,用于描述字段的数据类型。 基本数据类型用大写字母表示; 对象类型用“L 对象类型的全限定名”表示。 |
u2 | attributes_count | 1 | 属性表集合的长度 |
u2 | attributes | attributes_count | 属性表集合,用于存放属性的额外信息,如属性的值。 |
字段表集合中不会出现从父类(或接口)中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
-
字段表的access_flags的取值
在得知以上信息以后,解读字段表集合并不是难事,这里就不再花篇幅带着解读了。
方法表集合(Methods)
什么是方法表集合?
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
methods_count
表示方法的数量,而 method_info
表示方法表。
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。
方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
依旧是上面的程序:
这告知了我们总共有2个方法,也就是以下两个方法:
方法表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
u2 | attributes | attributes_count |
-
方法表的access_flag取值
volatile
关键字 和transient
关键字不能修饰方法,所以方法表的访问标志中没有ACC_VOLATILE
和ACC_TRANSIENT
标志。方法表的属性表集合中有一张
Code
属性表,用于存储当前方法经编译器编译后的字节码指令。但是增加了synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志。
属性表集合(Attributes)
什么是属性表集合?
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
属性表集合在上面已经提到了不止一次了。
在 Class 文件,「字段表」,「方法表」中都可以携带自己的「属性表」集合,以用于描述某些场景专有的信息。
与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序。
只要不与已有的属性名重复,任何人实现的编译器都可以向「属性表」中写入自己定义的属性信息,JVM
运行时会忽略掉它不认识的属性。
属性表结构
-
Code属性
Java程序方法体里的代码经过
javac
编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有方法都必须存在这个属性表,譬如接口或抽象类中的抽象方法就不存在Code属性,如果方法有Code属性表存在,那么它的结构如下表:
类型 名称 数量 说明 u2 attribute_name_index 1 attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名称 u4 attribute_length 1 attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。 u2 max_stack 1 max_stack代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。 u2 max_locals 1 max_locals代表了局部变量表所需的存储空间。 u4 code_length 1 code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度 。 u1 code code_length code是用于存储字节码指令的一系列字节流。 u2 exception_table_length 1 exception_info exception_table exception_table_length u2 attributes_count 1 attribute_info attributes attributes_count
小结
本章我们解读了一整个Java类文件的结构。学会了如何去自己查看这些信息。并且还顺便理了一遍字符串常量池的内容。
本章参考:
转载自:https://juejin.cn/post/7154334863225520141