我们先来研究下JVM的输入文件,上文说到JVM
的输入是字节码文件,但字节码是什么,长什么样,又是如何生成的呢?
这篇文章我们就来了解下这些信息。
本文我们先来看看 字节码文件长啥样,下一篇文章我再来分享 这个字节码是如何生成的。
大家可以从https://hg.openjdk.java.net/jdk8/jdk8/langtools 官网下载源码文件(左侧的.tar.gz或者.zip文件)
看我本地就是创建了一个javac
的工程
然后将源码进行解压(注意:mac系统建议通过命令行方式进行解压)
具体位置是:src/share/classes/com/sun/tools/javac
这里不要复制其他的源码哈,不然容易出现很多【类找不到】的错误。别问我是咋知道的
这种错误,代表没有源文件输入
然后我们在javac
包同级创建一个examples
这个包,做为我们学习的整个学习用例。我们先来创建一个helloWorld
文件
package examples;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/3/25 3:05 下午
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.printf("hello world");
}
}
然后在启动配置的Program arguments
里加入 HelloWorld.java 的绝对路径。
再运行Main
方法,会发现,在HelloWorld.java 同级别下生成了HelloWorld
.class文件
那我们咋看是如何生成的呢,启用万能的debug
我们在Main类的compile
方法加个断点,但发现开始调试以后会发现不管怎么设置,调试都会进入tool.jar
,没有走刚刚导入的源码。
打开 Project Structure 页面(File->Project Structure), 选中图中 Dependencies 选项卡,把 `` 顺序调整到项目 JDK 的上面:调整好后是
再重新断点,就可以进入源代码的断点啦。
我们还可以通过在IntelliJ IDEA上添加一个 tools
来查看javac
文件。
添加步骤
-c $FileClass$
表示文件名称$OutputPath$
可直接将结果打印到控制台然后就可以通过下面的这个入口来看到刚刚执行的java类的class文件
来个 效果图吧
/**
* @Description
* @Author 大龄波妞
* @Date 2021/3/23 10:58 上午
*/
public class FinallyTest {
public void testFinally(){
int a ;
try {
int b = 10;
}finally {
a = 3;
}
}
public static void main(String[] args) {
}
}
点击上面的tools
->java -c
之后控制台输出的是
这样一个内容
通过java -c
我们能直观看到字节码长啥样,然后再通过源码,我们就能知道这个是如何生成的。
上面的java -c
看到的是指令数据,能方便我们看到我们写的代码 具体转换成了哪些jvm指令(jvm指令也会有单独一篇文章介绍哈~)
而若我们想知道字节码的一些结构和属性,可以通过jclasslib
来做到。jclasslib
是一个IDEA的插件,所以可以通过插件方式找到并安装。具体步骤如下:
我们后面的介绍会 通过jclasslib和真实的.class
二进制文件 + 理论相结合的方式来介绍Class文件结构
想要了解Java语言书写的类是如何转变成字节码的就需要先知道Class文件的整体结构是啥样的,先知整体而后再看具体case案例。
其中 u1、u2、u4三种数据结构来表示1、2、4字节无符号整数。可以从图中得知:
class文件由下面十个部分组成:
接下来,我们将通过这个例子来一一介绍上面的这些。
/**
* @Description
* @Author 大龄波妞
* @Date 2021/3/26 5:38 下午
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
魔数是一个u4类型的数字,长4个字节;魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。也就是我们.class
文件中固定的“cafe babe”
若修改成其他的字符,则虚拟机将不识别;就好比.pdf
代表pdf文件,pdf阅读器能识别此文件,其他文件则不能识别一样。
在魔数之后的四个字节分别表示副版本号(Minor Version)和主版本号(Major Version)。
从图中看到 副版本是0,主版本是0X0034
也就是52;我们来看下jclasslib视图信息
注意:
副版本号不同版本的Java虚拟机实现支持的版本号也不同,高版本号的Java虚拟机实现可以支持低版本号的Class文件,但低版本的Java虚拟机则不能运行高版本的Class文件。
常量池有两个字段 constant_pool_count
和constant_pool
constant_pool_count
代表的是常量池的大小,用两个字节表示。假设常量池大小为n,常量池真正有效的索引是1~n-1。也就是说,如果constant_pool_count等于10,constant_pool数组的有效索引值是1~9。0属于保留索引,可供特殊情况使用。
两个字节,所以是0022
,这个是十六进制的数字,转变成十进制就是 34;也就是说 常量池大小是34;我们来通过jclasslib视图信息验证下:
从图中能看到 常量池里包含各种字符串常量、类和接口名、字段名以及在类文件结构及其子结构中引用的其他常量。而且下标索引是从1~n-1的。
也就是包含两个信息:
Java虚拟机目前一共定义了14种常量项tag类型,这些常量名都以CONSTANT开头,以info结尾。具体如下表
类型 | 值 | 描述 |
---|---|---|
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_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
接下来,我们来看看常量池里的这些tag都存储了哪些,以及其结构式啥样的。
CONSTANT_Methodref_info
类型数据,我们来看看这个的结构是啥样的。结构如下:
字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | U1 | CONSTANT_Methodref_info 也就是10 | |
class_index | U2 | 常量池中的有效索引且常量池中这个索引对应的成员必须是CONSTANT_Class_info 结构,且必须是类 | |
name_and_type_index | U2 | 常量池的有效索引 常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。 在Fieldref_info中的这个 就必须是方法描述符 ( 方法的参数最多有255个) |
如果一个CONSTANT_Methodref_info结构的方法名以“<”(’\u003c’)开头, 则说明这个方法名是特殊的, 即这个方法是实例初始化方法, 它的返回类型必须为空。 |
我们来看看上面的HelloWorld
的这个类型的数据:
0X0a
代表的就是tag=100006
的两个字节就是class_index
也就是在常量池中index=6的数据的类型是CONSTANT_Class_info
的数据。这个类型我们等下介绍。0014
的这两个字节就是name_and_type_index
,也就是在常量池中index=20的数据是CONSTANT_NameAndType_info
类型的数据,(这个类型我们等下介绍)在这个二进制文件后面紧接着就是tag=9
的数据,按照上面的表格,就是CONSTANT_Fieldref_info
我们来看看这个类型的数据结构是啥样的
字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | u1 | CONSTANT_Fieldref 也就是9 | |
class_index | u2 | 常量池中的有效索引 且常量池中这个索引对应的成员必须是 CONSTANT_Class_info 结构,表示一个类或者接口 |
|
name_and_type_index | u2 | 常量池的有效索引 常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。 在Fieldref_info中的这个 就必须是字段描述符 |
也就是说 二进制文件中的
这些代表的是CONSTANT_Fieldref_info
这个类型的数据,我们来看看存储了啥
09
是tag的值0015
的两个字节就是class_index
也就是在常量池中index=21的数据的类型是CONSTANT_Class_info
的数据,代表类或者接口(index=21)0016
的两个字节就是name_and_type_index
也就是在常量池中index=22的数据类型是CONSTANT_NameAndType_info
的数据,代表字段或方法的描述符。字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | u1 | CONSTANT_String 也就是8 | |
string_index | u2 | 是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构, 表示一组Unicode码点序列, 这组Unicode码点序列最终会被初始化为一个String对象。 |
08
就是tag的值0017
这两个字节代表的就是string_index
,也就是在常量池中index=33的CONSTANT_Utf8_info
类型的String对象。而这个index=33的数据就是接着看上面的二进制文件,是0a
开头的也就 代表着接下来的数据是CONSTANT_Methodref_info
这个类型的数据,也就是
这个是CONSTANT_Methodref_info
这个类型的数据
0X0a
代表的就是tag=100018
的两个字节就是class_index
也就是在常量池中index=24的数据的类型是CONSTANT_Class_info
的数据。这个类型我们等下介绍。0019
的这两个字节就是name_and_type_index
,也就是在常量池中index=25的数据是CONSTANT_NameAndType_info
类型的数据,(这个类型我们等下介绍)================================================
继续看下一组,tag=07;也就是CONSTANT_Class_info
类型
CONSTANT_Class_info的数据结构是:
字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | u1 | CONSTANT_Class的值,也就是7 | |
name_index | U2 | 对常量池的一个有效索引,指向CONSTANT_Utf8_info常量,这个字符串存储的是类或接口的全限定名 |
07
代表的是tag=7001a
这两个字节代表的是name_index
,也就是在常量池中index=26的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。================================================
继续看下一组,仍然是tag=07;也就是CONSTANT_Class_info
类型
07
代表的是tag=7001b
这两个字节代表的是name_index
,也就是在常量池中index=27的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。================================================
继续看下一组,仍然是tag=01;也就是CONSTANT_Utf8_info
类型
字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | u1 | CONSTANT_Utf8也就是1 | |
length | u2 | bytes[]数组的长度 CONSTANT_Utf8_info结构中的内容是以length属性确定长度而不是以null作为字符串的终结符 |
|
bytes[length] | u1 | bytes[]是表示字符串值的byte数组,bytes[]数组中每个成员的byte值都不会是0,也不在0xf0至0xff范围内 |
我们来看下二进制中是什么
01
代表的是tag=10006
的两个字节代表的是数组的长度,也就是6;从这块开始数6个字节为这个字符数组的值了,也就是图中标黄的地方。我们将上面的十六进制复制出来,然后用在线十六进制转字符串工具转换下==========================
我们继续往下看
01
代表的是tag=10003
的两个字节代表的是数组的长度,也就是3;从这块开始数3个字节为这个字符数组的值了,也就是图中标黄的地方。我们将上面的十六进制复制出来,然后用在线十六进制转字符串工具转换下以此类推,index【7-19】都是CONSTANT_Utf8_info
类型的数据,数据分别是
index | 值 |
---|---|
7 | |
8 | ()V |
9 | Code |
10 | LineNumberTable |
11 | LocalVariableTable |
12 | this |
13 | Ljvm/classLib/HelloWorld; |
14 | main |
15 | ([Ljava/lang/String;)V |
16 | args |
17 | [Ljava/lang/String; |
18 | SourceFile |
19 | HelloWorld.java |
图片中标黄的地方 就是index[7-19]的值
继续往下看,tag=0c
=12,也就是CONSTANT_NameAndType_info
类型
字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | u1 | CONSTANT_NameAndType 也就是12 | |
name_index | u2 | 必须对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构要么表示特殊的方法名,要么表示一个有效的字段或方法的非限定名(Unqualified Name)。 | |
descriptor_index | u2 | 必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构表示一个有效的字段描述符或方法描述符。 |
0c
代表的是tag=120007
的两个字节代表的是name_index
,就是在常量池中index=7的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。(由上面信息可知index=7的对应字符串是)0008
的两个字节代表的是descriptor_index
,就是在常量池中index=8的数据类型是CONSTANT_Utf8_info
的字段或方法描述符(由上面信息可知index=8的对应字符串是**()V**)==============================
咱们继续往下看,tag=07
也就是CONSTANT_Class_info
类型的数据
07
代表的是tag=7001c
这两个字节代表的是name_index
,也就是在常量池中index=28的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。(由下文可知index=28的字符串是java/lang/System)==============================
咱们继续往下看,tag=0c
也就是CONSTANT_NameAndType_info
类型的数据
0c
代表的是tag=12001d
的两个字节代表的是name_index
,就是在常量池中index=29的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。(由上面信息可知index=29的对应字符串是out)001e
的两个字节代表的是descriptor_index
,就是在常量池中index=30的数据类型是CONSTANT_Utf8_info
的字段或方法描述符(由上面信息可知index=30的对应字符串是Ljava/io/PrintStream;)===============================================
我们继续往下看,tag=1,也就是CONSTANT_Utf8_info类型数据
01
代表的是tag=1000b
的两个字节代表的是数组的长度,也就是12;从这块开始数12个字节为这个字符数组的值了,也就是图中标黄的地方。我们将上面的十六进制复制出来,然后用在线十六进制转字符串工具转换下===================================
我们继续往下看,tag=7,也就是CONSTANT_Class_info
类型的数据
07
代表的是tag=7001f
这两个字节代表的是name_index
,也就是在常量池中index=31的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。(由下面信息可知,index=31的字符串是java/io/PrintStream)===================================
我们继续往下看,tag=12,也就是CONSTANT_NameAndType_info
类型的数据
0c
代表的是tag=120020
的两个字节代表的是name_index
,就是在常量池中index=32的数据的类型是CONSTANT_Utf8_info
的类或者接口的全限定名。(由上面信息可知index=32的对应字符串是println)0021
的两个字节代表的是descriptor_index
,就是在常量池中index=33的数据类型是CONSTANT_Utf8_info
的字段或方法描述符(由上面信息可知index=33的对应字符串是**(Ljava/lang/String;)V**)==============================================
我们继续往后看,发现,index【26-33】都是CONSTANT_Utf8_info
类型的数据,
数据分别是
index | 值 |
---|---|
26 | jvm/classLib/HelloWorld |
27 | java/lang/Object |
28 | java/lang/System |
29 | out |
30 | Ljava/io/PrintStream; |
31 | java/io/PrintStream |
32 | println |
33 | (Ljava/lang/String;)V |
==================
至此,这个HelloWorld
类的常量池就到这,但常量池的类型还没有结束哈~
我们来继续看其他的类型
数据结构:
字段名称 | 字段类型 | 值 | 备注 |
---|---|---|---|
tag | u1 | CONSTANT_Integer也就是3 | |
bytes | u4 | 表示int常量的值,按照Big-Endian的顺序存储。 |
字段名称 | 字段类型 | 值 | 备注 |
---|---|---|---|
tag | u1 | CONSTANT_Integer也就是4 | |
bytes | u4 | 按照IEEE 754单精度浮点格式.表示float常量的值,按照Big-Endian的顺序存储 | bytes项的值首先被转换成一个int常量的bits: 如果bits值为0x7f800000,表示float值为正无穷。 如果bits值为0xff800000,表示float值为负无穷。 如果bits值在范围0x7f800001到0x7fffffff或者0xff800001到0xffffffff内,表示float值为NaN。 否则就按公式来计算 |
字段名称 | 字段类型 | 值 | 备注 |
---|---|---|---|
tag | u1 | CONSTANT_Long 也就是5 | |
high_bytes | u4 | 无符号的high_bytes和low_bytes项用于共同表示long型常量,构造形式为((long) high_bytes << 32) + low_bytes,high_bytes和low_bytes都按照Big-Endian顺序存储。 | 所有的8字节的常量都占两个表成员(项)的空间。如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池中的索引为n,则常量池中下一个有效的项的索引为n+2,此时常量池中索引为n+1的项有效但必须被认为不可用。 |
low_bytes | u4 |
字段名称 | 字段类型 | 值 | 备注 |
---|---|---|---|
tag | u1 | CONSTANT_Double也就是6 | |
high_bytes | u4 | high_bytes和low_bytes共同按照IEEE 754双精度浮点格式表示double常量的值。high_bytes和low_bytes都按照Big-Endian顺序存储 | high_bytes项和low_bytes项的值首先被转换成一个int常量的bits: 如果bits值为0x7ff0000000000000L,表示double值为正无穷。 如果bits值为0xfff0000000000000L,表示double值为负无穷。 如果bits值在范围0x7ff0000000000001L到 0x7fffffffffffffffL或者0xfff0000000000001L到 0xffffffffffffffffL内,表示double值为NaN 否则就按公式来计算 |
low_bytes | u4 |
字段名称 | 字段类型 | 值 | 备注 |
---|---|---|---|
tag | u1 | CONSTANT_MethodHandle也就是15 | |
reference_kind | u1 | 必须在1至9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 | 具体类型见下面单独的子表 如果reference_kind项的值为1(REF_getField)、2(REF_getStatic)、3(REF_putField)或4(REF_putStatic),那么常量池在reference_index索引处的项必须是CONSTANT_Fieldref_info结构,表示由一个字段创建的方法句柄。 如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或8(REF_newInvokeSpecial),那么常量池在reference_index索引处的项必须是CONSTANT_Methodref_info结构,表示由类的方法或构造函数创建的方法句柄。 如果reference_kind项的值是9(REF_invokeInterface),那么常量池在reference_index索引处的项必须是CONSTANT_InterfaceMethodref_info结构,表示由接口方法创建的方法句柄。 如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或9(REF_invokeInterface),那么方法句柄对应的方法不能为实例初始化()方法或类初始化方法()。 如果reference_kind项的值是8(REF_newInvokeSpecial),那么方法句柄对应的方法必须为实例初始化()方法 |
reference_index | U2 | 必须是对常量池的有效索引 |
字段名称 | 字段类型 | 值 | 备注 |
---|---|---|---|
tag | u1 | CONSTANT_MethodType 也就是16 | |
descriptor_index | u2 | 必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 |
字段名称 | 字段类型 | 值 | 说明 |
---|---|---|---|
tag | u1 | CONSTANT_InvokeDynamic 也就是18 | |
bootstrap_method_attr_index | u2 | 必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | u2 | 常量池的有效索引 |
值 | 对应英文名称 | 含义 | 说明 |
---|---|---|---|
1 | REF_getField | ||
2 | REF_getStatic | ||
3 | REF_putField | ||
4 | REF_putStatic | ||
5 | REF_invokeVirtual | ||
6 | REF_invokeStatic | ||
7 | REF_invokeSpecial | ||
8 | REF_newInvokeSpecial | ||
9 | REF_invokeInterface |
紧随常量池之后的区域是访问标记(Access flags),用来标识一个类为final、abstract等,由两个字节表示,总共有16个标记位可供使用,目前只使用了其中的8个
类型名称 | 十六进制值 | 定义 | 解释说明 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | 声明为公共的;可以从其包外部访问 | |
ACC_FINAL | 0x0010 | 声明为final;不允许有子类 | |
ACC_SUPER | 0x0020 | 当由invokespecial指令调用时,需要特别处理的超类方法。 | |
ACC_INTERFACE | 0x0200 | 标识是一个接口而非类 | 有这个标识的类表示其是接口,而非类,则会同时再被标记为ACC_ABSTRACT不拥有ACC_FINAL和ACC_ENUM 的标记 |
·ACC_ABSTRACT | 0x0400 | 声明为抽象的;不能实例化。 | |
ACC_SYNTHETIC | 0x1000 | 声明为合成的;不存在于源代码中。表明这个类是由编译器自己产生的而不是我们编写的代码生成的 | |
ACC_ANNOTATION | 0x2000 | 标识是一个注解 | |
ACC_ENUM | 0x4000 | 标识是一个枚举类型;可以是这个类或者其父类都是枚举类型 |
0021
也就是ACC_SUPER 和ACC_PUBLIC
但这些访问标记并不是可以随意组合的,比如ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED不能同时设置,ACC_FINAL和ACC_ABSTRACT也不能同时设置,否则会违背语义。更多的规则可以在javac源码的com.sun.tools.javac.comp.Check.java文件中找到。
类型名称 | 十六进制值 | 含义 | 解释说明 |
---|---|---|---|
ACC_PUBLIC |
0x0001 | 源文件定义public | |
ACC_PRIVATE |
0x0002 | 源文件定义private | |
ACC_PROTECTED |
0x0004 | 源文件定义protected | |
ACC_STATIC |
0x0008 | 源文件定义static | |
ACC_FINAL |
0x0010 | 源文件定义final | |
ACC_INTERFACE |
0x0200 | 源文件定义interface | |
ACC_ABSTRACT |
0x0400 | 源文件定义abstract | |
ACC_SYNTHETIC |
0x1000 | 声明synthetic,非源文件定义 | |
ACC_ANNOTATION |
0x2000 | 声明annotation | |
ACC_ENUM |
0x4000 | 声明enum |
this_class、super_name、interfaces
这三部分用来确定类的继承关系,this_class表示类索引,super_name表示直接父类的索引,interfaces表示类或者接口的直接父接口。this_class是一个指向常量池的索引,表示类或者接口的名字,用两字节表示,
紧随接口索引表之后的是字段表(fields),类中定义的字段会被存储到这个集合中,包括静态和非静态的字段,它的结构如下:
字段表也是一个变长的结构,fields_count表示field的数量,接下来的fields表示字段集合,共有fields_count个,每一个字段用field_info结构表示,
字段名称 | 类型 | 含义说明 | 备注 |
---|---|---|---|
access_flags |
u2 | 用于定义字段被访问权限和基础属性的掩码标志,,具体见Field的access_flag表 | |
name_index |
u2 | 对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示一个有效的字段的非全限定名。 | |
descriptor_index |
u2 | 必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示一个有效字段的描述符 | |
attributes_count |
u2 | 表示当前字段的附加属性的数量 | |
attributes[attributes_count] |
attribute_info |
attributes表的每一个成员的值必须是attribute结构,一个字段可以有任意个关联属性 |
类型名称 | 十六进制值 | 定义 | 解释说明 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | public,表示字段可以从任何包访问 | |
ACC_PRIVATE | 0x0002 | private,表示字段仅能该类自身调用 | |
ACC_PROTECTED | 0x0004 | protected,表示字段可以被子类调用 | |
ACC_STATIC | 0x0008 | static,表示静态字段 | |
ACC_FINAL | 0x0010 | final,表示字段定义后值无法修改 | |
ACC_VOLATILE | 0x0040 | volatile,表示字段是易变的 | |
ACC_TRANSIENT | 0x0080 | transient,表示字段不会被序列化 | |
ACC_SYNTHETIC | 0x1000 | 表示字段由编译器自动产生 | 表明这个字段是由编译器自己产生的而不是我们编写的代码生成的 |
ACC_ENUM | 0x4000 | enum,表示字段为枚举类型 |
字段描述符(field descriptor)用来表示某个field的类型,在JVM中定义一个int类型的字段时,类文件中存储的类型并不是字符串int,而是更精简的字母I。根据类型的不同,字段描述符分为三大类。
完整的字段类型描述符:
描述符 | 代表含义 | 解释说明 |
---|---|---|
B |
byte |
signed byte |
C |
char |
Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16 |
D |
double |
double-precision floating-point value |
F |
float |
single-precision floating-point value |
I |
int |
integer |
J |
long |
long integer |
L ClassName ; |
reference |
an instance of class ClassName |
S |
short |
signed short |
Z |
boolean |
true or false |
[ |
reference |
one array dimension |
与字段相关的属性包括ConstantValue、Synthetic、Signature、Deprecated、Runtime-Visible-Annotations和RuntimeInvisibleAnnotations这6个,比较常见的是ConstantValue属性,用来表示一个常量字段的值。具体见2.9
具体说明:
字段名称 | 类型 | 值 | 说明 |
---|---|---|---|
access_flags |
u2 | 用于定义当前方法的访问权限和基本属性的掩码标志 | |
name_index |
u2 | 必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,它要么表示初始化方法的名字(或),要么表示一个方法的有效的非全限定名 | |
descriptor_index |
u2 | 必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示一个有效的方法的描述符 | |
attributes_count |
u2 | 方法属性个数 | |
attributes[attributes_count] |
attribute_info |
attributes表的每一个成员的值必须是attribute结构,一个方法可以有任意个关联属性 |
类型名称 | 十六进制值 | 含义 | 解释说明 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | public,方法可以从包外访问 | |
ACC_PRIVATE | 0x0002 | private,方法仅能该类自身调用 | |
ACC_PROTECTED | 0x0004 | protected,方法可以被子类调用 | |
ACC_STATIC | 0x0008 | static,表示静态方法 | |
ACC_FINAL | 0x0010 | final,方法不能被重写(覆盖) | |
ACC_SYNCHRONIZED | 0x0020 | synchronized,方法由管程同步 | |
ACC_BRIDGE | 0x0040 | bridge,方法由编译器产生 | |
ACC_VARARGS | 0x0080 | 表示方法带有变长参数 | 说明方法在源码层的参数列表是否变长的。如果是变长的,则在编译时,方法的ACC_VARARGS标志设置1,其余的方法ACC_VARARGS标志设置为0 |
ACC_NATIVE | 0x0100 | native,方法引用非java语言的本地方法 | |
ACC_ABSTRACT | 0x0400 | abstract,方法没有具体实现 | |
ACC_STRICTFP | 0x0800 | strictfp,方法使用FP-strict浮点格式 | |
ACC_SYNTHETIC | 0x1000 | 方法在源文件中不出现,由编译器产生 | 表明这个方法是由编译器自己产生的而不是我们编写的代码生成的 |
方法属性表是method_info结构的最后一部分。前面介绍了方法的访问标记和方法签名,还有一些重要的信息没有出现,如方法声明抛出的异常,方法的字节码,方法是否被标记为deprecated等,属性表就是用来存储这些信息的。与方法相关的属性有很多,其中比较重要的是Code和Exceptions属性,其中Code属性存放方法体的字节码指令,Exceptions属性用于存储方法声明抛出的异常。
我们来看下上面的HelloWorld
的二进制文件,来分析下:
从红色方框开始表示的就是方法啦0002
这两个字节代表的就是这个类有2个方法,然后 黄色方框标记的0001
~绿色方框0009
之前描述的都是一个方法,我们来仔细瞅瞅.
第一个方法:
0001
表示的是方法的access_flag类型。这个表示的是public类型0007
表示的是name_index
,也就是说 在常量池index=7的位置放的是这个方法的名称,由上面信息可知,这个方法名是"init"0008
表示的是descriptor_index
方法描述符的index,也就是说在常量池index=8的位置放的是这个方法的描述符,由上面信息可知道,这个方法的描述符是“()V”0001
表示的是方法的attributes_count ,代表这个方法有1个属性,接下来的二进制就是讲解 方法属性的了0009
表示的是attribute_name_index
也就是说在常量池indeex=9的位置方式的是这个方法属性的名称,由上面信息可知,这个是“Code”(这个的含义后面讲解)0000002f
表示的是属性的长度,也就是说这个方法有47个属性,也就是这个后面的47个字节用来代表这个方法的属性再来看第二个方法
0009
表示的是方法的access_flag类型。这个表示的是static、public类型000e
表示的是name_index
,也就是说 在常量池index=14的位置放的是这个方法的名称,由上面信息可知,这个方法名是"main"000f
表示的是descriptor_index
方法描述符的index,也就是说在常量池index=15的位置放的是这个方法的描述符,由上面信息可知道,这个方法的描述符是“([Ljava/lang/String;)V”0001
表示的是方法的attributes_count ,代表这个方法有1个属性,接下来的二进制就是讲解 方法属性的了0009
表示的是attribute_name_index
也就是说在常量池indeex=9的位置方式的是这个方法属性的名称,由上面信息可知,这个是“Code”(这个的含义后面讲解)00000037
表示的是属性的长度,也就是说这个方法有56个属性,也就是这个后面的47个字节用来代表这个方法的属性,也就是上面标蓝色的那部分数据在方法表之后的结构是class文件的最后一部分——属性表。属性出现的地方比较广泛,不只出现在字段和方法中,在顶层的class文件中也会出现。相比于常量池只有14种固定的类型,属性表的类型更加灵活,不同的虚拟机实现厂商可以自定义属性,属性表的结构:
字段名称 | 类型 | 值 | 说明 |
---|---|---|---|
attribute_name_index |
u2 | 必须是对当前Class文件的常量池的有效16位无符号索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示当前属性的名字 | |
attribute_length |
u4 | 给出了跟随其后的字节的长度,这个长度不包括attribute_name_index和attribute_name_index项的6个字节。 | |
info[attribute_length] |
u1 |
我们来看下我们一直用的HelloWorld
的最后一部分数据:
0001
代表属性的个数,也就是这个类只有1个属性0012
代表的是attribute_name_index
,也就是在常量池中index=18的地方存储的是属性的名称,(由上面信息可知,这个是"SourceFile")00000002
这4个字节代表的是属性对应的字节长度,也就是说这个属性长度为2个字节0013
的这两个字节就是属性的内容在常量池中对应的index;也就是在常量池中index=19的地方存储的是“SourceFile”对应的源文件的名字(由上面信息可知,index=19对应的就是“HelloWorld.java”)到这,简单的HelloWorld的class文件我们就全部分析完了。
虚拟机预定义了20多种属性。分别是:
属性名称 | 首次出现的SE版本 | 首次出现的Class文件版本 |
---|---|---|
属性名称 | 首次出现的SE版本 | 首次出现的Class文件版本 |
ConstantValue |
1.0.2 | 45.3 |
Code |
1.0.2 | 45.3 |
StackMapTable |
6 | 50.0 |
Exceptions |
1.0.2 | 45.3 |
InnerClasses |
1.1 | 45.3 |
EnclosingMethod |
5.0 | 49.0 |
Synthetic |
1.1 | 45.3 |
Signature |
5.0 | 49.0 |
SourceFile |
1.0.2 | 45.3 |
SourceDebugExtension |
5.0 | 49.0 |
LineNumberTable |
1.0.2 | 45.3 |
LocalVariableTable |
1.0.2 | 45.3 |
LocalVariableTypeTable |
5.0 | 49.0 |
Deprecated |
1.1 | 45.3 |
RuntimeVisibleAnnotations |
5.0 | 49.0 |
RuntimeInvisibleAnnotations |
5.0 | 49.0 |
RuntimeVisibleParameterAnnotations |
5.0 | 49.0 |
RuntimeInvisibleParameterAnnotations |
5.0 | 49.0 |
AnnotationDefault |
5.0 | 49.0 |
BootstrapMethods |
7 | 51.0 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示字符串“ConstantValue”。 |
attribute_length |
u4 | 固定为2 |
constantvalue_index |
u2 | 必须是一个对常量池的有效索引。常量池在该索引处的项给出该属性表示的常量值。 |
Table 4.7. Constant value attribute types
Field Type | Entry Type |
---|---|
long |
CONSTANT_Long |
float |
CONSTANT_Float |
double |
CONSTANT_Double |
int , short , char , byte , boolean |
CONSTANT_Integer |
String |
CONSTANT_String |
我们来通过一个例子看看
package jvm.classLib.attributeTest;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/4/2 3:08 下午
*/
public class ConstantValueTest {
private static int age = 18;
public static void main(String[] args) {
System.out.println(age);
}
}
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示字符串“Code”。 |
attribute_length |
u4 | 表示当前属性的长度,不包括开始的6个字节 |
max_stack |
u2 | 给出了当前方法的操作数栈在运行执行的任何时间点的最大深度 这个字段存储为2个字节,所以方法栈帧中的操作数栈的大小深度为65535 此外:每个long和double类型的数据被认为占用max_locals中的两个单元,所以使用这些类型时,操作数栈的限制的最大值就会相应地减少 |
max_locals |
u2 | 给出了分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量。long和double型的局部变量的最大索引是max_locals-2,其它类型的局部变量的最大索引是max_locals-1. 这个字段存储为2个字节,所以方法调用时创建的栈帧的局部变量表中的最大局部变量个数65535个 |
code_length |
u4 | 给出了当前方法的code[]数组的字节数,code_length的值必须大于0,即code[]数组不能为空 |
code[code_length] |
u1 | 给出了实现当前方法的Java虚拟机字节码。code[]数组以按字节寻址的方式读入机器内存,如果code[]数组的第一个字节是按以4字节边界对齐的话,那么tableswitch和lookupswitch指令中所有涉及到的32位偏移量也都是按4字节长度对齐的 |
exception_table_length |
u2 | 给出了exception_table[]数组的成员个数量 |
exception_table[exception_table_length] |
exception_info | 异常处理器,值的顺序有含义,不能随意修改 |
attributes_count |
u2 | 给出了Code属性中attributes表的成员个数 |
attributes[attributes_count] |
attribute_info |
成员只能是LineNumberTable,LocalVariableTable,LocalVariableTypeTable和StackMapTable属性 这三者 |
我们来看上面的HelloWorld的第二个main方法的code信息
从黄色框的0009
开始的蓝色框表示的就是Code的内容,我们来具体分析下:
0009
的这两个字节就是index=9在常量池中存储的就是"Code"0000 0037
这四个字节存储的就是当前属性的长度,也就是后面的55个字节都是这个Code的内容0002
的两个字节代表的是max_stack
,也就是操作数栈的最大深度是2,方法执行的任意期间操作数栈的深度都不会超过这个值。它的计算规则是:有入栈的指令stack增加,有出栈的指令stack减少,在整个过程中stack的最大值就是max_stack的值,增加和减少的值一般都是1,但也有例外:LONG和DOUBLE相关的指令入栈stack会增加2,VOID相关的指令则为0。0001
的两个字节代表的是max_locals
,是局部变量表的大小,它的值并不等于方法中所有局部变量的数量之和。当一个局部作用域结束,它内部的局部变量占用的位置就可以被接下来的局部变量复用了。0000 0009
的四个字节代表的是code_length
,表示字节码指令的长度;code是一个长度为code_length的字节数组,存储真正的字节码指令;也就是说接下来的9个字节表示的是真正的字节码指令b200 0212 03b6 0004 b1
表示的就是真正的字节码指令00 00
的两个字节代表的是exception_table_length
,异常表长度,为000 02
的两个字节代表的就是 Code的属性个数,也就是Code有两个属性:
000a
表示的是第一个属性的attribute_name_index
,也就是index=10在常量池中存储的是这个属性的名称,也就是“LineNumberTable”00 00 00 0a
四个字节表示的是属性的长度,也就是"LineNumberTable"这个属性长10个字节,00 0200 0000 0a00 0800 0b
是代表“LineNumberTable”属性的内容.000b
代表的是第一个属性的attribute_name_index
,也就是index=11在常量池中存储的是这个属性的名称,也就是“LocalVariableTable”0000 000c
代表的是属性的长度,也就是说属性长度是12.00 0100 0000 0900 1000 1100 00
就是“LocalVariableTable”属性的内容。字段名称 | 字段类型 | 字段说明 |
---|---|---|
start_pc |
u2 | start_pc和end_pc两项的值表明了异常处理器在code[]数组中的有效范围。start_pc必须是对当前code[]数组中某一指令的操作码的有效索引,end_pc要么是对当前code[]数组中某一指令的操作码的有效索引,要么等于code_length的值,即当前code[]数组的长度。start_pc的值必须比end_pc小。当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设x为异常句柄的有效范围内的值,x满足:start_pc ≤ x < end_pc。 |
end_pc |
u2 | |
handler_pc |
u2 | 表示一个异常处理器的起点,它的值必须同时是一个对当前code[]数组中某一指令的操作码的有效索引。 |
catch_type |
u2 | 如果catch_type项的值不为0,那么它必须是对常量池的一个有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示当前异常处理器指定需要捕捉的异常类型。只有当抛出的异常是指定的类或其子类的实例时,异常处理器才会被调用。如果catch_type项的值如果为0,那么这个异常处理器将会在所有异常抛出时都被调用。这可以用于实现finally语句 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是对常量池的有效索引,常量池在该索引的项处必须是CONSTANT_Utf8_info结构,表示“StackMapTable”字符串。 |
attribute_length |
u4 | 表示当前属性的长度,不包括开始的6个字节 |
number_of_entries |
u2 | 给出了entries表中的成员数量。Entries表的每个成员是都是一个stack_map_frame结构的项 |
entries[number_of_entries] |
stack_map_frame |
给出了当前方法所需的stack_map_frame结构 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串"Exceptions" |
attribute_length |
u4 | 给出了当前属性的长度,不包括开始的6个字节。 |
number_of_exceptions |
u2 | exception_index_table[]数组中成员的数量。 |
exception_index_table[number_of_exceptions] |
u2 | 每个成员的值都必须是对常量池的有效索引。常量池在这些索引处的成员必须都是CONSTANT_Class_info结构,表示这个方法声明要抛出的异常的类的类型。 一个方法如果要抛出异常,必须至少满足下列三个条件中的一个: 要抛出的是RuntimeException或其子类的实例。 要抛出的是Error或其子类的实例。 要抛出的是在exception_index_table[]数组中申明的异常类或其子类的实例。 这些要求没有在Java虚拟机中进行强制检查,它们只在编译时进行强制检查 |
我们来看个例子
package jvm.classLib.attributeTest;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/4/2 4:23 下午
*/
public class ExceptionTest {
private void test() throws Exception {
throw new Exception("test throw exception");
}
public static void main(String[] args) {
}
}
看到在方法里有Exceptions 表,表内有详细信息。
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是一个对常量池的有效引用。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串"InnerClasses" |
attribute_length |
u4 | 给出了这个属性的长度,不包括前6个字节 |
number_of_classes |
u2 | 表示classes[]数组的成员数量。 |
classes[number_of_classes] |
inner_classes_info | 每个成员为inner_classes_info类型常量池中的每个CONSTANT_Class_info结构如果表示的类或接口并非某个包的成员,则每个类或接口在classes[]数组中都有一个成员与之对应。 如果Class中包含某些类或者接口,那么它的常量池(以及对应的InnerClasses属性)必须包含这些成员,即使某些类或者接口没有被这个Class使用过。 这条规则暗示着内部类或内部接口成员在其被定义的外部类(Enclosing Class)中都会包含有它们的InnerClasses信息。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
inner_class_info_index |
u2 | 内部类信息在常量池中的索引 |
outer_class_info_index |
u2 | 外部类信息在常量池中的索引 |
inner_name_index |
u2 | 内部类名称在常量池中的索引,匿名类则这个值为0 |
inner_class_access_flags |
u2 | 表示内部类的访问权限及基础属性 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示字符串"EnclosingMethod"。 |
attribute_length |
u4 | 值固定为4 |
class_index |
u2 | 必须是一个对常量池的有效索引。常量池在该索引出的项必须是CONSTANT_Class_info结构,表示包含当前类声明的最内层类。 |
method_index |
u2 | 如果当前类不是在某个方法或初始化方法中直接包含(Enclosed),那么method_index值为0,否则method_index项的值必须是对常量池的一个有效索引,常量池在该索引处的成员必须是CONSTANT_NameAndType_info结构,表示由class_index属性引用的类的对应方法的方法名和方法类型。Java编译器有责任在语法上保证通过method_index确定的方法是语法上最接近那个包含EnclosingMethod属性的类的方法(Closest Lexically Enclosing Method)。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是对常量池的一个有效索引,常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“Synthetic”。 |
attribute_length |
u4 | 固定为0 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 必须是一个对常量池的有效索引。常量池在索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“Signature”。 |
attribute_length |
u4 | 固定为2 |
signature_index |
u2 | 必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名或方法类型签名或字段类型签名:如果当前的Signature属性是ClassFile结构的属性,则这个结构表示类签名,如果当前的Signature属性是method_info结构的属性,则这个结构表示方法类型签名,如果当前Signature属性是field_info结构的属性,则这个结构表示字段类型签名。 |
这个其实对应到我们的代码中就是这类的范型,我们来举例子看看
例子一:范型在属性上
package jvm.classLib;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/3/23 9:47 下午
*/
public class SignatureClassTest<T> {
private T methodName;
public void print(T value){
}
public SignatureClassTest(T methodName) {
}
public static void main(String[] args) {
System.out.println(
SignatureClassTest.class.getName());
}
}
可以看到在字段里有Signature
,然后里面有属性信息和签名信息。我们再来看范型在类上的
这块存储了范型类型,且是继承Object类型的;这块有个“范型擦除”的黑科技,我们后文再说
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“SourceFile”。 |
attribute_length |
u4 | 固定为2 |
sourcefile_index |
u2 | 必须是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示一个字符串。sourcefile_index项引用字符串表示被编译的Class文件的源文件的名字。不包括源文件所在目录的目录名,也不包括源文件的绝对路径名。平台相关(绝对路径名等)的附加信息必须是运行时解释器(Runtime Interpreter)或开发工具在文件名实际使用时提供。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“SourceDebugExtension”。 |
attribute_length |
u4 | 给出了当前属性的长度,不包括开始的6个字节。即attribute_length项的值是字节数组debug_extension[]数组的长度 |
debug_extension[attribute_length] |
u1 | 用于保存扩展调试信息,扩展调试信息对于Java虚拟机来说没有实际的语义。这个信息用改进版的UTF-8编码的字符串表示,这个字符串不包含byte值为0的终止符。需要注意的是,debug_extension[]数组表示的字符串可以比Class实例对应的字符串更长。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“LineNumberTable”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
line_number_table_length |
u2 | line_number_table[]数组的成员个数。 |
line_number_table[line_number_table_length] |
line_number_info | 每个成员都表明源文件中行号的变化在code[]数组中都会有对应的标记点 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
start_pc |
u2 | start_pc项的值必须是code[]数组的一个索引,code[]数组在该索引处的字符表示源文件中新的行的起点。start_pc项的值必须小于当前LineNumberTable属性所在的Code属性的code_length项的值。 |
line_number |
u2 | line_number项的值必须与源文件的行数相匹配 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“LocalVariableTable”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
local_variable_table_length |
u2 | local_variable_table[]数组的成员个数。 |
local_variable_table[local_variable_table_length] |
local_variable_info |
每一个成员表示一个局部变量的值在code[]数组中的偏移量范围。它同时也是用于从当前帧的局部变量表找出所需的局部变量的索引。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
start_pc |
u2 | 所有给定的局部变量的索引都在范围[start_pc, start_pc+length)中,即从start_pc(包括自身值)至start_pc+length(不包括自身值)。start_pc的值必须是一个对当前Code属性的code[]数组的有效索引,code[]数组在这个索引处必须是一条指令操作码。start_pc+length要么是当前Code属性的code[]数组的有效索引,code[]数组在该索引处必须是一条指令的操作码,要么是刚超过code[]数组长度的最小索引值。 |
length |
u2 | |
name_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示一个局部变量的有效的非全限定名 |
descriptor_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示一个局部变量类型的字段描述符 |
index |
u2 | index为此局部变量在当前栈帧的局部变量表中的索引。如果在index索引处的局部变量是long或double型,则占用index和index+1两个索引。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“LocalVariableTypeTable”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
local_variable_type_table_length |
u2 | local_variable_type_table[]数组的成员个数。 |
local_variable_type_table[local_variable_type_table_length] |
local_variable_type_info |
每一个成员表示一个局部变量的值在code[]数组中的偏移量范围。它同时也是用于从当前帧的局部变量表找出所需的局部变量的索引。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
start_pc |
u2 | 所有给定的局部变量的索引都在范围[start_pc, start_pc+length)中,即从start_pc(包括自身)至start_pc+length(不包括自身)。start_pc的值必须是一个对当前Code属性的code[]数组的有效索引,code[]数组在这个索引处必须是一条指令操作码。start_pc+length要么是当前Code属性的code[]数组的有效索引,code[]数组在该索引处必须是一条指令的操作码,要么是刚超过code[]数组长度的最小索引值。 |
length |
u2 | |
name_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构表示一个局部变量的有效的非全限定名 |
signature_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构表示一个局部变量类型的字段签名 |
index |
u2 | index为此局部变量在当前栈帧的局部变量表中的索引。如果在index索引处的局部变量是long或double型,则占用index和index+1两个索引。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“Deprecated”。 |
attribute_length |
u4 | 固定为0 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“RuntimeVisibleAnnotations”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
num_annotations |
u2 | annotations[]数组的成员个数。 |
annotations[num_annotations] |
annotation |
每个成员的值表示一个程序元素的唯一的运行时可见注解 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
type_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构表示一个字段描述符,这个字段描述符表示一个注解类型,和当前annotation结构表示的注解一致 |
num_element_value_pairs |
u2 | 给出了当前annotation结构表示的注解的键值对(键值对的格式为:元素-值)的数量,即element_value_pairs[]数组成员数量。需要注意的是,在单独一个注解中可能含有数量最多为65535个键值对。 |
element_value_pairs[num_element_value_pairs] |
element_value_pairs_info |
每一个成员的值对应当前annotation结构表示的注解中的一个唯一的键值对。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
element_name_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示一个有效的字段描述符,这个字段描述符用于定义当前element_value_pairs的成员表示的注解的注解名 |
value |
element_value |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
tag |
u1 | 表明了当前注解的元素-值对的类型[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AuqRGCIZ-1617353140580)(https://wiki.corp.qunar.com/confluence/download/attachments/436912037/image2021-3-24_11-8-19.png?version=1&modificationDate=1616555300000&api=v2)] |
value | value_info | 表示当前注解元素的值。此项是一个联合体结构,当前项的tag项决定了这个联合体结构的哪一项会被使用 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
const_value_index |
const_value_index_info |
|
class_info_index |
u2 | 当tag项为’c’时,class_info_index项才会被使用。class_info_index项的值必须是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示返回描述符的类型,这个类型由当前element_value结构所表示的类型决定(譬如:'V’表示Void,'Ljava/lang/Object;'表示类java.lang.Object等)。 |
annotation_value |
annotation |
当tag项为’@'时,annotation_value项才会被使用。这时element_value结构表示一个内部的注解(Nested Annotation)。 |
array_value |
array_value_info |
当tag项为’['时,array_value项才会被使用 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
type_name_index |
u2 | 对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示一个有效的字段描述符,这个字段描述符表示当前element_value结构所表示的枚举常量类型的内部形式的二进制名称 |
const_name_index |
u2 | 是对常量池的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示一个有效的字段描述符,这个字段描述符表示当前element_value结构所表示的枚举常量类型的简单名称。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
num_values |
u2 | 给定了当前element_value结构表示的数组类型的成员的数量。注意,允许数组类型元素中最多有65535个元素 |
values[num_values] |
element_value |
每个成员的值都给指明了当前element_value 结构所表示的数组类型的一个元素值。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“RuntimeInvisibleAnnotations”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
num_annotations |
u2 | annotations[]数组的成员个数。 |
annotations[num_annotations] |
annotation |
annotations表的每个值表示程序元素上的单个运行时不可见注释。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“RuntimeVisibleParameterAnnotations”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
num_parameters |
u1 | parameter_annotations[]数组的成员个数。 |
parameter_annotations[num_parameters] |
parameter_annotations_info |
每个成员的值表示一个的参数的所有的运行时可见注解。它们的顺序和方法描述符表示的参数的顺序一致 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
num_annotations |
u2 | 表示parameter_annotations[]数组在当前所索引处的元素的可见注解的数量 |
annotations[num_annotations] |
annotation |
每个成员的值表示parameter_annotations[]数组在当前索引处的元素的一个唯一的可见注解 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“RuntimeInvisibleParameterAnnotations”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
num_parameters |
u1 | parameter_annotations[]数组的成员个数。 |
parameter_annotations[num_parameters] |
parameter_annotations_info |
每个成员的值表示一个的参数的所有的运行时不可见注解。它们的顺序和方法描述符表示的参数的顺序一致 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“AnnotationDefault”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
default_value |
element_value | 表示AnnotationDefault属性所对应注解类型元素的默认值 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
字段名称 | 字段类型 | 字段说明 |
attribute_name_index |
u2 | 是一个对常量池的有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,表示字符串“BootstrapMethods”。 |
attribute_length |
u4 | 当前属性的长度,不包括开始的6个字节。 |
num_bootstrap_methods |
u2 | bootstrap_methods[]数组的成员个数。 |
bootstrap_methods[num_bootstrap_methods] |
bootstrap_methods_info |
每个成员包含一个指向CONSTANT_MethodHandle结构的索引值,它代表了一个引导方法。还包含了这个引导方法静态参数的序列(可能为空)。 |
字段名称 | 字段类型 | 字段说明 |
---|---|---|
bootstrap_method_ref |
u2 | 是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构。注意:此CONSTANT_MethodHandle_info结构的reference_kind项应为值6(REF_invokeStatic)或8(REF_newInvokeSpecial),否则在invokedynamic指令解析调用点限定符时,引导方法会执行失败 |
num_bootstrap_arguments |
u2 | bootstrap_arguments[]数组成员个数 |
bootstrap_arguments[num_bootstrap_arguments] |
u2 | 每个成员必须是一个对常量池的有效索引。常量池在该索引出必须是下列结构之一:CONSTANT_String_infoCONSTANT_Class_infoCONSTANT_Integer_infoCONSTANT_Long_infoCONSTANT_Float_infoCONSTANT_Double_infoCONSTANT_MethodHandle_infoCONSTANT_MethodType_info |
由于篇幅的原因,在属性表章节中我并没有列出例子&进行分析例子,若小伙伴有需要,可以自己动手写一写,然后比对着这个文章&二进制文件进行查找验证。
这篇文章通过简单的案例来给大家讲解Class文件具体是长啥样的,里面的二进制符合都代表啥含义;有了这个后我们再来看JVM是如何处理这个解析处理这个Class文件并进行执行的。具体内容欢迎看下一篇文章~