JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码

前言:本文属于博主个人的学习笔记,博主也是小白。如果有不对的地方希望各位帮忙指出。本文主要还是我的学习总结,因为网上的一些知识分布比较零散故作整理叙述。如果有不对的地方,还请帮忙指正。本文不产生任何收益,如有出现禁止转载、侵权的图片,亦或者文字内容请联系我进行修改。


文章目录

  • JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码
  • 前言
  • 一、Class类文件结构
    • 1.1 class文件格式
      • 1.1.1 内部类与外部类的字节码文件
      • 1.1.2 字节码文件格式
    • 1.2 魔数及Class文件版本号
    • 1.3 常量池
    • 1.4 访问标记符
    • 1.5 类索引、父类索引与接口索引集合
    • 1.6 字段表集合
      • 1.6.1 字段表结构
      • 1.6.2 字段访问标志
      • 1.6.3 注意事项
    • 1.7 方法表集合
      • 1.7.1 方法表结构
      • 1.7.2 注意事项
    • 1.8 属性表集合
      • 1.8.1 属性表介绍
      • 1.8.2 属性表结构
      • 1.8.3 部分属性解读
        • 1.8.3.1 Code属性
        • 1.8.3.2 Exceptions属性
        • 1.8.3.3 LineNumberTable属性
        • 1.8.3.4 LocalVariableTable及LocalVariableTypeTable属性
        • 1.8.3.5 SourceFile属性
  • 二、示例:字节码文件阅读
    • 2.1 源文件
      • 2.1.1 文件代码
      • 2.1.2 字节码
      • 2.1.3 反编译
    • 2.2 魔数及版本号
    • 2.3 常量池
    • 2.4 访问标志 、类索引、父类索引及接口表
    • 2.4 方法表
  • 附录:
    • 虚拟机规范预定义的属性
    • Java虚拟机字节码指令表
  • 总结

前言

提示:本文分为两部分,第一部分中我们会先了解class类文件结构;第二部分我们会结合实例来进行解读。


参考:
  拭心:Java 基础巩固:内部类的字节码学习和实战使用场景:链接: link.

  昨夜星辰_zhangjg:深入理解Java Class文件格式(一):链接: link.

  祈祷ovo详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结):链接: link.

  Joy CR:Class类文件结构——访问标志:链接: link.

  亦山:《Java虚拟机原理图解》1.3、class文件中的访问标志、类索引、父类索引、接口索引集合:链接: link.

  四月葡萄 从一个class文件深入理解Java字节码结构:链接: link.

  波波烤鸭:Class文件结构介绍[属性表集合]:链接: link.

  图灵学院:全网最牛JVM字节码结构分析、Class类文件核心结构剖析、全网最清晰JVM常量池讲解、从字节码底层分析:链接: link.

  大宝11:Java字节码<init><clinit>的区别:链接: link.

食用建议:
   因为该博客写的又臭又长,所以各位看官结合自己需要查找的部分阅读即可,了解原理的盆友可以直接移步第二部分,对常量池、属性表不了解的盆友可以跳转相应部分。
   做好心理准备了么,我们开始了。

一、Class类文件结构

1.1 class文件格式

  首先我们要知道 任何一个Class文件都对应着唯一的一个类或接口的定义信息。
   当然你可能会疑问如果我可以在一个java程序中定义多个class,只声明一个public,这时候我的程序也是可以运行的,但里面有多个类,当我对这个程序编译的时候字节码文件怎么处理,还是只生成一个Class文件嘛?
   这里我们要区分清楚一个Java程序与一个类/接口的不同,那我们写些代码来看看当存在内部类和外部类的情况下,进行编译会发生什么事情。

1.1.1 内部类与外部类的字节码文件

   这分两种情况,第一种你定义的类是内部类,如下代码所示,总共有三个自定义类,其中两个为内部类。

public class InnerTest {private int n=0;static class Inner1{private static int i=0;}static class Inner2{private static int j=1;}public static void main(String[] args) {Inner1 inner1=new Inner1();Inner2 inner2=new Inner2();System.out.println(inner1.i);System.out.println(inner2.j);}
}

   javac编译结果如下,生成三个class字节码文件。

   如果是外部类,如下列代码,有一个外部定义的接口,以及该接口的实现类。

public class OuterDemo {public static void main(String[] args) {Outer1.method().show();Inter inter=()->{System.out.println("MyShow");};inter.show();}
}
interface Inter{void show();
}
class Outer1{static class InterImpl implements Inter{@Overridepublic void show() {System.out.println("Hello World");}}public static InterImpl method(){return new InterImpl();}
}

   编译后的字节码文件如下,注意此时InterImpl为Outer1的静态内部类,所以文件名为Outer1$InterImpl。

1.1.2 字节码文件格式

   Java良好的向后兼容性,很大一部分归功于Class文件结构的稳定性,Class文件是一组以字节为基础单位的二进制流,中间没有分隔符,所有内容都是必须的,Class文件中存储的数据类型共两种:无符号数:用来表示个数;表:用来记录具体数据。
   class文件格式如下所示,u1,u2等分别表示1个字节、2个字节等占位长度。
   这里大家可能比较疑惑的就是,为什么常量池表对应的常量个数是constant_pool_count-1,而不是constant_pool_count,这是因为我们的常量池必须空出一位给JVM,用于做空常量池标识.

类型 名称 数量
u4 magic (魔数) 1
u2 minor_version (次版本号) 1
u2 major_version(主版本号) 1
u2 constant_pool_count (常量池个数) 1
cp_info constant_pool (常量池表) constant_pool_count-1
u2 access_flags(访问标记符) 1
u2 this_class (本类) 1
u2 super_class (父类) 1
u2 interfaces_count (接口个数) 1
u2 interfaces (接口表) interfaces_count
u2 fields_count (字段个数) 1
field_info fields (字段表) field_count
u2 methods_count (方法个数) 1
method_info methods (方法表) methods_count
u2 attribute_count (属性个数) 1
attribute_info attributes(属性表) attributes_count

   我们依次来看一下表中的各数据介绍

1.2 魔数及Class文件版本号

   计算机一切数据都可以以二进制文件形式打开,包括你的各种文件。
   当你用执行程序打开后缀文件的时候,程序并非只根据文件名后缀来判别文件是否可执行,而是根据文件起头的魔数来判断。以java文件编译后的. class字节码文件为例,java虚拟机如果判断该文件是可执行的字节码文件?我们用Binary Viewer打开后,以16进制查看文件(4个二进制位),查看前8位(4字节,32位),就是class文件的魔数,CAFE BABE(咖啡宝贝)。这也是为什么Java语言图标是一杯咖啡的缘故。
   简单地来理解一下,魔数的作用就是来确定这个文件是否为能被虚拟机接受的Class文件。

                        图1
   跳过4个字节后,接下来的两个字节 00 00 是我们的次版本号,次版本号虽然是最初定义好的规范,但只在Java1.2版本之前被短暂使用过,一般固定为00 00.
   再跳两个字节 00 34 就是我们的主版本号转成10进制,值为52,对应着我们的JDK8.

1.3 常量池

   紧接着主次版本号之后的就是常量池入口,由于常量池中的常量个数并非固定的,所以我们用u2(两个字节 1个字节8bit 两个字节=4个16进制位,此后统一用u*表示)来记录我们的常量池个数,图1中 00 2E 表示该类共48个常量。
   注:由于u2最大值为65535,所以我们在Java程序中最多定义65535个常量。这个数在之后也会反复出现,因为诸如表示方法个数,接口个数等的占位大小也是u2.

   常量池的项目类型表如下.之后我们读常量池的时候会根据每个常量的tag值来查表判断是何种类型.

类型 标志 描述
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_MothodType_info 16 标志方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

  每个数据项类型都是一种数据结构,他们有着自己的结构体,拿CONSTANT_Utf8_info常量类型为例.

//CONSTANT_Utf8_info 类型的数据结构
class CONSTANT_Utf8_info{byte[] tag=new byte[1];  //u1 类型标识符,值固定为1byte[] length=new byte[2];  //u2 UTF-8编码的字符串占用的字节数byte[] bytes=new byte[1];  //u1 表示长度为length的UTF-8编码的字符串
}

常量池数据类型的结构总表如下,里面记录了每种类型的tag值,以及其对应的数据结构。

  此外常量池中常量可分作两大类:字面量(Literal)和符号引用(Symbolic References).字面量比较接近于Java语言层面的常量概念,如文本字符串,被声明为final的常量值等,而符号引用内部则是指向某些字面量型结构体的索引值.
  (因为涉及的表格以及图片太多了,摸鱼的博客选择采用他人的表格以及图片,引用文章的链接放在开头,侵删)

1.4 访问标记符

   常量池结束后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息.包括:这个Class是类还是接口,是否定义为public类,是否定义为abstract类型等等,同时**标志值是通过位运算得出的.**总共2字节16位,其中每位的取值如下所示。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为Public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM x4000 标志这是一个枚举

   比如ox0021=ox0020与ox0001进行位运算说的,查找下图可知,该类为public类型,同时该类允许使用invokespecial字节码指令的新语义.
   如果还是看不懂的朋友,建议去阅读一下这篇博客 《Java虚拟机原理图解》1.3、class文件中的访问标志、类索引、父类索引、接口索引集合(开头有博客链接) 此处不加赘述。

1.5 类索引、父类索引与接口索引集合

   类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件通过这三项数据来确定给类型的继承关系。类索引和父类索引指向的是常量池中的某个编号常量,通常指向的常量是UTF-8编码的字符串,用来描述全限定名,父类索引只有一个,所以Java语言类不支持多重继承,类的父类有且只有一个。同时,因为任何类都有父类,如果没有显示给出,编译器会默认继承Object作为该类的父类,所以所有Java类的父类索引都不为0
   接口索引集合就是用来描述这个类实现(implements)哪些接口的,如果这个Class文件表示的就是一个接口,则表示该接口可以继承(extends)的接口(类不能多重继承,接口可以)。按实现/继承的接口顺序从左到右排列在接口索引集合中。

1.6 字段表集合

1.6.1 字段表结构

   字段表(field_info)用于描述接口或者类中的声明的变量,Java语言中的字段(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表结构如下,其中 name_index、descriptor_index 是对常量池项的引用。
                     字段表结构

类型 名称 数量
u2 access_flags (权限修饰符) 1
u2 name_index (字段名称索引) 1
u2 descriptor_index (字段描述索引) 1
u2 attributes_count (属性表个数) 1
attribute_info attributes (属性表) attributes_count

1.6.2 字段访问标志

  字段可以包括的修饰符如下。修饰符有字段的作用域,是实例变量还是类变量(static),可变性(final),并发可见性(volatile,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
                字段访问标志

标志名称 标志值 含义
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_TRANSTENT 0x0080 字段是否为transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

  除了字段访问标志之外,还有修饰字段数据类型的描述符,比如L加对象全限定名描述对象类型,[[ 描述二维数组等。这里不展开赘述。而之后的属性表则用来存储一些额外的信息,字段表可以在属性表中附加描述0到多项的额外信息。

1.6.3 注意事项

  • 字段表集合中不会列出从父类或者父接口中继承而来的字段。
  • 内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  • 在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的

1.7 方法表集合

1.7.1 方法表结构

  Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式。并且这些数据项目的含义也与字段表极其相似,仅在访问标志及属性表集合的可选项上有所区别。
  读到这你可能会疑问,在下面这张结构表中怎么没有方法代码这一项,我写的Java源代码到底保存到哪了? 方法中的Java代码。经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为Code的属性里面,之后我们会结合实例再做分析。

类型 名称 含义 数量
u2 access_flags 访问标志 1
u2 name_index 方法名索引 1
u2 descriptor_index 描述符索引 1
u2 attributes_count 属性计数器 1
attribute_info attributes 属性集合 attributes_count

1.7.2 注意事项

  • 如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法。
  • 编译器可能会自动添加方法,最常见的便是类构造方法(类构造器)方法和(实例构造器)方法。
  • 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。

1.8 属性表集合

1.8.1 属性表介绍

   属性表在之前已经提及很多次了,其实Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。属性表要求更加宽松,允许只要不与已有属性名重复即可。
                     虚拟机规范预定义的属性

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量池
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature 类,方法表,字段表 用于支持泛型情况下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 用于存储额外的调试信息
Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的
LocalVariableTypeTable 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持
RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation 方法表 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation 方法表 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数

  

1.8.2 属性表结构

   对于每一个属性,它的名称都需从常量池中引用一个CONSTANT_Utf8_info类型的常量来进行表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性说明属性值所占用的位数即可。一个符合规则的属性表应该满足如下定义的结构。

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u2 attribute_length 1 属性长度
u1 info attribute_length 属性表

1.8.3 部分属性解读

1.8.3.1 Code属性

   Code属性是Class文件中最重要的属性,如果把Java程序信息分为代码(Java代码)和元数据(包括类、字段、方法及其他信息),那么Code属性就是用来描述代码的,而其他所有数据项目都是在描述元数据。

Code属性结构如下

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表所需的存续空间
u4 code_length 1 字节码指令的长度
u1 code code_length 存储字节码指令
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_length 异常表
u2 attributes_count 1 属性集合计数器
attribute_info attributes attributes_count 属性集合

部分类型说明:
attribute_name_index:一项指向CONSTANT_Utf8_info型常量的索引,该常量固定为"Code",代表了该属性的名称。
max_stack:操作数栈深度最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度。
max_locals: 局部变量表所需存储空间,单位为变量槽(Slot),Slot是JVM为局部变量分配内存的最小单位。
code_length:虽然是u4,但由于JVM规定方法不允许超过65535条字节码指令,所以只用了u2.

1.8.3.2 Exceptions属性

   Exceptions属性属于方法表,与Code属性平级,作用是列举出方法中可能抛出的受查异常,也就是方法描述时throws关键词之后的异常。结构如下。这和Code属性中的Exception table不一样,Code异常表是用来处理异常,实现finally处理机制的。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exception

1.8.3.3 LineNumberTable属性

  LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。通过两者的对应关系,当发生异常时,JVM可以准确的定义到是源码的哪行出现的错误。

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 line_number_table_length 1 行号表长度
line_number_info line_number_table line_number_table_length 行号表

1.8.3.4 LocalVariableTable及LocalVariableTypeTable属性

1.8.3.5 SourceFile属性

  attribute_name_index属性名索引指向常量池中的”SourceFile“字符串常量,sourcefile_index数据项指向常量池中CONSTANT_Utf8_info型常量索引,常量值是源码文件的文件名,通常类名和文件名是一致的,但也有一些特殊情况,比如内部类(见1.1.1)。

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 sourcefile_index 1 源码文件索引

写到这里,博主已经顶不住了。相信看到这里的你也一样

二、示例:字节码文件阅读

2.1 源文件

2.1.1 文件代码

  本次示例的Java源码如下。

public class ByteCodeTest {private String userName;public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}
}

2.1.2 字节码

  通常我们可以直接运行,然后在out文件夹中找到我们的class字节码(当然这个示例程序不行,因为没写main方法),也可以跳到文件夹,打开终端输入javac指令完成编译。

javac -encoding utf-8 ByteCodeTest.java

  然后我们将class文件拖入Binary Viewer中。它可以帮助我们读取二进制流文件,以16进制进行表示。

  现在我们可以看到class文件转成16进制之后的样子。

2.1.3 反编译

  把我们编译好的Class文件复制到out文件夹下,转到终端(这里博主使用的IDE是IDEA),利用javap指令进行反编译。

  反编译结果如下。我们可以很清楚的看到我们整个class文件的结构,比如主次版本号,常量池,方法表,同时方法表中还有一些我们比较熟悉的属性表,比如Code属性,再比如 LineNumberTable,SourceFile,因为该程序没有接口所以这里也就没有接口表。

Classfile /C:/Users/kouti/IdeaProjects/JVMDemo1/out/production/JVMDemo1/ByteCodeTest.classLast modified 2021-7-6; size 414 bytesMD5 checksum 3edd89f8dcb8cec0096512a634057689Compiled from "ByteCodeTest.java"
public class ByteCodeTestminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #4.#17         // java/lang/Object."<init>":()V#2 = Fieldref           #3.#18         // ByteCodeTest.userName:Ljava/lang/String;#3 = Class              #19            // ByteCodeTest#4 = Class              #20            // java/lang/Object#5 = Utf8               userName#6 = Utf8               Ljava/lang/String;#7 = Utf8               <init>#8 = Utf8               ()V#9 = Utf8               Code#10 = Utf8               LineNumberTable#11 = Utf8               getUserName#12 = Utf8               ()Ljava/lang/String;#13 = Utf8               setUserName#14 = Utf8               (Ljava/lang/String;)V#15 = Utf8               SourceFile#16 = Utf8               ByteCodeTest.java#17 = NameAndType        #7:#8          // "<init>":()V#18 = NameAndType        #5:#6          // userName:Ljava/lang/String;#19 = Utf8               ByteCodeTest#20 = Utf8               java/lang/Object
{public ByteCodeTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public java.lang.String getUserName();descriptor: ()Ljava/lang/String;flags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: getfield      #2                  // Field userName:Ljava/lang/String;4: areturnLineNumberTable:line 5: 0public void setUserName(java.lang.String);descriptor: (Ljava/lang/String;)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: aload_01: aload_12: putfield      #2                  // Field userName:Ljava/lang/String;5: returnLineNumberTable:line 9: 0line 10: 5
}
SourceFile: "ByteCodeTest.java"

  还不够?那我们还可以安装jclasslib插件,用插件来查看反编译结果,这样结构会更加清晰。

2.2 魔数及版本号

  先看开头的几个,依次读取我们的编码。

{"magic_num": "占位:u4 编码:(CA FE BA BE) -->描述:魔数","minor_version": "占位:u2 编码:(00 00) -->描述:次版本号","major_version": "占位:u2 编码:(00 34) -->描述:主版本号 这里数值为52,对应JDK版本为1.8"
}

2.3 常量池

  这里只拿前几号常量进行举例分析。因为常量池中的常量数据类型频繁涉及到常量池内部其他常量的索引调用,所以我们把刚才反编译出的常量池表也复制一份在下面。

   紧接着常量池个数后的就是我们的第一个常量,“Methodref_info”: "占位:u5 编码:(0A 00 04 00 11) ,其中0A是tag,值为10对应常量池表中的Methodref_info,tag用于描述该常量是何种常量,只有知道了常量类型我们才知道这个常量到底有多长;00 04是指向声明方法的类的描述符的,这里转换成十进制是4,也就是指向常量池中4号索引,对应我们的#4 class;

   之后是第二个常量Fieldref(09 00 03 00 12) ,他也有两个索引,分别是#3#18,查看1.3中的数据表,得知#3为指向声明字段的类或接口描述符这里指向的是一个class,而这个class常量又指向#19字符串- ByteCodeTest,这个字符串就该字段的所属类,但我们现在还不知道这个字段到底是什么,我们就需要查看第二个索引#18,这个索引是指向常量池中的NameAndType类型常量的,我们找到#18 NameAndType,查表后发现它也有两个索引,#5,#6分别表示字段/方法名称,字段/方法描述符,找到#5:userName,#6:java/lang/String,这下JVM就知道了:1.这个字段属于ByteCodeTest类;2.这个字段的名称是username;3.这个字段的描述符是String,这是一个String类型的字段。


{"constant_pool_count": "占位:u2 编码:(00 15) -->描述:共21个常量,减去被JVM占用的,实际为20个","Methodref_info": "占位:u5 编码:(0A 00 04 00 11) -->描述: #4.#17  // java/lang/Object.\"<init>\":()V\n","Fieldref_info": "占位:u5 编码:(09 00 03 00 12) -->描述:#3.#18 // ByteCodeTest.userName:Ljava/lang/String;\n","Class_info": "占位:u3 编码:(07 00 13) -->描述: #19 // ByteCodeTest","Class_info": "占位:u3 编码:(07 00 14) -->描述: #19 // ByteCodeTest","Utf8":占位:u11 编码(01  00 08   75 73 65 72 4E 61 6D 65):共8个UTF8编码组成的字符串 userName"Utf8":占位:u21 编码(01  00 12   4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B):18位字符串 Ljava/lang/String;"Utf8":占位:u9 编码(01  00 06   3C 69 6E 69 74 3E):6位字符串 <init>"Utf8":占位:u6 编码(01  00 03   28 29 56):3位字符串 ()V"Utf8":占位:u7 编码(01  00 04   43 6F 64 65):4位字符串 Code"Utf8":占位:u18 编码(01  00 0F   4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65):15位字符串  LineNumberTable"Utf8": 占位:u14 编码(01  00 0B   67 65 74 55 73 65 72 4E 61 6D 65):11位字符串 getUserName"Utf8": 占位:u23 编码(01  00 14   28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B):20位字符串  ()Ljava/lang/String;"Utf8":占位:u14 编码(01  00 0B   73 65 74 55 73 65 72 4E 61 6D 65):11位字符串 setUserName"Utf8":占位:u24 编码(01  00 15   28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56):21位字符串  (Ljava/lang/String;)V"Utf8":占位:u13 编码(01  00 0A   53 6F 75 72 63 65 46 69 6C 65):10位字符串 SourceFile"Utf8":占位:u20 编码(01  00 11   42 79 74 65 43 6F 64 65 54 65 73 74 2E 6A 61 76 61):17位字符串  ByteCodeTest.java"NameAndType":占位:u5 编码(0C   00 07   00 08)"NameAndType":占位:u5 编码(0C   00 05   00 06)"Utf8":占位:u15 编码(01  00 0C   42 79 74 65 43 6F 64 65 54 65 73 74):12位字符串 ByteCodeTest"Utf8":占位:u19 编码(01  00 10   6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74):16位字符串  java/lang/Object#1 = Methodref          #4.#17         // java/lang/Object."<init>":()V#2 = Fieldref           #3.#18         // ByteCodeTest.userName:Ljava/lang/String;#3 = Class              #19            // ByteCodeTest#4 = Class              #20            // java/lang/Object#5 = Utf8               userName#6 = Utf8               Ljava/lang/String;#7 = Utf8               <init>#8 = Utf8               ()V#9 = Utf8               Code#10 = Utf8               LineNumberTable#11 = Utf8               getUserName#12 = Utf8               ()Ljava/lang/String;#13 = Utf8               setUserName#14 = Utf8               (Ljava/lang/String;)V#15 = Utf8               SourceFile#16 = Utf8               ByteCodeTest.java#17 = NameAndType        #7:#8          // "<init>":()V#18 = NameAndType        #5:#6          // userName:Ljava/lang/String;#19 = Utf8               ByteCodeTest#20 = Utf8               java/lang/Object
}

对其中几个名称索引进行说明:

  • 常量池中的UTF8字符串,诸如Code,LineNumberTable等都是属性名,为之后的属性引用预先定义好的。

JVM指令码与Java源码的对应关系:

  • 其中Code属性是拿来记录JVM指令的,LineNumberTable用将Code中JVM指令行数与Java源码中行数进行对应,这样JVM抛出异常的时候我们才知道是Java源程序中的哪行代码出错。

this关键字:

  • aload_0意味着将0号局部变量压入栈,但我们发现我们并没有在ByteCodeTest(),也就是构造器方法中放置任何局部变量,甚至我们都没有写这个构造器方法,而是编译器自动帮我们添加的,那么我们哪来的0号元素?其实这里的0号元素是我们的this关键字,java语言的一条潜规则就是:在任何实例方法里面,都可以通过this来访问此方法所属对象,它的实现便是由Javac编译器编译的时候将this关键字的访问转变成对一个普通方法参数的访问,所有实例方法的局部变量表中至少会有一个指向当前对象实例的局部变量。
{
public ByteCodeTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0}

2.4 访问标志 、类索引、父类索引及接口表

{"access_flags":访问标志 占位:u2 编码:(00 21),0x0021是0020与0001位运算结果,1.4中有进行过分析,不再复述"this_class":类索引 占位:u2 编码:(00 03) 描述:-->#3Class -->#19 ByteCodeTest ,为类的全限定名"this_class":父类索引 占位:u2 编码:(00 04) 描述:-->#4Class -->#20 java/lang/Object,在java中没显示给出父类则默认继承Object
}


   再之后,紧接着的就是我们的接口个数,因为我们演示的程序并没有编写接口,所以此处为00 00,也就是没有接口。
   所以接口表为空,接下来的是字段个数以及我们的字段表。
   00 01:字段表个数,此时为1表示字段表中共一个字段。


   查询1.6.1的字段表结构,再依次分析上述编码含义。00 02:权限修饰符,private;00 05:常量池名称索引;#5 username ;00 06 常量池描述索引:#6 Ljava/lang/String String类型;00 00:属性表格个数 这里为0,意味着该字段属性表为空。

类型 名称 数量
u2 access_flags (权限修饰符) 1
u2 name_index (字段名称索引) 1
u2 descriptor_index (字段描述索引) 1
u2 attributes_count (属性表个数) 1
attribute_info attributes (属性表) attributes_count

2.4 方法表

   简单读一个方法,字节码阅读就结束吧,这里我已经按照方法表结构以及占位大小为每个数据项做了分割,我们还是逐一来看一下各个红框的具体含义。

00 03:方法表个数,共3个。

00 01:access_flags 方法访问标志 因为方法表访问标志和字段表近似,我们可以查1.6.2的字段访问标志表 ,查询结果 0001表示该方法为public方法。

00 07:name_index 名称索引 对应常量池#7 此方法为编译器自动添加的方法,为我们的实例构造器,是实例化类时调用的方法,对非静态变量解析初始化,。

补充:因为此处只做了第一个方法的介绍,其实之后还有编译器自动添加的另一个方法 ,两者的区别在于是类在初始化时调用的方法,是class类构造器对静态变量,静态代码块进行初始化,子类的方法中会先对父类方法的调用,并且clinit优先于init。这个我们在之后的类加载机制中会提及。

00 08: descriptor_index 方法描述,参数:返回值 对应常量池中的#8 ()V 意味着空参数空返回值

00 09:attributes_count 属性表个数,共1个

类型 名称 含义 数量
u2 access_flags 访问标志 1
u2 name_index 方法名索引 1
u2 descriptor_index 描述符索引 1
u2 attributes_count 属性计数器 1
attribute_info attributes 属性集合 attributes_count


   再来进一步分析该方法所带的属性表中的唯一属性,

00 09:attribute_name_index 对应常量池中的索引#9 Code

00 00 00 1D:attribute_length 属性长度 29

00 01:max_stack :1

00 01:max_locals: 1 这里之所以是1,因为有this

00 00 00 05:code_length:code指令码长度为5

Code属性结构如下

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表所需的存续空间
u4 code_length 1 字节码指令的长度
u1 code code_length 存储字节码指令
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_length 异常表
u2 attributes_count 1 属性集合计数器
attribute_info attributes attributes_count 属性集合


   然后我们来分析一下指令码2A B7 00 01 B1 的含义,以及找到它们的JVM助记符来理解JVM都执行了哪些指令:

{
Code:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0
}
{
0x2a    aload_0 将第一个引用类型本地变量
0xb7    invokespecial   调用超类构造方法,实例初始化方法,私有方法
0x00 01,指向常量池中的第1项
0xb1    return  从当前方法返回void
}

   后面的00 00是我们的exception_table_length因为这边没写try catch finally,所以长度为0,再之后的 00 01是我们Code属性的属性表个数(属性也是可以嵌套其他属性的),这里Code属性带的属性是LineNumberTable,刚才已经提过了就不再赘述了。

附录:

虚拟机规范预定义的属性

  由 波波烤鸭 整理,开头文章链接,推荐阅读。

属性名称 使用位置 含义
Code 方法表中 Java代码编译成的字节码指令(即:具体的方法逻辑字节码指令)
ConstantValue 字段表中 final关键字定义的常量值
Deprecated 类中、方法表中、字段表中 被声明为deprecated的方法和字段
Exceptions 方法表中 方法声明的异常
LocalVariableTable Code属性中 方法的局部变量描述
LocalVariableTypeTable 类中 JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
InnerClasses 类中 内部类列表
EnclosingMethod 类中 仅当一个类为局部类或者匿名类时,才能拥有这个属性,这个属性用于表示这个类所在的外围方法
LineNumberTable Code属性中 Java源码的行号与字节码指令的对应关系
StackMapTable Code属性中 JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类中、方法表中、字段表中 JDK1.5新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile 类中 记录源文件名称
SourceDebugExtension 类中 JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码运行在Java虚拟机汇中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension就可以存储这些调试信息。
Synthetic 类中、方法表中、字段表中 标识方法或字段为编译器自动产生的
RuntimeVisibleAnnotations 类中、方法表中、字段表中 JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。
RuntimeInvisibleAnnotations 类中、方法表中、字段表中 JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。
RuntimeVisible ParameterAnnotations 方法表中 JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法的参数。
RuntimeInvisible ParameterAnnotations 方法表中 JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法的参数。
AnnotationDefault 方法表中 JDK1.5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods 类中 JDK1.7新增的属性,用于保存invokedynamic指令引用的引导方法限定符

Java虚拟机字节码指令表

   指令表由 四月葡萄 整理,完整指令表请查看原文,开头文章链接,推荐阅读。

字节码 助记符 指令含义
0x00 nop 什么都不做
0x01 aconst_null 将null推送至栈顶
0x02 iconst_m1 将int型-1推送至栈顶
0x03 iconst_0 将int型0推送至栈顶
0x04 iconst_1 将int型1推送至栈顶
0x05 iconst_2 将int型2推送至栈顶
0x06 iconst_3 将int型3推送至栈顶
0x07 iconst_4 将int型4推送至栈顶
0x08 iconst_5 将int型5推送至栈顶
0x09 lconst_0 将long型0推送至栈顶
0x0a lconst_1 将long型1推送至栈顶
0x0b fconst_0 将float型0推送至栈顶
0x0c fconst_1 将float型1推送至栈顶
0x0d fconst_2 将float型2推送至栈顶
0x0e dconst_0 将do le型0推送至栈顶
0x0f dconst_1 将do le型1推送至栈顶
0x10 bipush 将单字节的常量值(-128~127)推送至栈顶
0x11 sipush 将一个短整型常量值(-32768~32767)推送至栈顶
0x12 ldc 将int, float或String型常量值从常量池中推送至栈顶
0x13 ldc_w 将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14 ldc2_w 将long或do le型常量值从常量池中推送至栈顶(宽索引)
0x15 iload 将指定的int型本地变量
0x16 lload 将指定的long型本地变量
0x17 fload 将指定的float型本地变量
0x18 dload 将指定的do le型本地变量
0x19 aload 将指定的引用类型本地变量
0x1a iload_0 将第一个int型本地变量
0x1b iload_1 将第二个int型本地变量
0x1c iload_2 将第三个int型本地变量
0x1d iload_3 将第四个int型本地变量
0x1e lload_0 将第一个long型本地变量
0x1f lload_1 将第二个long型本地变量
0x20 lload_2 将第三个long型本地变量
0x21 lload_3 将第四个long型本地变量
0x22 fload_0 将第一个float型本地变量
0x23 fload_1 将第二个float型本地变量
0x24 fload_2 将第三个float型本地变量
0x25 fload_3 将第四个float型本地变量
0x26 dload_0 将第一个do le型本地变量
0x27 dload_1 将第二个do le型本地变量
0x28 dload_2 将第三个do le型本地变量
0x29 dload_3 将第四个do le型本地变量
0x2a aload_0 将第一个引用类型本地变量
0x2b aload_1 将第二个引用类型本地变量
0x2c aload_2 将第三个引用类型本地变量
0x2d aload_3 将第四个引用类型本地变量
0x2e iaload 将int型数组指定索引的值推送至栈顶
0x2f laload 将long型数组指定索引的值推送至栈顶
0x30 faload 将float型数组指定索引的值推送至栈顶
0x31 daload 将do le型数组指定索引的值推送至栈顶
0x32 aaload 将引用型数组指定索引的值推送至栈顶
0x33 baload 将boolean或byte型数组指定索引的值推送至栈顶
0x34 caload 将char型数组指定索引的值推送至栈顶
0x35 saload 将short型数组指定索引的值推送至栈顶
0x36 istore 将栈顶int型数值存入指定本地变量
0x37 lstore 将栈顶long型数值存入指定本地变量
0x38 fstore 将栈顶float型数值存入指定本地变量
0x39 dstore 将栈顶do le型数值存入指定本地变量
0x3a astore 将栈顶引用型数值存入指定本地变量
0x3b istore_0 将栈顶int型数值存入第一个本地变量
0x3c istore_1 将栈顶int型数值存入第二个本地变量
0x3d istore_2 将栈顶int型数值存入第三个本地变量
0x3e istore_3 将栈顶int型数值存入第四个本地变量
0x3f lstore_0 将栈顶long型数值存入第一个本地变量
0x40 lstore_1 将栈顶long型数值存入第二个本地变量
0x41 lstore_2 将栈顶long型数值存入第三个本地变量
0x42 lstore_3 将栈顶long型数值存入第四个本地变量
0x43 fstore_0 将栈顶float型数值存入第一个本地变量
0x44 fstore_1 将栈顶float型数值存入第二个本地变量
0x45 fstore_2 将栈顶float型数值存入第三个本地变量

总结

没有总结,写完了,溜了溜了。

等等!

不来个免费的收藏和点赞吗?(贴一个自己剪的表情包)

JVM学习笔记(Ⅰ):Class类文件结构解析(带你读懂Java字节码,这一篇就够了)相关推荐

  1. 窥一斑而知全豹,几分钟带你读懂Java字节码,再也不怕了

    目录 1.如何看字节码 2.一个简单的例子 3.字节码结构 4.总结: 引言:都知道java的源文件最后会被编译成class文件,class文件的内容是字节码.为什么java要编译成字节码呐?我觉得最 ...

  2. 一文带你读懂Java字节码

    文章目录 前言 准备事宜 1 下载UltraEdit 下载Java虚拟机规范(Java SE 8版) 一.生成字节码 二.字节码阅读 class文件总览 魔数与副主版本号 常量池 字段 方法 统一讲解 ...

  3. jvm学习笔记-chapter6 类文件结构

    class类文件结构 数据及结构 是一组以8位字节为基础单位的二进制流.当遇到占有8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储 采用一种类似于C语言结构体的伪结构来存 ...

  4. 简单一文带你读懂Java变量的作用和三要素

    Java变量的作用 不只是java,在其他的编程语言中变量的作用只有一个:存储值(数据) 在java中,变量本质上是一块内存区域,数据存储在java虚拟机(JVM)内存中 变量的三要素 变量的三要素分 ...

  5. 【QT学习】QRegExp类正则表达式(一文读懂)

    文章目录 前言 一.QRegExp简介 二.元字符及通配模式 1.元字符 2.通配模式 三.QRegExp构造和方法 1.默认构造函数 2.模式构造函数 3. isValid() 函数 4. case ...

  6. 1.6 万字长文带你读懂 Java IO

    Java IO 是一个庞大的知识体系,很多人学着学着就会学懵了,包括我在内也是如此,所以本文将会从 Java 的 BIO 开始,一步一步深入学习,引出 JDK1.4 之后出现的 NIO 技术,对比 N ...

  7. 分分钟带你读懂-ButterKnife-的源码

    } }); target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", ...

  8. 带你读懂Java GC日志信息 教你如何使用工具查看【图文演示】

    文章目录 一.常见参数列表 二.设置参数位置 三.参数设置演示 四.日志补充说明 五.工具查看日志 一.常见参数列表 1.目的:通过阅读GC日志,我们可以了解Java虛拟机内存分配与回收策略. 2.内 ...

  9. JVM学习笔记0:Java虚拟机概述

    目录 第1章 Java虚拟机概述 1.1 虚拟机与Java虚拟机 1.2 JVM 1.2.1 JVM的位置 1.2.2 JVM的整体结构 1.2.3 Java代码执行流程 1.2.4 JVM的架构模型 ...

最新文章

  1. python 在内存中读写:StringIO / BytesIO
  2. uygurqa输入法android,uygurqa输入法
  3. 掌握这些 NumPy Pandas 方法,快速提升数据处理效率!
  4. leetcode 82. 删除排序链表中的重复元素 II(map)
  5. 12021.ADS7952采集芯片
  6. struct interface_今天就谈谈go中的接受 interface 参数,返回 struct
  7. oracle游标缓存,【oracle】游标——数据的缓存区
  8. 基于arcpy包在arcmap里面实现图层的随机选取
  9. IBM发布基于内存的人工智能计算架构
  10. 星际争霸环境旧版本replay回放无法观看问题
  11. mp4播放器带后台开源源码
  12. Mysql(2)事务
  13. 将视频的以flv格式转换mp4格式
  14. 商汤科技面试——AI算法岗
  15. 词法语法分析器EDG C++
  16. 最好的Google表格插件
  17. PGPool-II master/slave mode using caveat
  18. 寻找我梦,再见2016
  19. ceph存储 scsi设备驱动体系架构
  20. ESP8266/ESP32 NodeMCU接入阿里云物联网平台

热门文章

  1. springboot毕设项目古诗词鉴赏与交流平台04ps3(java+VUE+Mybatis+Maven+Mysql)
  2. ckeditor5 图片上传,tp5整合ckeditor5编辑器使用
  3. 微信小程序-如何获取用户表单控件中的值
  4. 鸿蒙系统开创者上海交通大学,朱新远 - 上海交通大学 - 系统生物医学研究院
  5. windows ODBC数据源管理程序(64位) 添加系统DSN时没有Microsoft Acess Driver(*.mdb,*.accdb)选项的解决办法
  6. #笔记-面向对象基础知识
  7. VMware中NET模式无法获取IP地址
  8. 时钟芯片PCF8563应用
  9. 微信小程序获取手机号码第一次失败第二次成功的解决方案
  10. 【高级篇 / SDWAN】(7.0) ❀ 08. 访问指定网站最快的宽带优先上网 ❀ FortiGate 防火墙