JVM基础回顾记录(四):类文件
一、JAVA的平台无关性
我们最开始学习java的时候了解到java代码文件必须经过javac
的编译,生成字节码文件才可以被JVM识别并运行,而JVM则拥有多种操作系统的版本:
正是因为这些不同操作系统下的JVM版本,使得我们被编译后的字节码文件可以被不同操作系统上的JVM识别并运行,继而实现跨平台。
二、字节码文件组成
字节码文件是经过编译器编译后的class文件,所以也可以称为Class类文件
,这个文件内的内容你是看不懂的,它是以某种顺序堆叠的二进制流
组成的文件,以字节(byte)
为基本单位,我们学习java基础时知道,单字节占有8个bit位,虽然你很难看懂,但对于JVM来说却可以很容易的解析这类文件,这里先不谈它如何解析,我们本次只说明这个文件里都放了些什么东西,接下来会结合实例来详细说明。
上面刚说过,文件内部的基本单位是字节(8bit),利用这些字节数据所处的先后顺序,来决定Class文件里不同的数据项,JVM在做字节码加载分析的时候也是按照这个顺序来进行的,这就像是一种序列化的方式,而JVM加载解析它的时候,相当于在做反序列化,这个在本节不做探讨,留到类加载章节会详细说明。
字节码文件里存储的数据类型分为两大类,一种是基本的无符号数字
类型,另外一种则是类似C语言中结构体
的一种东西,我们管它叫表
,表可能包含无符号数字,也可能包含另外一个表,也可能由无符号数字和其他表共同组成(其实也很像java里的类对吧?可能只包含基本类型的属性,也可能包含另外一个类的对象属性,虽然它跟类完全不是一回事,但为了便于理解,可以这样想)
由于字节码文件的“反序列化”过程非常的单纯,就是从前往后读,那么Class文件中的无符号数和表的堆叠顺序就显得非常重要,这其实是一种java虚拟机约定好的协议,比如Class文件读进来的二进制流,前4个字节一定是某个具体的字段,紧接着往后2个字节一定是另外一个字段,就这样,这个二进制字节流被协议切分成了具体的组,每一个组都代表着不同含义的字段。
三、前三个字节组:魔数、次版本号、主版本号
通过第二节
的了解,我们知道了Class文件就是字节与字节的顺序堆叠排列,然后按照字节码约定协议进行以字节为单位分组,每一组的数据代表着不同的含义,接下来让我们看下字节码文件的头部三个组都分别代表什么吧:
上图展示的就是Class文件开头几个字节被约定分组的情况,其实后面更复杂的表也是按照类似的方式做的。
无符号数按照不同数量的字节分组,形成了不同的数据类型
,需要占用4个字节才能存下的字段的类型被称为u4
,而只需要占用2个字节的字段类型被称为u2
,图2
中“魔数”就可以被说成是一个u4类型的数字,而次版本号和主版本号则为u2
。
魔数(magic):确定当前文件是否是一个可以被jvm加载的Class文件(像mp3、pdf等文件,开头一样也会有类似的魔数)
主版本号(major_version)&次版本号(minor_version):用来记录当前Class文件的版本号,每个版本的jdk编译过的Class文件,会保有其版本号信息,学习java基础的时候都了解过,java是自上向下兼容
的,比如jdk1.8编译出来的Class文件不可以被1.7版本的虚拟机加载运行,但jdk1.7编译出来的Class文件却可以被1.8版本的虚拟机加载运行,至于能不能被加载的第一道坎,就是按照这俩版本号进行判断的。
四、常量池
图2
里我们至少已经知道了魔数
、次版本号
、主版本号
这三个字段,往后的则被省略了。往后是什么呢?它是非常复杂且庞大的一个分组集合,被称为常量池(注意,这个常量池是指Class文件内的常量池,他们会被Class文件内一些索引项给索引到,准确的说并不是运行期的那个JVM方法区内的运行时常量池
,但随着JVM的类加载,类里的这个常量池会被加载进运行时常量池,顺便说下,到了那个时候,很多符号引用
也会转变为直接引用
)它们位于主版本号后面,第一项是一个u2类型的数字,表示有几个常量表如图:
4.1:池里常量的分类
常量池里的常量按照类型被分为了两大类:字面量和符号引用,而符号引用又往下细分了几个分类,如图:
4.1.1:字面量
很简单的概念,类似于常量,下面的分节将会详细介绍,主要是类似语言层面的基本类型,比如int
、float
、double
这些,都有对应的字面量常量表:CONSTANT_Integer_info
、CONSTANT_Float_info
、CONSTANT_Double_info
4.1.2:符号引用
符号引用都是以字符串的方式存储在常量池里的,它们通常用来描述类的全限定名、方法和字段的名称以及描述符,因此被分了三类.
类和接口的全限定名
第一类叫全限定名,例如,一个类叫:com.bilibili.test.Test
,则它的全限定名为:com/bilibili/test/Test
字段、方法的简单名称和描述符
相对来说,字段和方法的描述符更为复杂,我们得通过实际的例子来说明问题。
我们知道,一个类里面有字段以及方法,字段有它自己声明的类型以及名称,而方法则更加复杂,它存在入参、返回、名称,因此要想用描述符这样的字符串来描述一个字段或者一个方法,往往需要一定的格式,让我们来看看其格式吧。
先说字段,假如我的类里有个字段是int型,名称为num,名称没什么好说的就是num,但它的类型描述符却是:I
为什么int型的数据的类型描述符是I
?那double呢?下面我们就列出所有类型对应的描述符的映射关系表,后面更复杂的方法描述符也可以用到这个表:
类型 | 描述符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
reference(引用类型) | L,格式为L类型全限定名; ,例如Object类型的引用变量可表示为:Ljava/lang/Object; |
array(数组) | [,格式为:[类型描述符 ,比如int[] 可以表示为[I ,再比如Object[] 可以表示为[Ljava/lang/Object; ,高维数组,只需要多加一个[ 即可,比如int[][] 可以表示为[[I |
void(方法描述符独有,用于形容方法的无参数返回类型) | V |
ok,了解完各种类型的描述符规则,再来一遍开始那个字段描述符为I
就可以很容易理解了,下面来继续了解下方法的描述符,这个就更为复杂,其格式为:
1 | (参数1类型描述符 参数2类型描述符...)返回值类型描述符 |
下面通过几个例子来说明方法描述符,希望可以帮助你理解:
方法定义 | 描述符 |
---|---|
int getId() | ()I |
double getPrice(Object[] o, double price) | ([Ljava/lang/Object;D)D |
void setId() | ()V |
void setId(int id) | (I)V |
那么字段或方法的名称及描述符存在哪个地方呢?接下来要介绍的一个叫做CONSTANT_NameAndType_info
的常量表就是专门存放这俩数据的,它的内部结构请参考``里对它的介绍。
常量池里的每一项都是个表,下面,我们来深层次探讨下常量池里的每一个表的结构~
4.2:CONSTANT_Utf8_info(UTF-8编码的字符串)
这个表的详细结构如下:
即便是表,也是按照类似的方式堆叠的,后续几种类型也是类似。
基本上常量池里的表第一个字节都是tag
,tag在图4
中已经解释过了,它用来区分当前表属于什么类型的表,比如本节里tag=1,规范里tag为1的表就是CONSTANT_Utf8_info
,当虚拟机知道了表为CONSTANT_Utf8_info,那么很自然的后面两个字节肯定是length
,至于其内容,肯定就是length后面的字节了,然后截取length长度的字节,就是这个CONSTANT_Utf8_info所存放的实际内容。
4.3:CONSTANT_Integer_info(整型字面量)
直接上图:
我们都知道int型数据占4字节,32位,现在通过上图字节码的分配可以证实,下面的字面量也大体相同。
4.4:CONSTANT_Float_info(浮点型字面量)
4.5:CONSTANT_Long_info(长整型字面量)
4.6:CONSTANT_Double_info(双精浮点型字面量)
到这里,基本类型就介绍完了,通过对
2.3.2 ~ 2.3.5
的了解,可以发现,基本类型是很简单的类型,而且其实际内容符合java里基本类型所占位数。
4.7:CONSTANT_Class_info(类、接口的符号引用)
这里要详细介绍下index这个属性,因为在后面的表中,这个字段出现的频率会非常高。
这个字段代表了一个内容的索引,它索的谁的引呢?答案还是常量池,举个例子吧,Class_info是一个用来描述类或者接口全限定名
的表,既然是全限定名,那肯定是个字符串,那么index指向的肯定是常量池里一张CONSTANT_Utf8_info
的表,现在让我们假设一个常量池,里面已经排列好了各种表数据,按照常量池索引值从1开始,因此按照规则,绘制出下图:
当Class_info的index属性就是用来指向常量池中某一个表的,例如上图里index=2,则意味着常量池里索引下标为2的表的内容,就是Class_info的内容,开头说过,就是类或接口的全限定名。
4.8:CONSTANT_String_info(字符串类型字面量)
4.9:CONSTANT_NameAndType_info(字段、方法的部分符号引用)
之前介绍了常量池里存放的字段、方法都是存在名称以及描述符的,本节介绍的这个结构就是用来存放这两项内容的(对字段或方法的名称及描述符不熟悉的话,建议加强理解下图4
、表1
、表2
里的内容)
该结构存储了某个字段或方法的名称索引
和描述符索引
,那肯定有具体的某个字段或方法的表里会索引向它,接下来要介绍的CONSTANT_Fieldref_info
和CONSTANT_Methodref_info
,均有指向它的索引字段。
4.10:CONSTANT_Fieldref_info(类中引用字段的符号引用)
这个常量专门用来描述类内被引用到的属性
,包含你自定义
的出现在该类的属性,也包含该类里方法调用时使用的别的类的属性
。
字段的符号引用表,除了要描述声明自己的类或接口,还需要索引到具体的CONSTANT_NameAndType_info
,这个表里上面介绍过,内含一个名称
和具体的描述符
,下方的方法符号引用也是同样的结构,即一个字段或方法的符号引用等于:声明该字段或方法的类或接口的全限定名
+ 该字段或方法的名称
+ 该字段或方法的描述符
4.11:CONSTANT_Methodref_info(类中引用方法的符号引用)
这个常量专门用来描述类内被引用到的方法
,比如你在A类
里定义了一个叫test()
的方法,这个test方法并不会
在常量池存在一个CONSTANT_Methodref_info
,如果你再定义一个方法test2()
,让它调用test方法,这时test方法相当于被引用,这时就拥有了对应的Methodref_info.
再比如,你方法里经常会调用一些依赖包的类方法,比如最常用的System.out.println
,这个过程相当于你使用了out对象
的println方法
,此时当前类的字节码常量池便会有一个println
的Methodref_info出现,同时也会有一个叫out
的Fieldref_info出现。(这些均会在实战篇
讲解)
纠错,图里第一个info块里的内容应该是“Methodref_info的tag值为10”
4.12:CONSTANT_InterfaceMethodref_info(接口中方法的符号引用)
截止到目前,常见的11种常量已经介绍完了,下面再介绍3种JDK1.7
引入的新常量。
4.13:CONSTANT_MethodHandle_info(表示方法句柄)
这个常量是1.7
新增的特性,即方法句柄,可以和下方MethodType
结合使用,用法这里暂时不说,你可以简单理解,它是类似反射
的功能,可以指定调用哪个对象的哪个方法。但不同于反射,它可以在编译期就指定好方法调用,而不是运行期,这相比反射要安全的多。
4.14:CONSTANT_MethodType_info(标识方法类型)
待补全…
4.15:CONSTANT_InvokeDynamic_info(动态方法调用点)
待补全…
五、访问标志
紧挨着常量池后面的两个字节,代表访问标志
(access_flags
),因为其具备2个字节,所以它有16个bit位可以利用,每个位置的0或1代表不同的含义,当前只定义了8个,如下:
六、继承关系
接着access_flag后面,有描述本类继承关系的几个变量,分别如下:
学过java的人都知道,java类允许单继承
和多实现
,因此图中super_class
只有一个,interface
却对应了一个集合,当然,它们都只是u2类型的索引而已,指向常量池里的CONSTANT_Class_info
(参考4.7
)
七、字段表集合
我们离开了继承关系后,紧挨着的就是字段表集合。字段表,即类(或接口)里声明的变量,变量分为静态变量
(类变量)以及成员变量
(实例变量)。
现在详细介绍下图中虚线
部分,这四个u2类型的字段以及属性表
(attribute_info
)集合代表了一个field_info
表,它的这些字段解释如下:
7.1:access_flags
这个字段也叫access_flags
,跟之前Class本身的access_flags所具备的意义一样,它是用来描述字段
的访问标志
,让我们来看看它每一位代表什么意思吧:
7.2:name_index & descriptor_index
name_index
和descriptor_index
都是索引值,前者指向常量池
里一个CONSTANT_Utf8_info
,用来表示该字段的简单名称
,后者也是指向常量池
里一个CONSTANT_Utf8_info
,但它用来表示字段的描述符
(什么是简单名称,什么是描述符?请参考4.1.2
)
7.3:attributes_count & attribute_infos
这段信息很复杂,代表属性表,attributes_count
代表后面跟几个属性表,attribute_infos
代表属性表集合
,属性表是很复杂的一块内容,Class文件、字段表、方法表,甚至Code属性表都可以携带自己的属性表集合,属性表的种类很繁多,在9.1节
会列举一个与本节(方法表)相关的属性表ConstantValue
的结构。
八、方法表集合
离开字段表后,紧挨着的就是方法表集合,它几乎跟字段表结构一致。方法表,即类(或接口)里声明的方法,同样的,方法分为静态方法
(类方法)以及成员方法
(实例方法)。
同样的,来介绍下图中虚线部分,这四个u2类型的字段以及属性表
(attribute_info
)集合代表了一个method_info
表,它的这些字段解释如下:
8.1:access_flags
不多说了,前面遇到很多次了,它用来描述方法
的访问标志
,让我们来看看它每一位代表什么意思吧:
8.2:name_index & descriptor_index
跟字段表对应的字段作用相同,name_index
和descriptor_index
都是索引值,前者指向常量池
里一个CONSTANT_Utf8_info
,用来表示该方法的简单名称
,后者也是指向常量池
里一个CONSTANT_Utf8_info
,但它用来表示方法的描述符
(什么是简单名称,什么是描述符?请参考4.1.2
)
8.3:attributes_count & attribute_infos
属性表,在7.3节
提到过,这里的俩属性跟字段表里的意思一样,但在本节你可能会好奇,方法表定义了方法的名称和描述符,那么方法的代码跑哪里去了?其实方法本身的代码会被编译成字节码指令,存放在每个方法后面的某个属性表里,而这个属性表就是Code
,在后续的9.2
里会详细介绍,这里只需要记住它是出现在本节(方法表)这个位置的即可。
九、属性表
前面两节或多或少提到过属性表,这是很复杂也很重要的一个表结构,下面来看下它的基本组成:
不管属性表多么复杂,它开始的两个属性总是attribute_name_index
和attribute_length
,具体含义如图所示。
本节只少量介绍几个很重要的属性表,更详细的属性表可以查阅https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7
9.1:ConstantValue
先回看下7.3
,该属性表仅作用在字段表
上,用来表示一个static final
字段的初始值,且要求该字段必须是基本类型
或String类型
才可以用该表表示初始值,为什么?我们来看下该表的结构:
因为constantvalue_index
指向的一定常量池里某字面量,因此它只能是CONSTANT_Long_info
、CONSTANT_Float_info
、CONSTANT_Double_info
、CONSTANT_Integer_info
、CONSTANT_String_info
里的一种,所以只有被声明为static final的基本类型和String类型的字段,才可能存在此属性表,用来代表其初始化值。
9.2:Code
这绝对是最高能的一个属性表,我们详细来解析下,从它的名字就可以看出来,它代表的是一个方法的代码
属性,属于方法表
(这点请牢记),这在8.3
里提到过,我们写的程序代码最终会被javac
编译成字节码指令
,而这些指令就存储在Code属性表内,它的结构是怎样的呢?看图:
这里面有很多细节,我们来逐一介绍下,就从max_stack
开始吧~
9.2.1:max_stack
这个值跟操作数栈
有关系,代表它的最大深度
,当jvm处于运行期的时候,会根据这个值来分配栈帧
中操作数栈
的深度。(栈帧
是后续文章将会介绍的概念,运行期时每一个Code模块,都会与之对应一个栈帧结构,可通过10.2.5.1
节了解其基本概念)
9.2.2:max_locals
这个值也是围绕着运行期栈帧
来生效的,它代表栈帧里局部变量表
所需的存储空间。什么是局部变量表?早期接触java时就知道,每个方法内产生的对象、变量我们都称之为局部变量
,它们会在运行期被暂存入方法所属栈帧里的局部变量表中,方便操作数栈存取与操作,而它需要多少存储大小呢?就是靠这个值来决定的,结合9.2.1
可以知道,一个java方法的局部变量表大小
、操作数栈最大深度
都是在编译期
就已经确定了的。
那么,存储大小的单位又是什么呢?是字节(byte)吗?比如max_locals为5,代表的是5byte的存储空间吗?
并不是,它是一个叫做Slot
的东西,max_locals=5,代表需要分配5个Slot。下面来介绍下Slot。
Slot有32个bit位
,意思就是说每个Slot占4字节
,因此,如果你有一个局部变量是int型,那么它就占1个Slot空间,如果是float或者long之类的,那就需要两个Slot。
那现在我们就可以很容易推算出一个方法的局部变量表的容量到底需要多大了:总Slot量 = 所有局部变量所用的字节量 / 4
但是,这样是不对的!因为根据每个局部变量的作用域不同,Slot是可以被复用
的,这点很重要,让我们来看个例子:
1 | public int slot(int a) { //入参a,为a分配一个Slot |
这段代码一共有a、b、c、d四个int型变量,理论上需要分配4个
Slot存储,但实际只分配了3个
,具体原因请参考代码中的注释,这种利用java语法来节省Slot的做法还是相当聪明的。
9.2.3:字节码指令
参考图25
,离开了max_stack
和max_locals
,紧接着就进入了一个非常冗长的区域,那就是code
区域,它是你写的java程序被翻译成的一个个指令码
,是最重要的一部分,也是直接可以被java程序员操控的部分。
字节码指令大全:https://luisstruggle.github.io/blog/javaSE7-JVM.html
后续会以某个实例的字节码指令集来分析这些指令是如何在栈帧
里运行的(虽然是运行期的东西,但还是想在这一节做下简单的说明)
十、实战
10.1:快速回顾
终于把字节码的详细内容讲完了,太多太长,上面是详细介绍,下面就通过一张图简单回顾一下字节码文件的堆叠顺序和内容种类:
10.2:字节码分析实战
10.2.1:源代码
本篇会通过以下实例来具体解析字节码组成:
1 | package sun.juwin.test; |
10.2.2:字节码信息
首先我们利用javac
将其编译成字节码文件(我这里使用jdk11
版本做的编译),然后利用javap
指令,输出其字节码信息:
1 | javac&javap指令 |
javap这个指令是干嘛的呢?它实际上是将编译好的字节码文件反编译
,然后输出字节码文件里各数据项的详细信息,相比直接去读二进制的字节码文件,javap输出的字节码详细信息更加通俗易读。
现在来看下代码块2
的字节码详细信息(太冗长了,后面会拆解说明):
1 | Compiled from "Test.java" |
太长了,我们来将上面的字节码信息拆解一下进行详细分析。
10.2.3:字节码-头部
1 | Compiled from "Test.java" |
10.2.3:字节码-常量池
1 | Constant pool: //下面就是大杂烩的常量池了,#xx代表一个索引,所以看得时候需要找序号的按照这个值找就行了。 |
10.2.4:字节码-字段表
看完了最为复杂的常量池,下面来看下字段表部分,这部分简单许多:
1 | public static final int j; //类变量j |
10.2.5:字节码-方法表
ps:想用更加简洁的方式看方法指令的话,建议使用
javap -c
。本节只挑选代码块2
实例中的几个具有代表性的方法来说明。
下面就是我们的方法表了,内部会包含Code这个涵盖方法主要逻辑的指令码,我们这节会一点点分析这些指令是如何在操作数栈
里运行的(操作数栈
是运行期要学的一个结构,先做个了解,它位于栈帧
内,主要负责执行Code指令码),这里要注意:栈是一种先进后出
的数据结构,既然操作数栈是栈,那么必然满足这一点特性。
10.2.5.1:无参构造器-Test()
javap输出的详细信息:
1 | public sun.juwin.test.Test(); //Test方法,Code区域的指令集是如何在操作数栈运行的?请看图27 |
先来看下其源代码:
1 | public Test() { //隐式传入this作参数 |
它对应的指令码参考代码块7-Code部分
当一个线程运行到此方法,会为其创建一个栈帧
。
栈帧的基本概念
栈帧是用于支持jvm进行
方法调用
和方法执行
的数据结构
,它是运行期保存在虚拟机栈
中的栈元素,是线程运行的基本单元,由执行引擎
触发执行其内部保存的字节码指令(这里执行引擎在执行代码时还分为解释执行
和编译执行
)。栈帧由局部变量表
、操作数栈
、动态连接
、方法返回地址
、附加信息
组成
Test()方法栈帧结构如图:
现在,让我们看一下这个方法在运行时,栈帧内部是如何变化的:
栈帧初始状态:入参首先填充进局部变量表
解释:首先Test方法是一个非静态方法,其方法第一个入参就是
this
这个reference类型的变量,因此aload_0就是将该变量推向操作数栈的栈顶。
第一步:aload_0
,代表将局部变量表内第0个Slot内保存的变量入栈
第二步:invokespecial
,代表一次构造方法的调用,根据代码块7
可知这里调用的是其父类的构造方法
随着Object构造方法触发,随之又产生了一个Object构造器的栈帧,用来执行父类构造器逻辑,传入的是子类对象指针this
,这点要记住。为了优化内存,jvm开发者们经常会将新开栈帧的局部变量表
和上一栈帧的操作数栈
共享一些数据,共享是完全按照下一栈帧接收的参数个数来的,比如下一栈帧需要5个参数,那么当前栈帧操作数栈从栈顶开始往下数5个,全部会共享进下一栈帧的局部变量表里。
还有一点需要说明一下,我们知道在一个对象的构造器被触发时,它的父类构造器首先会被触发,此时很多人认为父类跟子类一样,肯定也是被new
了一次,其实不然,集合图29
和图30
可知,在Test类的构造器被触发之前,就已经存在this指针
了,那意味着类对象已经产生了,因此构造器是在对象产生后主动触发的,而父类也没有被new,仅仅是触发了一下它的构造器而已,你可以理解,一个子类对象,其实就是包含了其父类公共方法的类,不存在对象嵌套的情况(即一个子类的对象是不会包含一个父类对象的),这个在第六节
会详细讲,本节做个预热即可。
最后随着新开栈帧运行结束,线程又会再次回到当前栈帧,并将当前栈帧内的操作数栈共享部分
清空,若下一栈帧有返回值
,则将该值存入当前栈帧的栈顶
,否则不进行其他操作。说到这里也知道为什么栈帧也是栈了,先进入的栈帧最后才会执行完,越是新开的栈帧越先结束,这似乎也是栈的特性。
第三步:getstatic
,用来获取某个类的静态属性,由代码块7
可知,这里是从类常量池里获取到的PrintStream
类型的System
类的out
静态属性,即System.out
。
提示:
getstatic
的执行流程应为:从常量池获取常量的描述符
,然后根据描述符里指示的类和属性名,去对应的类里拿到该属性的值。
第四步:ldc
,由代码块7
可知,这是从常量池里加载String类型字面量“Test init !”入栈
第五步:invokevirtual
,代表了一次虚方法的调用,本例指的是对out对象的println
方法的调用
结合上图来思考下println的两个参数,首先我们需要明确一点,println不是静态方法,因此它传入的第一个参数一定是“this”,即对象引用,其实任何非静态方法的访问都是如此,需要知道自己的对象是哪位,否则没意义,那么针对println这个方法的“this”,又是谁呢?答案是out,我们是用out对象调用的它,所以这里第一个入参一定是out对象,第二个入参则是“Test init !”,所以上图中共享给下游栈帧的一共有俩,已用红框
标出。
第六步:return
,方法执行结束,栈帧出让,就不画图了。
到这里,我们已经完成了一组指令码的执行过程解析。
10.2.5.2:sum_i(int a, int b)
再来分析一个方法的执行流程,我们把javap的信息丢出来(这里就不加注释了):
1 | public int sum_i(int, int); |
它的源代码如下:
1 | public int sum_i(int a, int b) { |
它有3个参数,还发生过一次方法调用,最后还要将方法返回的值再加上i属性,然后将结果返回出去,我们来看下这个方法的运行过程。
老样子,来看下栈帧初始状态:入参首先填充进局部变量表
这是个非静态方法,第一个参数依然是该方法对应类的某个对象的引用指针(即this),剩下两个参数就是源代码里的a和b。
第一步:aload_0
,代表将局部变量表内第0个Slot内保存的变量入栈,即this入栈,因为接下来的getfield
指令要用到,这跟非静态方法需要this的道理一样,获取某个非静态的属性,也需要知道是哪个对象,才能取到对应的值,这就是非静态属性和方法与静态属性方法最大的区别。
第二步:getfield
,这个指令用来获取某对象的某个属性值,本例中为i
提示:
getfield
执行流程:从常量池获取属性的描述符
,然后根据描述符里指示的类和属性名,去对应的类对象
里拿到该属性的值。(图中第2步就是利用当前栈顶的this,来打入其内部,将其对应属性i
的值取出,并设置入栈顶,替换掉原来的this)
第三步:aload_0
,再次将局部变量表内第0个Slot内保存的变量入栈
第四步:将入参a和b入栈
第五步:invokevirtual
,虚方法调用,本例指的是对sum方法
的调用,通过实例代码可知,sum方法拥有三个参数,即this
、a
、b
,因此开栈帧时需要共享当前栈帧自栈顶往下数3个栈位作为新栈帧的入参
注意,sum方法
是有返回值的,返回值为a和b相加的结果,最终sum栈帧执行完毕再次回到sum_i栈帧时,会将其返回结果入栈,最终sum执行完毕后的情况如下:
第六步:iadd
,将栈顶两int型数值相加,并且其运算结果入栈
第七步:ireturn
,将现在位于栈顶的数据返回出去给上游的栈帧,就不画图了。
到目前,sum_i方法的执行流程就剖析完毕。
十一、小结
本章介绍了Class文件本身的组成
以及它内部数据的堆叠
方式,还介绍了运行期指令码是怎么被执行引擎
所运行的,可以尝试利用这些知识,分析日常中较为复杂的类的字节码文件,熟悉字节码指令对于我们理解日常java代码的一些“特殊性质”有一定的积极作用。