深入理解JVM(9)——字节码文件结构(2)

接上一篇

目录

3.4 访问标志

3.5 类索引

3.6 父类索引

3.7 接口索引集合

3.8 字段表集合

3.9 方法表集合

3.10 属性表集合

(1)Code(字节码指令)

(2)Exceptions(异常表)

(3)LineNumberTable

(4)LocalVariableTable

(5)SourceFile

(6)ConstantValue

(7)InnerClasses

(8)Deperecate及Synthetic

(9)StackMapTable

(10)Signature

(11)BootstrapMethods

4.使用Idea的插件来查看字节码文件的结构

5.示例字节码文件的最终结构划分图


3.4 访问标志

  • 在常量池之后,紧接着的2个字节代表访问标志
  • 访问标志的作用:
    • 用于识别一些类或者接口层次的访问信息,包括
      • 这个class是类还是接口
      • 是否定义为public
      • 是否为abstract类型
      • 如果是类的话,是否被声明为final等
  • 2个字节有16位可以用,但JVM规范中目前只定义了8种标志,只使用到了其中8位,标志如下:
标志名称标志二进制值标志十六进制值标志含义
ACC_PUBLIC0000 0000 0000 00010x0001是否为public
ACC_FINAL0000 0000 0001 00000x0010是否被声明为final,只有类可设置
ACC_SUPER0000 0000 0010 00000x0020

是否允许使用invokespecial字节码指令的新语义

(invokespecial指令的语意在JDK1.02发生过改变,为了区别这条指令使用哪个语意,JDK1.02之后编译出来的类的这个标志位都必须为真

ACC_INTERFACE0000 0010 0000 00000x0200标识这是一个接口
ACC_ABSTRACT0000 0100 0000 00000x0400是否为abstract类型,对于接口或者抽象类来说,此标志为真,其他类值为假
ACC_SYNTHETIC0001 0000 0000 00000x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0010 0000 0000 00000x2000标识这是一个注解
ACC_ENUM0100 0000 0000 00000x4000标识这是一个枚举

 

 

 

 

 

 

 

 

 

 

 

 

示例中这两个字节为:

为什么表中没有呢?

  • 其实该两个字节是当前类满足的多个标志相与的结果
  • 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.字段名称
  • 其实通过前面的结构的表示,我们也可以想到,对于修饰符我们可以参考类的修饰符,使用标志位来表示;而字段的数据类型和字段名称通过索引在常量池中获取

所以最终字段表集合的结构如下:

类型 名称数量
u2访问标志1
u2字段名称索引1
u2描述字段类型的索引1
u2额外属性的个数(attribute_count)1
attribute_info额外的属性集合attribute_count

 

 

 

 

 

 

接下来对上述结构一个一个进行解释:

访问标志

标志名称标志二进制值标志十六进制值标志含义
ACC_PUBLIC0000 0000 0000 00010x0001字段是否为public
ACC_PRIVATE0000 0000 0000 00100x0002字段是否为private
ACC_PROTECTED0000 0000 0000 01000x0004字段是否为protected
ACC_STATIC0000 0000 0000 10000x0008字段是否为static
ACC_FINAL0000 0000 0001 00000x0010字段是否为final
ACC_VOLATILE0000 0000 0100 00000x0040字段是否为volatile
ACC_TRANSIENT0000 0000 1000 00000x0080字段是否为transient
ACC_SYNTHETIC0001 0000 0000 00000x1000字段是否由编译器自动生成
ACC_ENUM0100 0000 0000 00000x4000字段是否是一个枚举

 

 

 

 

 

 

 

 

 

 

  • 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_PUBLIC0000 0000 0000 00010x0001方法是否为public
ACC_PRIVATE0000 0000 0000 00100x0002方法是否为private
ACC_PROTECTED0000 0000 0000 01000x0004方法是否为protected
ACC_STATIC0000 0000 0000 10000x0008方法是否为static
ACC_FINAL0000 0000 0001 00000x0010方法是否为final
ACC_SYNCHRONIZED0000 0000 0010 00000x0020方法是否为synchronized
ACC_BRIDGE0000 0000 0100 00000x0040方法是否由编译器产生的桥接方法
ACC_VARARGS0000 0000 1000 00000x0080方法是否接受不定参数
ACC_NATIVE0000 0001 0000 00000x0100方法是否为native
ACC_ABSTRACT0000 0100 0000 00000x0400方法是否为abstract
ACC_STRICTFP0000 1000 0000 00000x0800方法是否为strictfp
ACC_SYNTHETIC0001 0000 0000 00000x1000字段是否由编译器自动生成

 

 

 

 

 

 

 

 

 

 

 

 

 

接下来我们按照它的结构解析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内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性局部变量表
StackMapTableCode属性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
u2max_stack1
u2max_locals1
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.示例字节码文件的最终结构划分图

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值