1. 字节码文件里是什么?

源代码经过编译之后变会生辰雇一个字节码文件, 字节码是一种二进制的类文件, 它的内容是JVM的指令, 而不像C\C++经由编译器直接生成机器码.


2. 什么是字节码指令?

Java虚拟机的指定由一个字节长度的, 代表着某种特定操作含义的操作码(opcode)以及跟随其后的0至多个代表此操作所需参数的操作数(operand)所构成. 虚拟机中需要指定并不包含操作数, 只有一个操作码
image.png


3. 怎么解读字节码文件?

方式一: 一个一个二进制的看. 使用16进制方法打开字节码文件, 比如用Noteppad++, 安装一个HEX-Editor插件
或 vs code安装Hexdump
image.png
image.png

方式二 : javap -v xxx.class
javap是JDK自带的反编译查看类文件二进制信息

javap的用法 -help —help -? 输出次用法信息 -version 版本信息, 其实是javap所在jdk的版本信息 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -p | -private 显示所有类和成员 -package 显示程序包/受保护的/公共类和成员(默认) -sysinfo 显示正在处理的类的系统信息(路径, 大小, 日期, MD5, 散列, 源文件名) -constants 显示静态最终常量

-s 输出内部类型签名 -l 输出行号和本地变量表 -c 对代码进行反汇编 -v -verbose 输出附加信息(包括行号, 本地变量表, 反汇编等详细信息)

image.png

方式三: IDEA中安装jclasslib插件
image.png


4.JVM指令助记符

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:

  • i代表对int类型的数据操作
  • l代表1ong
  • s代表shor
  • b代表byte
  • c代表char
  • f代表f1oat
  • d代表doub1e

也有一些指令的助记符中没有明确地指明操作类型的字母,如 arraylength指令,它没有代表数据类型的特殊字符,但
操作数永走只能是一个数组类型的对象

还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。

大部分的指令都没有支持整数类型byte、char和 short,甚至没有任何指令支持 boolean类型。
编译器会在编译期或运行期将byte和 short类型的数据带符号扩展(sign- Extend)为相应的int类型数据,
将 boolean和char类型数据零位扩展(zero- Extend)为相应的int类型数据。
与之类似,在处理boan、byte、 shbrt和har类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于 boolean、byte、 short和char类型数据的操作实际上都是使用相应的主nt类型作为运算类型

4.1 加载与存储指令

1、作用

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递

2、常用指令

xload_<n>xload【局部变量压栈指令】将一个局部变量加载到操作数栈:
>> x为i、l、f、d、a, ,代表是int类型数据,代表1ong类型,代表f1oat类型,代表 double类型
>> n 为0到3
变量到操作数栈:iload,iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_

上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如ioad<n>)。这些指令助记符实际上代表这个组指令(例如iload<n>代表了i1oad0iload1i1oad2i1oad3这几个指令)。
比如:
iload_0: 将局部变量表中索引为0位置上的数据压入操作数栈中
iload 4: 将局部变量表中索引为4位置上的数据压入操作数栈中

  • 【常量入栈指令】将一个常量加载到作数栈: bipushsipushldcldcw1dc2_w

aconst_nulliconst_m1iconst_<i>lconst_<l>fconst_<f>const_<d>

常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同 又可以分为 const系列、push系列和 ldc指令


  • 指令const系列: 用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有: icons_<i>

(i从-1到5)、lconst_<1>(1从0到1)、 fconst_<f>(f从0到2)、 dconst_<d>(d从0到1), aconst_null

常数到操作数栈:bipush,sipush,ldc,ldcw,ldc2_w,aconst_null,iconst_ml,iconst,lconst,fconst,dconst 比如: iconst_m1 将-1压入操作数栈; iconst_x(x为到5) 将x压入栈 lconst、 lconst1 分别将长整数θ和1压入栈 fconst_0、 fconst_1、 fconst_2 分别将浮点数θ、1、2压入栈 dconst_0和 dconst1分别将 double型θ和1压入栈\ ` acoust_null 将null压入操作数栈 从指令的命名上不难找岀规律, 指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数, f表示浮点数,d表示双精度浮点, 习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出

  • 指令push系列: 主要包括 bipushsipush。它们的区别在于接收数据类型的不同, blush接收8位整数作为参数, sipush接收16位整数, 它们都将参数压入栈

  • 指令ldc系列: 如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该常量池中的int、float或者 String的索引,将指定的内容压入堆栈

总结

类型 常数指令 范围
int(boolen,byte, char, short) iconst [-1,5]
bipush [-128, 127]
sipush [-32768, -32767]
ldc any int value
long lconst 0,1
ldc any long value
float fconst 0,1,2
ldc any float value
double dconst 0,1
ldc any double value
reference aconst null
ldc String literal, Class literal

image.png

  • 【出栈装入局部变量表指令】将一个数值从操作薮栈存储到局部变量表:store、 store(其中

x为i、1、f、d、a 、n 为0到3); pastore(其x为i、1、f、d、a、b、c、s)

这类指令主要以 store的形式存在, 比如 xstore、xstoren(x为i、l、f、d、a, n为0-3) 操作数栈到变量:istore,istore,lstore,lstore,fstore,fstore,dstore,dstor,astore,astore

其中,指令 istorp-n将从操作数栈中弹出一个整数, 并把它赋值给局部变量索引n位置 指令 store由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置

一般说来,类似像 store这样的命令需要带一个参数,用来指明将弹出的元素放在局部变量表的第几个位置。但是,为 了尽可能压缩指令大小,使用专门的 istore_1指令表示将弹出的元素放置在局部变量表第1个位置。类似的还有 store_0、 istore_2、 istore_3,它们分别表示从操作数栈顶弹出一个元素,存放在局部变量表第0、2、3个位置

由于局部变量表前几个位置总是非常常用,因此这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积如果局部变量表很大,需要存储的槽位大于3,那么可以使用 istore指令,外加一个参数,用来表示需要存放的槽位位

  • 扩充局部变早表的访问索引的指令:wide


4.2 算术指令

1、作用:

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈

2、分类

大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令

3、byte、 short、char和 boolean类型说明

在每一大类中,都有针对]ava虚拟机具体数据类型的专用算术指令。
但没有直接支持byte、 short、char和 boolean类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理 boolean、byte、 short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理

Java虚拟机中的实际类型与运算类型
实际类型 运算类型 分类
boolean int
byte int
char int
short int
int int
float float
reference reference
returnAddress returnAddress
long long
double double

4、运算时的溢出
数据运算可能会导致溢出,例如两个很大的正整数相加,结果能是一个负数。其实Java虚拟机规范并无明确规定过整
型数据溢岀的具体结果,仅规定了在处理整型薮据时,只有除法指令以及求余指令中当岀现除数为0时会导致虚拟机抛出
异常 ArithmeticException

5、运算模式

  • 向最接近数舍入模式:JwM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须

舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的

  • 向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值

的数字作为最精确的舍入结果

6、NaN值使用
当一个操作产生溢岀时, 将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN值
来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回NaN

7.所有的算术指令包括
加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem // remainder:余数
取反指令:ineg、lneg、fneg、dneg // negation:取反

自增指令:iinc
位运算指令,又可分为:

  • 位移指令:ishl、ishr、 iushr、lshl、lshr、lushr
  • 按位或指令:ior、lor
  • 按位与指令:iand、land
  • 按位异或指令:ixor、lxo

比较指令: dcmpg, dcmpl, fcmpg, fcmpl, lcmp

8. i++和++i的区别

1.不涉及其他赋值时, 指令是相同
image.png

2.涉及到赋值时, 操作指令有区别
image.png

image.png

9,比较指令
比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈

  • 比较指令有: dcmpg, dcmpl, fcmpg, fcmpl, lcmp

    • 与前面讲解的指令类似,首字符d表示doub1e类型,f表示f1oat,l表示long
  • 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两

个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同

  • 指令dcmpl和 dcmpg也是类似的 ,根据其命名可以推测其含义,在此不再赘述
  • 指令lcmp针对1ong型整数,由于long型整数没有NaN值,故无需准备两套指令

举例:
指令 fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,
若v1=v2,则压入;若v1>v2则压入1; 若v1两个指令的不同之处在于,如果遇到NaN值, fcmpg会压入1,而fcmpl会压入-1

4.3 类型转换指令

1、类型转换指令说明
①类型转换指同以将两种不同的数值类型进行相互转换
②这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法
与数据类型一一对应的问题

2.宽化类型转换
a.转换规则
Java虚拟机直接支持以下数值的宽化类型转换( widening numeric conversion,小范围类型向大范围类型的安全
转换)。也就是说,并不需要指令执行,包括

  • 从int类型到long、 float或者 double类型。对应的指令为:i21、i2f、i2d
  • 从1ong类型到float、double类型。对应的指令为:i2f、i2d
  • 从float类型到double类型。对应的指令为:f2d

简化为:int—>long—>float —>double

b.精度损失问题
1. 宽化类型转换是不会因为超过目标类型最大值而丢失信息的, 例如,从int转换到long,或者从int转换到
double,都不会丢失任何信息,转换前后的值是精确相等的

  1. 从int、long类型数值转换到float, 或者long类型数值转换到double时,将可能发生精度丢失能丢失掉
    几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值

尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常

c.补充说明
从byte、char和shot类型到int类型的宽化类型转换实际上是不存在的。对于byte类型转为int,虚拟机并没有做实
质性的转化处理,只是简单地通过操作数栈交换了两个数据。而将byte转为long时,使用的是i21,可以看到在内部
byte在这里己经等同于int类型处理,类似的还有 short类型,这种处理方式有两个特点:
一方面可以减少实际的数据类型,如果为 short和byte都准备一套指令,那么指令的数量就会大增,而虚拟机目前的
设计上,只愿意使用一个字节表示指令,因此指令总数不能超过256个,为了节省指令资源,将 short和byte当做
int处理也在情理之中
另一方面,由于局部变量表中的槽位固定为32位,无论是byte或者 short存入局部变量表,都会占用32位空间。从这
个角度说,也没有必要特意区分这几种数据类型.

3.窄化类型转换(Narrowing Numeric Conversion)
a.转换规则
Java虚拟机也直接支持以下窄化类型转换

  • 从int类型至byte、 short或者char类型。对应的指令有:i2b、i2c、i2s
  • 从long类型到int类型。对应的指令有:12i
  • 从f1oat类型到int或者long类型。对应的指令有:f21、f21
  • 从 double类型到int、long或者float类型。对应的指令有:d2i、d2l、d2f

b.精度损失问题
窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度

尽管数据类型窄化转换可能会发生上限溢岀、下限溢岀和精度丢失等情况,但是Java虚拟杋规范中明确规定数值类型
的窄化转换指令永远不可能导致虚拟机抛出运行时异常

c.补充说明

  1. 当将一个浮点值窄化转换为整数类型T(T限于int或1ong类型之一)的时候,将遵循以下转换规则
  • 如果浮点值是NaN,那转换结果就是int或1ong类型的0
  • 如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入模式取整,获得整数值v,如果v在目标类型T

(int或1ong)的表示范围之内,那转换结果就是v。否则,将根据v的符号,转换为T所能表示的最大或者最小正数

  1. 当将一个 double类型窄化转换为 float类型时,将遵循以下转换规则:

通过向最接近数舍入模式舍入一个可以使用float类型表示的数字。最后结果根据下面这3条规则判断

  • 如果转换结果的绝对值太小而无法使用float来表示,将返回float类型的正负零
  • 如果转换结果的绝对值太大而无法使用float来表示,将返回 float类型的正负无穷大
  • 对于 double类型的NaN值将按规定转换为f1oat类型的NaN值

    4.4 对象的创建与访问指令


    4.5 方法调用与返回指令

    4.6 操作数栈管理指令

    4.7 比较控制指令

    4.8 异常处理指令

    4.9 同步控制指令

加:iadd,ladd,fadd,dadd
减:isub,lsub,fsub,dsub
乘:imul,lmul,fmul,dmul
除:idiv,ldiv,fdiv,ddiv
余数:irem,lrem,frem,drem
取负:ineg,lneg,fneg,dneg
移位:ishl,lshr,iushr,lshl,lshr,lushr
按位或:ior,lor
按位与:iand,land
按位异或:ixor,lxor
类型转换:i2l,i2f,i2d,l2f,l2d,f2d(放宽数值转换)
i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f(缩窄数值转换)
创建类实便:new
创建新数组:newarray,anewarray,multianwarray
访问类的域和类实例域:getfield,putfield,getstatic,putstatic
把数据装载到操作数栈:baload,caload,saload,iaload,laload,faload,daload,aaload
从操作数栈存存储到数组:bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore
获取数组长度:arraylength
检相类实例或数组属性:instanceof,checkcast
操作数栈管理:pop,pop2,dup,dup2,dup_xl,dup2_xl,dup_x2,dup2_x2,swap
有条件转移:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpene,
if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne,lcmp,fcmpl
fcmpg,dcmpl,dcmpg
复合条件转移:tableswitch,lookupswitch
无条件转移:goto,goto_w,jsr,jsr_w,ret
调度对象的实便方法:invokevirtual
调用由接口实现的方法:invokeinterface
调用需要特殊处理的实例方法:invokespecial
调用命名类中的静态方法:invokestatic
方法返回:ireturn,lreturn,freturn,dreturn,areturn,return
异常:athrow
finally关键字的实现使用:jsr,jsr_w,ret