JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂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{
@Override
public 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 类型标识符,值固定为1
byte[] 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.class
Last modified 2021-7-6; size 414 bytes
MD5 checksum 3edd89f8dcb8cec0096512a634057689
Compiled from "ByteCodeTest.java"
public class ByteCodeTest
minor version: 0
major version: 52
flags: 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: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public java.lang.String getUserName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field userName:Ljava/lang/String;
4: areturn
LineNumberTable:
line 5: 0
public void setUserName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field userName:Ljava/lang/String;
5: return
LineNumberTable:
line 9: 0
line 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: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
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=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
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型数值存入第三个本地变量 |
总结
没有总结,写完了,溜了溜了。
等等!
不来个免费的收藏和点赞吗?(贴一个自己剪的表情包)