前面咱们讲解了Class文件的结构、以及采用不一样的方式来解读Class文件html
可是针对于Class文件里方法的字节码指令,咱们并无进行细节的指令分析java
本篇文章,咱们开始对字节码指令进行分析,看看示例代码里的方法到底作了什么事情?数组
Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。缓存
Java虚拟机的指令由一个字节长度
的、表明着某种特定操做含义的数字
(称为操做码,Opcode)以及跟随其后的零至多个表明此操做所需参数
(称为操做数,Operands)而构成。oracle
因为Java虚拟机采用面向操做数栈而不是寄存器的结构,因此大多数的指令都不包含操做数jvm
咱们能够采用上一篇文章的示例代码与字节码分析进行解析看看ide
咱们根据上篇的思路,找找这些字节码指令对应的字节码是什么呢?表明什么意思呢?性能
虚拟机限制了Java 操做码的长度为一个字节(即0~255),这意味着操做码总数不可能超过256条学习
官方文档: https: //docs.oracle.com/javase/specs/jvms/se8/htm1/jvms-6.html
ui
熟悉虚拟机的指令对于动态字节码生成、反编译Class文件、Class文件修补都有着很是重要的价值
。所以阅读学节码做为了解ava虚拟机的基础技能,须要熟练掌握常见指令。
================================
若是不考虑异常处理的话
那么Java虚拟机的解释器可使用下面这个伪代码当作最基本的执行模型来理解
do{ 自动计算PC寄存器的值加1; 根据PC寄存器的指示位置,从字节码流中取出操做码; if(字节码存在操做数)从字节码流中取出操做数; 执行操做码所定义的操做; }while(字节码长度 > 0);
================================
在Java虚拟机的指令集中,大多数的指令都包含了其操做所对应的数据类型信息。例如:
咱们能够根据上篇的示例代码进行解析分析看看
咱们能够看看局部变量表里的索引:0的值是什么?
对于大部分与数据类型相关的字节码指令,它们的操做码助记符中都有特殊的字符来代表专门为哪一种数据类型服务
:
也有一些指令的助记符中没有明确地指明操做类型
的字母,如arraylength指令没有表明数据类型的特殊字符,但操做数永远只能是一个数组类型的对象
。
还有另一些指令,如无条件跳转指令goto
则是与数据类型无关
的。
可是大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型
编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据
,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据
。
与之相似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。所以大多数对于boolean、byte、short和char类型数据的操做,实际上都是使用相应的int类型做为运算类型
。
byte b1 = 12; short s1 = 10 int i = b1 + s1
================================
因为彻底介绍和学习这些指令须要花费大量时间。为了让你们可以更快地熟悉和了解这些基本指令,这里将JVN中的字节码指令集按用途大体分红9类
写在前面的,关于这些不一样分类指令,大多在作值相关操做时:
一个指令能够从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(多是值,多是对象的引用)被压入操做数栈。
一个指令也能够从操做数栈中取出一到多个值(pop屡次),完成赋值、加减乘除、方法传参、系统调用等等操做。
================================
加载和存储指令用于将数据从栈帧的局部变量表和操做数栈之间来回传递
。
================================
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如:iload_<n>
)。
指令助记符实际上表明了一组指令
例如:iload_<n>
表明了iload_0、iload_一、iload_二、iload_3
这几个指令。
这几组指令都是某个带有一个操做数的通用指令(例如:iload
)的特殊形式,对于这若干组特殊指令来讲,它们表面上没有操做数,不须要进行取操做数的动做,但操做数都隐含在指令中
。
除此以外它们的语义与原生的通用指令彻底一致
例如 iload_0
的语义与操做数为时的 iload 指令语义彻底一致。
示例举例:
iload_0:将局部变量表中索引为0位置上的数据压入操做数栈中,这是占一个字节
iload 0:将局部变量表中索引为0位置上的数据压入操做数栈中,这是占两个字节
在尖括号之间的字母指定了指令隐含操做数的数据类型,具体信息以下:
<n>表明非负的整数、<i>表明是int类型
<l>表明long类型、<f>表明float类型
<d>表明double类型
操做byte、char、short和boolean类型数据时,常常用int类型的指令来表示。
================================
咱们知道Java字节码是Java虚拟机所使用的指令集。所以与Java虚拟机基于栈的计算模型是密不可分
在解释执行过程当中每当为Java方法分配栈桢时,Java虚拟机每每须要开辟一块额外的空间做为操做数栈,来存放计算的操做数以及返回结果
。
具体来讲即是:执行每一条指令以前,Java虚拟机要求该指令的操做数已被压入操做数栈中。在执行指令时,Java虚拟机会将该指令所需的操做数弹出,而且将指令的结果从新压入栈中
。
以加法指令 iadd为例。假设在执行该指令前,栈顶的两个元素分别为 int值 1和 int 值 2,那么iadd 指令将弹出这两个int,并将求得的和int值 3 压入栈中。
因为 iadd
指令只消耗栈顶的两个元素,所以,对于离栈顶距离为?的元素,即图中的问号,iadd
指令并不关心它是否存在,更加不会对其进行修改。
================================
Java方法栈桢的另一个重要组成部分则是局部变量区,字节码程序能够将计算的结果缓存在局部变量区之中
。
实际上,Java虚拟机将局部变量区当成一个数组,依次存放 this 指针〈仅非静态方法),所传入的参数,以及字节码中的局部变量。
和操做数栈同样,long类型
以及 double类型
的值将占据两个单元,其他类型仅占据一个单元。
举例: public vid foo( long l,fl1oatf) { { int i = 0; } { string s = "He11o, wor1d" ; } }
在栈帧中,与性能调优关系最为密切的部分就是局部变量表
。局部变量表中的变量也是重要的垃圾回收根节点(GC Roots),只要被局部变量表中直接或间接引用的对象都不会被回收
。
在方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量压栈指令将给定的局部变量表中的数据压入操做数栈。
这类指令大致能够分为:
xload_<n>
,描述为:x为i、l、f、d、a,n为0到3
xload
,描述为: x为i、l、f、d、a
说明:在这里x
的取值表示数据类型
指令xload_n
表示将第n个局部变量压入操做数栈
好比iload_一、fload_0、aload_e
等指令。其中aload_n
表示将个对象引用压栈
。
指令xload
经过指定参数的形式,把局部变量压入操做数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,好比指令iload、 fload等。
接下来使用示例代码来演示一下局部变量压栈指令
public class LoadAndStoreTest { //1.局部变量压栈指令 public void load(int num,object obj,long count,boolean flag,short[] arr){ system.out.println(num); system.out.println(obj); system.out.println(count); system.out.print1n(flag); system.out.println(arr); } }
咱们使用编译一下,而且在idea中使用插件来看看该方法具体的指令有哪些?
此时咱们根据这些指令进行分析看看,而且看看局部变量表与操做数栈是怎么样的状况
咱们也可使用idea的插件校验一下,看看是否方法里的局部变量表一致?
接下来咱们分析一下指令是怎么操做局部变量表与操做数栈的,
当咱们操做局部变量表索引为:5的时候,就会发现它占用了两个字节:iload 5,why?
常量入栈指令的功能是
常数压入操做数栈,根据数据类型和入栈内容的不一样,又能够分为const系列、push系列和ldc指令。
================================
用于对特定的常量入栈,入栈的常量隐含在指令自己里。
指令有: iconst_<i>
、描述:i从-1到5
指令有: lconst_<l>
、描述:l从0到1
指令有: fconst_<f>
、描述:f从0到2
指令有: dconst_<d>
、描述:d从0到1
指令有: aconst_null
、描述:d从0到1
好比有示例:
从指令的命名上不难找出规律,指令助记符的第一个字符老是喜欢表示数据类型。
若是指令隐含操做的参数,会如下划线形式给出。
================================
主要包括bipush和sipush
,它们区别在于接收数据类型的不一样
:
bipush接收8位整数做为参数、sipush接收16位整数,它们都将参数压入栈。
================================
若是以上指令都不能知足需求,那么可使用万能的ldc指令,它能够接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的内容压入堆栈
。
相似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc。
若是要压入的元素是1ong或者double类型的,则使用1dc2_w指令
,使用方式都是相似的。
接下来使用示例代码来演示一下常量压栈指令
public class LoadAndStoreTest { //2.常量入栈指令 public void pushConstLdc() { int i = 1; int a = 5; int b = 6; int c = 127; int d = 128; int e = 32767; int f = 32768; } }
咱们使用编译一下,而且在idea中使用插件来看看该方法具体的指令有哪些?
虽然咱们都是int类型的变量,可是指令里也有byte、long、short这些类型
因此咱们能够总结一下,具体类型的范围具体定义,能够看以下图:
那么对于float、long类型,咱们也进行示例代码看看具体是怎么样的?
public class LoadAndStoreTest { //2.常量入栈指令 public void constLdc() { 1ong a1 = 1; long a2 = 2; float b1 = 2; f1oat b2 = 3; double c1 = 1; double c2 = 2; Date d = null; } }
咱们使用编译一下,而且在idea中使用插件来看看该方法具体的指令有哪些?
咱们前面也提到过压入的元素是1ong或者double类型的,则使用ldc2_w指令
当咱们超出float类型的范围一样也是使用ldc2_w的指令
出栈装入局部变量表指令
用于将操做数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值
这类指令主要以store的形式存在
指令:xstore
,描述:x为i、l、f、d、a
、
指令:xstore_n
,描述:x 为i、l、f、d、a,n为至 3
。
其中指令istore_n
将从操做数栈中弹出一个整数,并把它赋值给局部变量索引n位置
指令xstore因为没有隐含参数信息,故须要提供一个byte类型的参数类指定目标局部变量表的位置
接下来使用示例代码来演示一下常量压栈指令
public class LoadAndStoreTest { //3.出栈装入局部变量表指令 public void store(int k,double d){ int m = k + 2; long l = 12; string str = "atguigu"; float f = 10.0F; d = 10; } }
咱们使用编译一下,而且在idea中使用插件来看看该方法具体的指令有哪些?
此时咱们根据这些指令进行分析看看,而且看看出栈指令是怎么样的状况
接下来使用示例代码来演示一下其余的状况说明
public class LoadAndStoreTest { //4.出栈装入局部变量表指令 public void foo( 1ong l,f1oat f){ { int i = 0; } { string s = "He1lo,wor1d" } } }
咱们使用编译一下,而且在idea中使用插件来看看该方法具体的指令有哪些?
仅接着咱们来看看局部变量表里有什么呢?
可是咱们的局部变量表长度是多少呢?咱们一块儿来看看
此时咱们根据这些指令进行分析看看,而且看看出栈指令是怎么样的状况