接上一篇
目录
3.4 访问标志
- 在常量池之后,紧接着的2个字节代表访问标志
- 访问标志的作用:
- 用于识别一些类或者接口层次的访问信息,包括
- 这个class是类还是接口
- 是否定义为public
- 是否为abstract类型
- 如果是类的话,是否被声明为final等
- 用于识别一些类或者接口层次的访问信息,包括
- 2个字节有16位可以用,但JVM规范中目前只定义了8种标志,只使用到了其中8位,标志如下:
标志名称 | 标志二进制值 | 标志十六进制值 | 标志含义 |
ACC_PUBLIC | 0000 0000 0000 0001 | 0x0001 | 是否为public |
ACC_FINAL | 0000 0000 0001 0000 | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0000 0000 0010 0000 | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 (invokespecial指令的语意在JDK1.02发生过改变,为了区别这条指令使用哪个语意,JDK1.02之后编译出来的类的这个标志位都必须为真 |
ACC_INTERFACE | 0000 0010 0000 0000 | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0000 0100 0000 0000 | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志为真,其他类值为假 |
ACC_SYNTHETIC | 0001 0000 0000 0000 | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0010 0000 0000 0000 | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0100 0000 0000 0000 | 0x4000 | 标识这是一个枚举 |
示例中这两个字节为:
为什么表中没有呢?
- 其实该两个字节是当前类满足的多个标志相与的结果
- 0x0021就是ACC_PUBLIC | ACC_SUPER, 即0x0001 | 0x0020 =0x0021
3.5 类索引
- 紧接着2个字节是类的索引,即在常量池中的存放类的符号引用的位置
- 类索引是一个u2类型的数据
- 作用:
- 用来确定这个类的全限定类名
示例中接下来为(3)
- 即表示在常量池的第3个元素处
- 表示此类的全限定类名为ByteCode/ByteCodeDemo01
3.6 父类索引
- 紧接着2个字节是当前类的父类的索引,即在常量池中的存放父类的符号引用的位置
- 父类索引也是一个u2类型的数据
- 由于Java只支持单继承,所以父类索引只有一个
- 作用:
- 用来确定当前类的父类的全限定类名
示例中接下来为(4)
- 即表示在常量池的第4个元素处
- 表示此类的父类的全限定类名为java/lang/Object
3.7 接口索引集合
- 紧接着是接口索引集合(接口计数器+按照顺序排列的接口的索引(即u2类型的索引,指向常量池中对应接口的符号引用的位置))
- 第一项为u2类型的数据作为接口计数器
- 作用:
- 用于描述这个类实现了哪些接口
- 接口排列顺序:
- 被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口计数器的后面
示例中接口计数器为
- 表示此类未实现任何接口,后面就没有接口索引集合
3.8 字段表集合
- 字段个数(u2类型)+字段表集合
所以示例中接下来的2个字节为,表示字段表集合中有1个字段
- 字段表用于描述类和接口中声明的变量,这里的字段包含了(类变量即static变量,以及实例变量,但不包括方法内部声明的局部变量)
- Java中描述一个字段所包含的信息:
- 1.修饰符
- 字段的作用域(public、private、protected)
- 是实例变量还是类变量(static)
- 是否可变(final)
- 并发可见性(volatile)
- 可否被序列化(transient)
- 2.字段的数据类型(基本类型、对象、数组)
- 3.字段名称
- 1.修饰符
- 其实通过前面的结构的表示,我们也可以想到,对于修饰符我们可以参考类的修饰符,使用标志位来表示;而字段的数据类型和字段名称通过索引在常量池中获取
所以最终字段表集合的结构如下:
类型 | 名称 | 数量 |
u2 | 访问标志 | 1 |
u2 | 字段名称索引 | 1 |
u2 | 描述字段类型的索引 | 1 |
u2 | 额外属性的个数(attribute_count) | 1 |
attribute_info | 额外的属性集合 | attribute_count |
接下来对上述结构一个一个进行解释:
访问标志
标志名称 | 标志二进制值 | 标志十六进制值 | 标志含义 |
ACC_PUBLIC | 0000 0000 0000 0001 | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0000 0000 0000 0010 | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0000 0000 0000 0100 | 0x0004 | 字段是否为protected |
ACC_STATIC | 0000 0000 0000 1000 | 0x0008 | 字段是否为static |
ACC_FINAL | 0000 0000 0001 0000 | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0000 0000 0100 0000 | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0000 0000 1000 0000 | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0001 0000 0000 0000 | 0x1000 | 字段是否由编译器自动生成 |
ACC_ENUM | 0100 0000 0000 0000 | 0x4000 | 字段是否是一个枚举 |
- ACC_PUBLIC、ACC_PROTECTED、ACC_PRIVATE这三个标志只能选择其中一个
- ACC_FINAL、ACC_VOLATILE不能同时选择
- 接口中的字段必须由ACC_PUBLIC、ACC_FINAL、ACC_STATIC
- 同理该两个字节最终的结果,也是所有标志相与的结果
示例中再接下来的2个字节表示标志,即
- 表示该字段为private修饰的
字段名称索引
- 表示字段名称为常量池中第5个元素
- 字段名称为a
描述字段类型的索引
- 表述描述字段类型的索引为常量池中第6个元素
- 字段的类型为int类型
额外属性的个数
- 额外属性的个数为0
- 所以后面就没有额外属性,此字段到此结束
3.9 方法表集合
- 方法个数(u2类型)+方法表集合
接下来2个字节为方法个数,即表示方法集合中有3个方法,我们可以看到源程序中我写了2个方法,编译器还帮我们自动生成了1个构造方法
与上述字段表集合类似,方法表集合也有它自己的结构,如下:
类型 | 名称 | 数量 |
u2 | 访问标志 | 1 |
u2 | 方法名称索引 | 1 |
u2 | 描述方法的索引 | 1 |
u2 | 额外属性的个数(attribute_count) | 1 |
attribute_info | 额外的属性集合 | attribute_count |
访问标志
标志名称 | 标志二进制值 | 标志十六进制值 | 标志含义 |
ACC_PUBLIC | 0000 0000 0000 0001 | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0000 0000 0000 0010 | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0000 0000 0000 0100 | 0x0004 | 方法是否为protected |
ACC_STATIC | 0000 0000 0000 1000 | 0x0008 | 方法是否为static |
ACC_FINAL | 0000 0000 0001 0000 | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0000 0000 0010 0000 | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0000 0000 0100 0000 | 0x0040 | 方法是否由编译器产生的桥接方法 |
ACC_VARARGS | 0000 0000 1000 0000 | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0000 0001 0000 0000 | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0000 0100 0000 0000 | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0000 1000 0000 0000 | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0001 0000 0000 0000 | 0x1000 | 字段是否由编译器自动生成 |
接下来我们按照它的结构解析3个方法:
第1个方法:
- 访问标志:
- 表示该方法是public的
- 方法名称索引
- 表示该方法的名称是常量池中第7个元素
- 表示该方法是构造方法
- 描述方法的索引
- 表示对该方法的描述在常量池中第8个元素
- 表示该方法是一个空参并且空返回值的方法
- 额外属性的个数
- 属性表集合中有1个属性
- 额外的属性集合
- 属性名在常量池中的索引
- 在常量池的第9个元素
- 表示该属性是Code属性
- 属性的长度
- 属性的长度为56个字节
- 属性名在常量池中的索引
3.10 属性表集合
- 属性表在字节码文件中出现在三个位置:(从前面我们可以看到两个)
- 字段表携带自己的属性表集合
- 方法表携带自己的属性表集合
- class文件的最后是整个class文件的属性表集合
属性表的结构:
类型 | 名称 | 数量 |
u2 | 属性名索引 | 1 |
u4 | 属性长度 | 1 |
u1 | 属性集合 | 属性长度 |
JavaSE7之后,预定义的属性有以下21种:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 异常表 |
EnclosingMethod | 类 | 仅当一个类为局部类或者匿名类的时候才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 局部变量表 |
StackMapTable | Code属性 | JDK1.6新增的属性,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | 记录泛型签名信息 |
SourceFile | 类 | 记录源文件的名称 |
SourceDebugExtension | 类 | 存储额外的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | 为动态注解提供支持,用于指明哪些注解是运行时可见的(实际运行时就是进行反射调用) |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | 用于指明哪些注解是运行时可见的,作用于方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | 用于指明哪些注解是运行时不可见的,作用于方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokedynamic指令引用的引导方法限定符 |
以下是部分属性的详解
(1)Code(字节码指令)
- Java程序方法体中的代码经过Javac编译处理后,最终变成字节码指令存储在Code属性内
- Code属性出现在方法表的属性当中,但并非所有的方法表都必须存在这个属性,比如接口或者抽象类中的抽象方法就不存在Code属性
Code属性的结构:
类型 | 名称 | 数量 |
u2 | 属性名称索引 | 1 |
u4 | 属性值的长度 | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | 字节码指令长度 | 1 |
u1 | 字节码指令 | 字节码指令长度 |
u2 | 异常表长度 | 1 |
异常集合 | 异常表 | 异常表长度 |
u2 | 属性数量 | 1 |
属性集合 | 属性 | 属性数量 |
属性名称索引
- 指向常量池中的一个CONSTANT_Utf8_info型常量
- 常量的固定值为Code
属性值的长度(attribute_length)
- 即此结构之后还有attribute_length个字节属于Code属性表,
- 也即,属性值的长度 = 属性表长度 - 6(索引与长度本身占6个字节)
max_stack
- 代表操作数栈深度的最大值
- 在方法执行的任意时刻,操作数栈都不会超过这个深度
- 虚拟机运行的时候需要根据这个值来分配栈帧中的操作数栈深度
max_locals
- 代表局部变量表所需的存储空间
- max_locals的单位是Slot
- Slot是虚拟机为局部变量分配内存所使用的最小单位
- 对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot
- 对于double、long这两种64位的数据类型则需要两个Slot来存放
- 而reference的长度与实际使用32位还是64位虚拟机有关
- 方法参数(包括实例方法中的隐藏参数)、显式异常处理器的参数(即try-catch语句中catch块所定义的异常)、方法体中的局部变量都需要使用局部变量表来存放
- max_locals != 用到的局部变量所占的Slot之和
- 原因:局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域的时候,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小
字节码指令长度
- 指明接下来的字节码指令所占的长度
- 虽然它是一个u4类型的长度值,理论上最大值可以达到2^32 - 1,但是虚拟机规范中明确限制了,一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过了这个限制,javac编译器也拒绝编译
- 平常我们自己编写代码不太可能让一个方法的指令超出这个最大限制
- 但是在某些特殊情况下,如在编译一个很复杂的jsp文件的时候,某些jsp斌一起会把jsp内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败
字节码指令
- 此处其实是字节码指令的集合,而每个字节码指令占用1个字节
- 而这里实际存储的是一个字节,而每个字节码指令都会对应1个字节指令码,比如
- 当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解
- u1类型的取值范围在0x00~0xFF,对应十进制的0~255,也就是可以表达256条字节码指令,而目前Java虚拟机规范中只定义了其中约200条编码值对应的指令含义,具体可以查阅我的另一篇博客:https://blog.csdn.net/qq_34805255/article/details/99659323
(2)Exceptions(异常表)
(3)LineNumberTable
(4)LocalVariableTable
(5)SourceFile
(6)ConstantValue
(7)InnerClasses
(8)Deperecate及Synthetic
(9)StackMapTable
(10)Signature
(11)BootstrapMethods
4.使用Idea的插件来查看字节码文件的结构
5.示例字节码文件的最终结构划分图