JVM —— 类文件结构(下)

简介 

  Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成
  虚拟机中许多指令并不包含操作数.只有一个操作码。
  如果忽略异常处理,执行逻辑类似
do{
  自动计算pc寄存器以及从pc寄存器的位置取出操作码;
  if(存在操作数){
    取出操作数;
  }
  执行操作码所定义的操作;
}while(处理下一次循环);

  操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将大端排序存储,即高位在前的字节序。限制了操作码长度为一个字节 0~255,   但是也就导致操作码个数不能超过256。

  例如,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte]和byte2 ),那这个16位无符号整数的值就是:  (bytel<<8) | byte2.

  字节码指令流应当都是单字节对齐的,只有,tableswitch和lookupswitch两个指令例外 这俩货是4字节为单位的。
  
  如果向上面那样如果操作码处理超过一个字节的数据时,就必须在运行时从字节流中重建出具体数据结构,将会有一定程度的性能损失
  但是优势也是很明显的:
    1.放弃编译后代码的操作数对齐 也就省略很多填充和间隔符号;
    2.限制长度和放弃对齐也尽可能的让编译后的代码短小精干。
 
字节码与数据类型
    在Java虚拟机指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如,iload 指令用于从局部变量表中加载int 型的数据到操作数栈中。

    但是由于虚拟机操作码长度只有一个字节,所以包含了数据类型的操作码就为指令集的设计带来了很大的压力:

    如果每一种数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令集的数据就会超过256个了。因此虚拟机只提供了有限的指令集来支持所有的数据类型。


    JVM共有9种基本类型,对于基本类型  指令在设计的时候都用一个字母缩写来指代(boolean除外)。

      

     当然也有一些并没有明确字母指代数据类型,比如arraylength 指令,并没有代表数据类型的特殊字符,操作数只能是一个数组类型的对象,另外还有一些,比如无条件跳转指令goto 则是与数据类型无关的。


 

指令-相关计算机英语词汇含义

 


指令-数据类型相关的指令

 

 

     从上表的空白处可以看得出来:大部分数据类型相关联的指令,都没有支持整数类型 byte char short ,而且没有任何指令支持boolean类型。

    因为编译器会在编译期或者运行期  将byteshort 类型的数据 带符号扩展 为相应的int类型数据。

    类似的,booleanchar类型数据零位扩展为相应的int类型数据,在处理boolean byte short char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理


 

 按照逻辑功能进行划分

 加载存储指令

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

   这类指令包括如下内容:

    1. 将一个局部变量加载到操作栈iload、iload_<n>、lload、lload_<n>、fload、fload<n>、dload、dload<n>、aload、aload<n>   

    2. 将一个数值从操作数栈存储到局部变量表istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>

    3. 将一个常量加载到操作数栈bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

    4. 扩充局部变量表的访问索引的指令wide

  存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作的,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈运输数据。


 运算指令

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

  算术指令分为两种:

    1. 整型运算的指令。

    2. 浮点型运算的指令。

  无论是哪种算术指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,使用操作int类型的指令代替。

    加法指令: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、ishr、iushr、lshl、lshr、lushr
    按位或指令:ior、lor
    按位与指令:iand、land
    按位异或指令:ixor、lxor
    局部变量自增指令:iinc
    比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp


类型转换指令
   类型转换指令可以将两种不同的数值类型进行相互转换。这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来解决字节码指令集不完备的问题。

   因为数据类型相关指令无法与数据类型一一对应的问题,比如byte short char boolean使用int,   所以必须要转换。

   分为宽化窄化含义如字面含义,存储长度的变宽或者变窄。宽化也就是常说的安全转换,不会因为超过目标类型最大值丢失信息。窄化则意味着很可能会丢失信息。

 

   宽化指令

  • int类型到long、float或者double类型 (i2l、i2f、i2d)
  • long类型到float、double类型(l2f 、l2d)
  • float类型到double类型(f2d)

   窄化指令

  • int类型到byte short char类型(i2b 、i2s 、i2c)
  • long类型到int类型(l2i)
  • float类型到int或者long类型(f2i 、f2l)
  • double类型到int long 或者float类型(d2i 、d2l 、d2f)

对象创建与访问指令

  类实例和数组都是对象。但是Java虚拟机对类实例和数组的创建使用了不同的字节码指令。

     指令如下:

  1. 创建类实例 : new

  2. 创建数组的指令 : 

    newarray  分配数据成员类型为基本数据类型的新数组
    anewarray  分配数据成员类型为引用类型的新数组
    multianewarray   分配新的多维数组

   3. 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例字段)的指令:

    getstatic 从类中获取静态字段
    putstatic 设置类中静态字段的值
    getfield 从对象中获取字段值
    putfield 设置对象中的字段的值

   4. 把一个数组元素加载到操作数栈的指令:baload  caload  saload  iaload  laload  faload  daload  aaload

   5. 把一个操作数栈的值存储到数组元素中的指令:bastore  castore  sastore  iastore  lastore  fastore  dastore  aastor

   6. 取数组长度的指令:arraylength

   7. 检查类实例或者数组类型的指令:instanceof  checkcast


 操作数栈管理指令

    操作数栈管理指令,顾名思义就是直接用于管理操作栈的,操作数栈的直接操作主要有 出栈/复制栈顶元素 / 以及 交换栈顶元素。

    如同操作应普通数据结构中的堆栈那样,java虚拟机提供了一些用于直接操作数栈的指令,包括:

    1. 将操作数栈的栈顶一个或两个元素出栈:pop、pop2。

    2. 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2.

    3. 将栈最顶端的两个数值互换:swap。


 控制转移指令

  控制转移指令可以让Java虚拟机有条件或者无条件的从指定的位置指令继续执行程序,而不是当前控制转移指令的下一条。

控制指令如下:

  1. 条件分支

ifeq  当栈顶int类型元素    等于0时    ,跳转
ifne  当栈顶int类型元素    不等于0    时,跳转
iflt    当栈顶int类型元素    小于0    时,跳转
ifle    当栈顶int类型元素    小于等于0    时,跳转
ifgt   当栈顶int类型元素    大于0    时,跳转
ifge  当栈顶int类型元素    大于等于0    时,跳转
if_icmpeq    比较栈顶两个int类型数值的大小 ,当前者  等于  后者时,跳转
if_icmpne    比较栈顶两个int类型数值的大小 ,当前者  不等于  后者时,跳转
if_icmplt      比较栈顶两个int类型数值的大小 ,当前者  小于  后者时,跳转
if_icmple    比较栈顶两个int类型数值的大小 ,当前者  小于等于  后者时,跳转
if_icmpge    比较栈顶两个int类型数值的大小 ,当前者  大于等于  后者时,跳转
if_icmpgt    比较栈顶两个int类型数值的大小 ,当前者  大于  后者时,跳转
if_acmpeq  比较栈顶两个引用类型数值的大小 ,当前者  等于  后者时,跳转
if_acmpne  比较栈顶两个引用类型数值的大小 ,当前者  不等于  后者时,跳转

  2.复合条件分支

tableswitch    switch 条件跳转 case值连续
lookupswitch  switch 条件跳转 case值不连续

  3.无条件分支 

goto    无条件跳转
goto_w   无条件跳转  宽索引
jsr      SE6之前 finally字句使用 跳转到指定16位的offset,并将jsr下一条指令地址压入栈顶
jsr_w    SE6之前 同上  宽索引
ret      SE6之前返回由指定的局部变量所给出的指令地址(一般配合jsr  jsr_w使用)
w        同局部变量的宽索引含

 方法调用和返回指令

   方法调用分为:实例方法,接口方法,调用父类私有实例,初始化等特殊方法,类静态方法等

以下5条指令用于方法调用:
invokevirtual指令用于调用对象的实例方法
invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)
invokedynamic 调用动态链接方法

异常处理指令

  Java程序中显式抛出异常的操作  throw语句,都是由athrow 指令来实现的,除了throw语句显式的抛出异常情况之外,

  Java虚拟机规范还规定了许多运行时异常,会在其他Java虚拟机指令检测到异常情况时,自动抛出。


同步指令

   java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

  同步一段指令集序列通常是由Java语言中的synchronized 语句块来表示的,

  Java虚拟机的指令集中有monitorenter  monitorexit  (monitor  +enter/exit)

指令小结:

加载存储指令 将数据在栈帧中的局部变量表和操作数栈之间来回传输。
运算指令 用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。
类型转换指令 将两种不同的数值类型进行相互转换。
对象创建和访问指令 ....
操作数栈管理指令 用于管理操作栈的,操作数栈的直接操作主要有 出栈/复制栈顶元素 / 以及 交换栈顶元素。
控制转移指令 让Java虚拟机有条件或者无条件的从指定的位置指令继续执行程序,而不是当前控制转移指令的下一条。条件分支、复合条件分支、无条件分支
方法调用和返回指令  
异常处理指令  
同步指令  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2019-11-18 23:10  王大军  阅读(206)  评论(0编辑  收藏  举报