Java虚拟机(一)Java虚拟机初步了解

Java virtual Machine官方简介

The Java Virtual Machine

  1. Java虚拟机是Java平台的基石。是java实现硬件和操作系统的独立性(也就是无关性),生成极小体积的编译代码,以及保护用户免受恶意程序攻击的的关键技术组成部分。
  2. Java虚拟机是一种抽象计算机器。像真正的计算机一样,它有一个指令集以及在运行时各种内存区域。
    使用虚拟机实现编程语言是相当普遍的;
    最著名的虚拟机可能是 P-Code machine of UCSD Pascal.
  3. Java虚拟机的第一个原型实现是在Sun Microsystems inc.完成的,它在手持设备上仿真实现了Java虚拟机指令集,该设备类似于现在的个人数字助理(PDA)。Oracle现在已经将许多java虚拟机实现应用于 移动设备 pc 服务器等领域,但是Java虚拟机不局限于任何特定的实现技术、主机硬件或主机操作系统。也不局限于特定的代码执行方式,虽然不强求使用解释器来执行程序,但是也可以通过将它的指令集编译成一个实际的CPU来实现。它也可以在微代码microcode实现,也可以直接在CPU中实现。
  4. Java虚拟机对Java编程语言一无所知,只知道一种特定的二进制格式,class文件格式(其实就是Java虚拟机的“机器语言”)。一个class文件包含Java虚拟机指令集(或者称之为字节码bytecode)和符号表,以及其他辅助信息。
  5. 基于安全方面的考虑,Java虚拟机在class文件中施加了许多强制性的语法和结构化约束,凡是能用class文件正确表达出来的编程语言,都可以放在Java虚拟机里面执行。由于它是一个通用的、机器无关的执行平台,所以其他语言的实现者都可以考虑将Java虚拟机作为那些语言的交付媒介。

相关性和无关性

JVM:平台相关,不同的平台有不同的虚拟机。
JVM虚拟指令集:不同的平台提供统一的JVM虚拟指令集
JVM本身的平台相关性–>Java语言的平台无关性
class文件:源代码编译后,与平台无关的二进制文件格式【任何一种语言之遥能编译成符合规范的class文件格式,就能被JVM进行执行】

Java语言实现和运行原理

Java语言解决问题的方式:【Java语言规范】和【应用程序编程接口】
Java运行原理:【class文件格式】和【Java虚拟机】
对于一个已经定义好的class文件,JVM需要进行一系列严格的验证,因为可能class文件不是由一个标准的编译器生成也可能被其他人恶意篡改过。

Javap命令

在这里插入图片描述
class文件就是一张巨大的表,信息是单独存放的,可以把行号#XXX理解成地址,从头不断根据地址信息串联起来,直到最终的叶子信息,形成一张巨大的信息表在这里插入图片描述
在这里插入图片描述
JVM要做的就是解析执行了。

Java虚拟机

JVM既然是虚拟机,终归要运行在物理机器上,在操作系统上体现出来的就是一个进程,操作系统会给他分配资源,割一块内存作为它的地盘。class文件是静态的,想要运行程序,JVM需要将class文件中的信息加载到它的地盘,然后它可以处理数据类型的数据。

Java虚拟机可以处理的数据类型
  • 虚拟机可以处理的数据类型分为【所以对于值,也就存在基本类型值和引用类型值】
  1. 基本类型
    • 基本类型:数值类型/boolean/returnAddress三种
    • 数值类型:整数类型/浮点数类型【该两种类型与Java语言中的值域在任何地方都是一致的】
    • boolean编译后使用Java虚拟机中的int数据类型代替,不过Java虚拟机支持boolean类型的数组,0表示false,1表示true
    • returnAddress在Java语言中不存在相应的类型,即程序员不能使用这个类型,也不能在程序运行期间更改
  2. 引用类型
    • 类类型:对类实例的引用
    • 接口类型:对实现了该接口的某个类实例的引用
    • 数组类型:对数组对象的引用
    • 特殊引用:null
      在这里插入图片描述
运行时数据区的内存模型

程序要运行必须装载数据到内存,class文件经classLoader加载到JVM运行时数据区域,程序的运行需要数据,方法,还要知道从哪里开始执行。程序计数器就是指向要执行的指令地址,标志从哪个位置开始执行;栈是方法调用概念的具体化数据结构,描述了怎么执行;堆用于保存程序运行时需要用到的数据对象等,描述了执行什么,操作什么。
在这里插入图片描述

内存结构个部分详情

一个运行时的Java虚拟机就是负责一个Java程序的运行。启动一个Java程序一个虚拟机实例也就诞生,当这个虚拟机关闭,这个虚拟机实例就销毁。每个Java程序都运行在属于他自己的Java虚拟机实例中。在一个虚拟机实例中,堆和方法区是这个Java程序的所有线程共享的,Java虚拟机栈,本地方法栈,程序计数器是线程隔离独有的。
在这里插入图片描述

方法区
  • 方法区是可供各个线程共享的运行时内存区域,存储了每一个类的结构信息
  • 方法区在虚拟机启动的时候创建
  • 方法区也可以被垃圾收集
  • 方法区大小可以是不固定的,可以根据需要动态扩展
  • 方法区空间也不必是连续的
具体存储信息内容
类型信息类的全限定名/类型的直接超类全限定名/类型 类还是接口/访问修饰符/直接超接口的全限定名
字段信息字段名/字段类型/字段的修饰符
方法信息方法名/方法返回类型/方法的参数数量和类型/方法的修饰符/方法的字节码(有方法体的)/操作数栈和该方法栈帧中的局部变量表的大小(其实也还是class文件属性表的内容 静态的)
除了常量以外的所有类变量类变量是所有类实例共享的,即使没有任何类实例,他也可以被访问,这些变量仅仅和类有关,所以类变量总是作为类型信息的一部分存储在方法区。除了在类中声明的编译时常量外,虚拟机使用某个类之前 必须在方法区中为这些类分配空间,编译时常量指的是final声明以及用编译时已知的值初始化的类变量,这种和一般的类变量还不一样,每个使用编译时常量的类型,都会复制他的所有常量到自己的常量池中 或者嵌入到他的字节码流中,说白了对于这种值不变的,直接复制过去
类ClassLoader的引用/Class类的引用每个类被装载后都必须跟踪他是由哪个类加载器加载的,对于每个被装载的类型,不管是类还是接口,虚拟机都会相应的为他创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来
运行时常量池运行时常量池属于方法区的一部分,class文件中每一个类或者接口的常量池表 constant_pool table 运行时的表示形式,只需要记住与class文件中的constant_pool相对应即可理解所包含的内容,包括了若干种不同的常量,从编译器可知的数值字面量到必须在运行期解析后才能获得的方法或字段引用

总结
所有的类型信息,静态数据信息,都加载到方法区;类加载器以及当前Class对象这种运行时必须的信息,也被保存在方法区

  • 一个java程序独占一个虚拟机实例,也就是每个java程序一个独立的堆空间。但是对于同一个java程序,堆是各个线程共享的运行时内存区域,是所有类实例和数组对象分配内存的区域
  • Java堆在虚拟机启动时就被创建了,存储了被自动内存管理系统 也就是GC( garbage Collector) 所管理的各种对象,这些受管理的对象不需要也也不能显式的销毁。之所以这么说是因为有分配新对象的指令,却没有释放内存的指令,所以就不能显式的销毁,堆是垃圾收集器工作的主要区域
  • 堆空间不必连续也可以动态扩展或者收缩,对象的内部表示形式,规范并没有规定 实现者可以按需发挥
    在这里插入图片描述

Java虚拟机栈

  • Java虚拟机栈是对方法调用这个抽象概念的具体化描述,方法执行的内存模型,启动一个新线程,Java虚拟机就会给它分配一个Java栈,用于保存栈帧,虚拟机只会对Java栈执行出栈和入栈两种操作
  • 每个方法在执行的同时都会创建一个栈帧 栈帧用于存储局部变量表 操作数栈 动态链接 方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程
  • 栈上所有的数据都是线程私有的任何线程都不能访问另一个线程的栈数据;也就是说,完全无需考虑多线程情况下数据的访问同步问题。当一个线程调用另一个方法时,方法的局部变量保存在调用线程的Java虚拟机栈的栈帧中只有一个线程总是能访问那些局部变量即调用方法的线程

本地方法栈

  • 本地方法栈并不是虚拟机明确定义的,是可选的
  • Java虚拟机实现可能会使用到传统的栈(通常称之为C stack) 来支持native方法(指 用Java以外的其他语言编写的方法),这个栈就是本地方法栈。 当Java虚拟机使用其他语言比如C语言,来实现指令集解释器的时候,也可以使用本地方法栈
  • 如果Java虚拟机本身不支持native方法,或是本身不依赖传统栈,那么可以不提供本地方法栈;如果支持本地方法栈,那这个栈一般会在创建线程的时候按线程分配

程序计数器

  • 程序计数器 又叫做 PC寄存器 PC为program counter,保存当前正在执行的指令的地址,可以看做是当前线程所执行的字节码的行号指示器,也就是程序的运行完全依赖PC寄存器,需要依靠他获取下一条需要执行的字节码指令
  • JVM的多线程时通过线程轮流切换并分配处理器执行时间片的方式实现的,在任何一个确定的时刻一个处理器(一个内核) 都只能执行一条线程中的指令,为了线程切换后能恢复到正确的位置,每个线程都需要一个独立的程序计数器,所以程序计数器是线程私有的 线程启动时创建
  • 如果执行的是Java方法,值为正在执行的虚拟机的字节码指令地址
    如果是Native方法,计数器值为空 Undefined ,此区域没有OOM(OutOfMemory)

直接内存

  • 直接内存并不是虚拟机运行时的数据区,也不是Java虚拟机规范中定义的内存区,但是这部分内存也被频繁的调用,也可能导致OOM
  • 是引入NIO后,引入的一种基于通道与缓冲区的IO方式,可以使用native 函数库直接分配堆外内存(也就是说不在Java虚拟机分配的内存上工作,在本地物理机上直接分配内存),然后通过Java堆中DirectByteBuffer对象作为这块内存的引用进行操作,能在一些场景中显著提高性能
  • 既然不属于java堆,自然不受制于Java堆大小的限制,但是,必须运行于物理机,自然受制于本机总内存大小

JVM运行时的内存结构,就是为了执行字节码文件,而将class文件中的信息加载到内存中的一个逻辑映射。class文件是源代码的静态抽象的数据结构描述,运行时内存结构是对于class文件的执行行为的结构描述。以上所有的要求说明都是属于规范上的并不要求所有的实现与规范中定义的抽象元素完全的对应起来。抽象的内部组件和行为的描述,仅仅是定义Java虚拟机所应该呈现出来的外部行为,也就是说,一个具体的虚拟机实现,可能与我们说过的规范相同,也可能与规范有出入。但是只要他的外部行为是一致的,正确识别class文件,遵守class文件中包含的Java代码的语义,能够按照规定所需要呈现出来的行为结果执行字节码文件即可,至于方法区到底应该如何分配空间,对象的内部表现形式如何,垃圾收集器如何运作,如何加载类都是由设计者来决定实现的.

举一个浅显的例子
你去超市购物,为了方便携带,你可能会按照他们的形状或者类别组织放到购物袋里面,比如 生鲜放到一个袋子,零食放到一个袋子,或者放置稍大商品的购物袋里面的缝隙处,放置一些小的商品,这是属于所有商品的静态描述组织
回到家需要做饭,可能你会把鱼拿出来放到盘子里,可能你会把青菜放到水槽中浸泡清洗,然后你可能会准备作料,洗锅准备做菜等等
一切都按照你下厨的习惯来放置食材以及步骤进行做菜
这就是属于动态执行行为的结构描述
我们的内存结构 程序计数器 堆 栈 就是对于代码执行行为过程的一种描述
可以理解你想要先做那道菜? 程序计数器((如果把想要做的菜都列一个清单,程序计数器就是从什么位置开始做,就是先做哪道菜)
都有哪些食材? 台面上有青菜 鱼 豆腐… 这都是存放在堆中
具体的怎么做? 红烧还是清蒸?这些具体的行为封装在虚拟机栈的栈帧中 每次做一道菜就是入栈,做好了刷锅就是出栈
而每道菜所需要的调味料和配菜可能是独有的,不能乱放,这些就相当于栈帧中的局部变量和操作数栈

Java指令

  1. 计算机的指令就是指挥机器工作的指令和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程
  2. 通常一条指令包含两方面的内容:操作码和操作数,操作码决定要完成的操作,操作数指参加运算的数据及所在的单元地址
  3. 虚拟机的字节码指令亦是如此含义;class文件相当于JVM的机器语言;class文件是源代码信息的完整表述;方法内的代码被保存到code属性中,字节码指令序列就是方法的调用过程
  4. Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode);以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成;虚拟机中许多指令并不包含操作数.只有一个操作码。
    在这里插入图片描述
  • 操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将大端排序存储,即高位在前的字节序。
    例如,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte1和byte2 )那这个16位无符号整数的值就是: (byte1<<8) | byte2.

  • 字节码指令流应当都是单字节对齐的,只有,tableswitch和lookupswitch两个指令例外 这俩货是4字节为单位的

  • 限制了操作码长度为一个字节 0~255, 但是也就导致操作码个数不能超过256

  • 放弃编译后代码的操作数对齐 也就省略很多填充和间隔符号
    【限制长度和放弃对齐也尽可能的让编译后的代码短小精干】

  • 但是如果向上面那样如果操作码处理超过一个字节的数据时,就必须在运行时从字节流中重建出具体数据结构,将会有一定程度的性能损失

指令详解

操作码是一个字节长度,也就是8位二进制数字,也就是两位16进制数字;class文件只会出现数字形式的操作码,只是便于人的识别,所以才会有助记符,但实际执行的时候不存在助记符

  • 指令本身是为了逻辑功能运算,运算自然要处理数据,所以说指令的设计是逻辑功能点和数据类型的结合
数据类型

在这里插入图片描述
在这里插入图片描述

逻辑功能
加载存储功能
算数指令
类型转换指令
对象的创建与操作
操作数栈管理指令
控制转移指令
方法调用和返回指令
抛出异常
同步

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

在不同的分类中,有些指令是重复的,因为有很多操作是需要处理数据的。也就是说,数据类型相关的指令里面可能跟很多逻辑功能点相关联,比如 加载存储指令,可以加载int 可以加载long等,可能不仅仅会出现在数据类型相关的指令中,也会出现在加载存储指令的介绍中,请不要疑惑,就是要从多维度介绍这些指令,才能更好地理解他们

在这里插入图片描述

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

Java中的操作码长度只有一个字节,所以必然的不会所有的类型都有对应的操作;Java虚拟机指令集对于特定的操作只提供了有限的类型相关指令;有一些单纯的指令可以在必要的时候用来将一些不支持的类型转换为可支持的类型
在这里插入图片描述
大部分数据类型相关联的指令,都没有支持整数类型byte\char\short,而且没有任何指令支持boolean类型
原因:编译器会在编译器或者运行期,将byte和short类型数据带符号扩展为相应的int类型数据;boolean和char类型数据零位扩展为相应的int类型数据
在处理boolean、byte、short、char类型数组时,也会转换为使用对应的int类型的字节码指令来处理
在这里插入图片描述

实际类型与运算类型的 关系

在这里插入图片描述

指令-按逻辑功能进行划分

加载存储指令

【加载存储指令用于局部变量与操作数栈交换数据以及常量装载到操作数栈】

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

说明:

  • 操作数为局部变量的位置序号,序号从0开始,局部变量以slot为单位分配的;
  • 将序号为操作数的局部变量slot的值加载到操作数栈;
  • 指令可以读作:将第(操作数+1)个X(i l f d a)类型局部变量,推送到栈顶【操作数+1 是因为序号是从0开始的】
  1. 将一个数据从操作数栈存储到局部变量表
istore istore_<n> lstore lstore_<n> fstore fstore_<n> dstore dstore_<n> astore astore_<n>
  • 操作数为局部变量的位置序号,序号从0开始,局部变量以slot为单位分配的
  • 将操作数栈的值保存到序号为操作数的局部变量slot中
  • 指令可以读作:将栈顶X(i l f d a)类型的数值保存到第(操作数+1)个局部变量中
  1. 将一个常量加载到操作数栈
bipush sipush 
ldc ldc_w ldc2_w
aconst_null 
iconst_m1 iconst_<i>
lconst_<l>
fconst_<f>
dconst_<d>
  • 操作数为将要操作的数值或者常量池号
  • 指令可以读作:将类型X的值xxx推送至栈顶或者将行号为xxx的常量推送至栈顶
  1. 扩充局部变量表的访问索引的指令:wide
  • 宽索引:扩展局部变量数,将8位的索引扩展成16位(65536)
  • 形式:wide 要被扩展的操作码比如iload 操作数 (wide iload 257 也就是 wide iload byte1 byte2)
    iload操作码是作为wide 操作码的一个操作数来执行的;wide可以修饰 load store ret,如果wide修饰的是iinc 格式有些变化 :wide iinc byte1 byte2 constbyte1 constbyte2 本身 iinc为 iinc byte constbyte
    扩展后的前两个字节16位为局部变量索引
    后两个字节16位计算为 16位带符号的增量
    计算的形式依旧是 (constbyte1 << 8) | constbyte2
  1. 形如xxx_<n>以尖括号结尾的代表了一组指令(eg:iload_0. iload_1. iload_2. iload3)这一组指令都是某个带有一个操作数的通用指令(如iload)的通用形式
  2. 对于这些特殊形式来说,它们表面上没有操作数,实际上操作数隐含在指令里面来,除此之外,语义与原指令并没有任何的不同(eg:iload_0的语义与操作数为0时的iload语义完全相同)
  3. <>中的字母表示了指令隐含操作数的数据类型
  4. <n> 表示非负整数<i>表示int,<l>表示long<d>double,而byte char short类型 的数据经常使用int来表示;下划线_后面紧跟的值就是操作数
  5. 需要注意的是 <n> 的形式不是无限的,对于load 和 store系列指令对于>3下标是4 往后都是直接只用原始形式 iload 4 不再使用的形式 所以你不会看到 load_4 load_5…或者store_4 store_5…
数据交换模型

在这里插入图片描述

  • 对于虚拟机执行方法来说,操作数栈是工作区,所以数据的流向是针对 操作数栈来说的:load就是局部变量加载到操作数栈,store就是从操作数栈存储到局部变量表,对于常量来说只有加载到操作数栈进行使用,没有存储的说法,比较特殊
  • 对于上图中的数据交换模型中,操作数栈是可以确定的也是唯一的,栈就在那里,不管你见或不见
    对于操作数栈与局部变量交换数据时,需要确定的是从哪个局部变量取数据或者保存到哪个局部变量中 所以load 和 store的操作数都是局部变量的位置
  • 对于操作数栈与常量交换数据,需要确定的是到底加载哪个值到操作数栈或者是从常量池哪行加载,所以加载常量到操作数栈的操作数 是 具体的数值或者常量池行号
常量加载到操作数栈比较特殊的单独说明
  • 根据数据类型以及数据的取值范围使用了不同的方式
  1. const指令:主要负责把简单的数值类型送到栈顶。对于int只能把-1 0 1 2 3 4 5 送到栈顶,其他的数值使用push系列指令
    简言之,取值-1~5时,JVM采用const指令将常量压入栈顶。
  2. push指令:负责把一个整型数字(长度比较小)送到栈顶;该系列命令有一个参数,用于指定要送到栈顶的数字
    【注意该系列指令只能操作一定范围内的整数值,超出该范围的使用将使用ldc命令系列】
    在这里插入图片描述
  3. ldc系列:负责把数值常量或String常量从常量池中推送至栈顶,该命令后面需要给一个表示常量在常量池中位置(编号)的参数即行号
    对于const系列命令和push系列命令操作范围之外的数值类型常量,都放在常量池中
    在这里插入图片描述
    eg:final static int id = 32768;//32767+1就不在sipush范围内了
    在这里插入图片描述
算术指令
  • 运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶
  • 算术指令分两种:整型运算的指令和浮点运算的指令
  • 无论是哪种算术指令,都使用Java虚拟机的数据类型;由于没有直接支持byte、short、char和boolean类型的算术指令,使用操作int类型的指令代替
    在这里插入图片描述
    补充说明:
  1. 加 减 乘 除 求余 取反 支持<int i long l float f double b>四种类型;==常用操作支持四种常用类型 byte short char boolean使用int
  2. 移位运算与按位与或异或运算,支持<int i long l>;
  3. 移位与位运算支持整型,byte char char boolean使用int,另外还有long
    自增支持 <int i>
    在这里插入图片描述
  • 关于移位运算:
    1. 左移只有一种:丢弃高位,往左移位,右边空出来的位置补0
    2. 算术右移有两种:
      【1】逻辑右移:丢弃最低位,向右移位,左边空出来的位置补0
      【2】算术右移:丢弃最低位,向右移位,左边空出来的位置补原来的符号位(即原来的最高位)
类型转换指令
  • 类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来解决字节码指令集不完备的问题(因为数据类型相关指令无法与数据类型一一对应的问题,比如byte char short boolean使用int,所以必须要转换
  • 分为宽化窄化;含义如字面含义,存储长度的变宽或者变窄。宽化也就是常说的安全转换,不会因为超过目标类型最大值丢失信息
    ;窄化则意味着很可能会丢失信息,宽化指令和窄化指令的形式为 【操作类型 2 (to) 目标类型 比如 i2l int 转换为long】
    在这里插入图片描述
对象的创建与访问
  • 实例和数组都是对象,但是Java虚拟机对类实例和数组的创建使用了不同的字节码指令
  • 涉及到对象的创建与访问的相关操作:
    1. 创建实例对象/数组
    2. 访问实例变量和类变量
    3. 加载与存储,对于类实例属于引用类型,加载使用加载存储指令,所以此处只有数组相关的操作
    4. 还有一些附属信息,数组长度以及检查类实例或者数组类型
      在这里插入图片描述
      在这里插入图片描述
控制转移指令
  • 转移控制指令可以让Java虚拟机有条件或无条件的从指定的位置指令继续执行程序而不是当前控制转移指令的下一条
  • 控制转移包括:条件转移、复合条件转移以及无条件转移
  • boolean byte short char都是使用int类型的比较指令;long float double类型的条件分支比较,会先执行相应的比较运算指令,运算指令会返回一个整型数值到操作数栈,然后再执行int类型的条件分支比较来完成整个分支跳转
  • 虚拟机对int类型的支持最丰富,所有的int类型的条件分支指令进行的都是有符号的比较
    在这里插入图片描述
    在这里插入图片描述
操作数栈管理指令
  • 直接用于管理操作栈的,对于操作数栈的直接操作主要有:出栈/复制栈顶元素/交换栈顶元素
  • 出栈pop:分为将操作数栈栈顶的几个元素出栈,一个或者两个,pop表示出栈,数值代表个数 pop pop2
  • 交换swap:将栈顶的两个元素交换
  • 复制dup:根本含义为复制栈顶元素然后入栈,不过涉及到复制几个元素,以及操作数栈的数据类型,所以比较复杂
复制指令具体说明
dup复制操作数栈顶元素,并且将此值压入栈顶,value必须为分类1(int returnAddress reference等)【形式:... ,value. ...,value,value
dup_x1复制操作数栈顶一个元素,插入栈顶以下两个值之后 value1插入到value2之后,二者必须分类1 【形式:...,value2,value1. ...,value1,value2,value1
dup_x2复制操作数栈顶的一个元素,插入栈顶以下2个或3个值之后 三者全是分类1插入value3之后【形式1:...value3,value2,value1->. ...value1,value3,value2,value1】value1是分类1,value2是分类2【...,value1,value2,value1 ...,value2,value1->
dup2复制操作数栈顶1个或2个元素,并按照原顺序,入栈到操作数栈 如果value1,value2全部是分类1 【形式:...,value2,value1->. ...,value2,value1,value2,value1】value是分类2,复制栈顶一个元素,插入到栈顶 【形式:...,value2-> ...,value,value
dup2_x1复制操作数栈顶1个或2个元素,按照原有顺序,插入到栈顶以下2或3个值之后 value3,value2,value1都是分类1,复制两个元素,插入栈顶下,即value3之后【形式:...,value3,value2,value1 -> ...,value2,value1,value3,value2,value1】value1是分类2 value2是分类1 复制一个元素,插入到栈顶以下2个元素之后【形式...value2,value1 ->. ...value1,value2,value1
dup2_x2复制操作数栈顶1个或2个元素,并按照原顺序,插入栈顶以下2,3或4个值之后 全都是分类1 , 复制两个元素,插入到栈顶 第四个值后面【形式:..., value4, value3, value2, value1 → ..., value2, value1, value4, value3, value2, value1】 如果 value1 是分类2 value2 和 value3 是分类1 中的数据类型 复制一个元素 插入到栈顶 第三个值后面【形式..., value3, value2, value1 → ..., value1, value3, value2, value1】 如果value 1 value2 是分类1 value3 是分类2 复制两个元素 插入到栈顶 第三个值后面【形式:..., value3, value2, value1 →..., value2, value1, value3, value2, value1】 当value1 和value2 都是分类2 复制一个元素 插入到栈顶 第二个值后面【形式:…., value2, value1 →..., value1, value2, value1

【上述内容着实难以记忆,所以要总结事物的一般规律,不仅是表面形式上的规律,而且要是内部的规律】
理解方式:
已知局部变量空间的分配有两种:1个字节,2个字节;上述的数据类型的分类便是据此进行分类的,也就是说如果是类型2,压入的时候要占用两份字节,而类型1只需要占用1个字节,把握了这个规律,就明白上述的虚拟机规范规定的内容了
比如: 当value1 和value2 都是分类2 复制一个元素 插入到栈顶 第二个值后面【形式:…., value2, value1 →..., value1, value2, value1
这个时候value1占用2个字节,value2占用两个字节,占用数目相同,所以在复制的时候,可以直接插入到value2后面
【按实际内部空间的字节分配复制,按外部所见的直接数值进行插入】
在这里插入图片描述
对栈元素的处理,显然指的是对于栈元素内部数组的处理,所以自然要分为 【到底是直接复制一个单位的数据 ;还是直接复制两个单位的数据 】
一次复制占用一个单位空间 的指令 使用dup
一次复制占用两个单位空间 的指令 使用dup2
PART1:一次复制占用一个单位空间

  1. dup,dup 可以理解为dup_x0
    假设复制的栈顶是array[0] ,插入到他栈顶的内部线性结构的第(1+0)个元素下面 所以array[0] 对应的必然是一个完整的栈元素 ,必然是分类1 不可能是分类2的一半!
    在这里插入图片描述
  2. dup_x1
    插入到他栈顶的内部线性结构的第(1+1)个元素下面 也就是插到第二个下面 因为array[0] 对应value1为分类1
    如果接下来的是分类2的数据,必然接下来的两个单元array[1] 和array[2]是不可分割的,也就是不可能插入到array[1] 后面,所以array[1] 对应value2 也必须是分类1 也就是两个都是分类1
    在这里插入图片描述
  3. dup_x2
    插入到他栈顶的内部线性结构的第(1+2)个元素下面 也就是插到第三个后面,array[0] 对应value1为分类1 为分类1 .那么接下来的两个单位array[1] 和array[2],可以是一个分类2 也可以是两个分类1,都是可以的
    在这里插入图片描述
    在这里插入图片描述
    PART2:一次复制占用两个单位的数据类型
  4. dup2 可以理解为dup2_x0
    插入到他栈顶的内部线性结构的第(2+0)个元素下面 .这一次复制的两个单位array[0] 和 array[1], 到 array[1下面 ,可能是对应value1 和value2 表示两个分类1 也可能是对应一个value1 表示类型为分类2
    在这里插入图片描述
    在这里插入图片描述
  5. dup2_x1 插入到他栈顶的内部线性结构的第(2+1)个元素下面 也就是复制array[0] 和 array[1] 到第三个元素 array[2]的下面
    array[0] 和 array[1] 可能分别对应value1 和value2 表示两个分类1 数据 也可能是对应着一个value1表示一个分类2数据
    但是array[2] 作为第三个单位,既然能被分割,自然他必须是分类1
    所以要么三个都是分类1,要么value1 分类2 value2 分类1
    在这里插入图片描述
    在这里插入图片描述
  6. dup2_x2
    插入到他栈顶的内部线性结构的第(2+2)个元素下面 也就是复制array[0] 和 array[1] 到第四个内部元素 array[3]的下面
    一次复制两个,放到第四个下面
    这种情形下的组合就非常多了
    全都是分类1的数据
    在这里插入图片描述
    全部都是分类2
    array[0] 和 array[1] 对应value1 表示一个分类2数据
    array[2] 和 array[3] 对应value2 表示一个分类2数据
    在这里插入图片描述
    array[0] 和 array[1] 对应value1 表示一个分类2数据
    array[2] 和 array[3] 对应value2 和 value3表示两个分类1数据
    在这里插入图片描述
    array[0] 和 array[1] 对应value1 和value2 表示两个分类1 数据
    array[2] 和 array[3] 对应value3表示一个分类2数据
    在这里插入图片描述
    总结:当记忆规范时,有的时候不好理解可能是因为他是逆向考虑的,直接把答案告诉你了,这时候要记得反推。实际上,所有的dup指令,不过是根据栈元素的实际存放类型,梳理出来的一些复制一个或复制两个栈顶元素的实际操作方式。两个相邻的内部单元组合起来表示一个栈元素时,是不能拆分的。
方法调用和方法返回指令
  • 方法调用分为:实例方法 、接口方法、调用父类私有实例初始化等特殊方法、类静态方法等
  • 以下5条指令用于方法调用:
    1. invokevirtual:用于调用对象的实例方法
    2. invokeinterface:用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用
    3. invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
    4. invokestatic:用于调用类方法(static方法)
    5. invokedynamic:调用动态链接方法,比较复杂

在这里插入图片描述

异常指令
  • Java程序中显式抛出异常的操作 throw语句都是由athrow指令来实现的,除了throw语句显式抛出的异常情况之外,Java虚拟机规范还规定了许多运行时异常,会在Java虚拟机指令检测到异常情况时,自动抛出
同步指令
  • 同步一段指令集序列通常是用Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter monitorexit(monitor+enter/exit)

Java编译器如何编译代码

一条普通的指令格式

在这里插入图片描述
在这里插入图片描述

index(行号/偏移量)可以作为控制跳转指令的跳转目标,比如 goto 8 表示跳转到索引位8的指令上,javap查看到的内容,你可以认为是class文件表述的信息,但是绝不能理解为就是class文件中的内容

加载存储与算术指令

  • -1 ~ 5 使用const加载到操作数栈
  • -1 使用iconst_m1
  • -128~127 使用bipush
  • -32768~32767使用sipush
  • 其余常量池ldc
  • store从操作数栈保存到局部变量表
  • load加载局部变量到操作数栈

public static void main(String[] args) {
int i = -1;
int j = 3;
int k = 5;
int l = 127;
int m = 32767;
int n = 32768;
//操作
int add = i+j;
int sub = i-j;
int mul = j*k;
int div = j/k;
int rem = k%j;
int neg = ~j;
int inc = i++;
}

在这里插入图片描述
解释说明
0. 常量-1 加载到操作数栈
1 操作数栈保存到1号局部变量表 也就是 i = -1;
2 常量 3 加载到操作数栈
3. 操作数栈保存到2号局部变量表 也就是j = 3;
4. 常量 5 加载到操作数栈
5. 操作数栈保存到3号局部变量表 也就是k =5;
6 常量 127 加载到操作数栈
8 操作数栈保存到4号局部变量表 也就是l = 127;
10.常量 32767 加载到操作数栈
13.操作数栈保存到5号局部变量表 也就是m = 32767;
15.加载#17号常量池数据到操作数栈
17 操作数栈保存到6号局部变量表 也就是n = 32768;
19 加载1号局部变量到操作数栈 对应 i
20 加载2号局部变量到操作数栈 对应 j
21 执行iadd指令计算并将结果压入栈顶 对应 i+j;
22 保存栈顶元素到7号局部变量
24 加载1号局部变量到操作数栈 对应 i
25 加载2号局部变量到操作数栈 对应 j
26 执行isub指令计算并将结果压入栈顶 对应i-j;
27 保存栈顶元素减法结果到8号局部变量
29,30 加载 2号和3号局部变量到操作数栈 也就是j k
31 执行imul指令并将结果压栈 j*k
32 保存栈顶元素乘法结果到9号局部变量
34.35 加载 2号和3号局部变量到操作数栈 也就是j k
36 执行idiv 结果压入栈顶
37保存idiv结果到10号局部变量
39.40 加载3号 和 2号 也就是k j
41 执行求余irem 结果压入栈顶
42 栈顶元素结果保存到11号局部变量
44加载2号局部变量 对应 j 到操作数栈
45 加载常量-1到操作数栈
46 执行异或运算结果压入栈顶 (~x = -1 ^ x;)
47栈顶结果保存到12号局部变量
49 加载1号局部变量 对应 i
50 执行增量 1 计算 结果压入栈顶
53 栈顶结果保存到13号变量
55 void方法 return返回

类型转换指令


public static void main(String[] args) {
boolean bNum = true;
 
char cNum = 2;
byte byteNum = 127;
short sNum = 32767;
int iNum = 100;
long lNum = 65536;
float fNum = 2.5f;
double dNum = 6.8;
 
char c1 = (char)byteNum;
char c2 = (char)sNum;
char c3 = (char)iNum;
char c4 = (char)lNum;
char c5 = (char)fNum;
char c6 = (char)dNum;
 
byte b1 = (byte)cNum;
byte b2 = (byte)sNum;
byte b3 = (byte)iNum;
byte b4 = (byte)lNum;
byte b5 = (byte)fNum;
byte b6 = (byte)dNum;
 
short s1 = (short)cNum;
short s2 = (short)byteNum;
short s3 = (short)iNum;
short s4 = (short)lNum;
short s5 = (short)fNum;
short s6 = (short)dNum;
 
int i1 = (int)cNum;
int i2 = (int)byteNum;
int i3 = (int)sNum;
int i4 = (int)lNum;
int i5 = (int)fNum;
int i6 = (int)dNum;
 
long l1 = (long)byteNum;
long l2 = (long)cNum;
long l3 = (long)sNum;
long l4 = (long)iNum;
long l5 = (long)fNum;
long l6 = (long)dNum;
 
float f1 = (float)byteNum;
float f2 = (float)cNum;
float f3 = (float)sNum;
float f4 = (float)iNum;
float f5 = (float)lNum;
float f6 = (float)dNum;
 
double d1 = (double)byteNum;
double d2 = (double)cNum;
double d3 = (double)sNum;
double d4 = (double)iNum;
double d5 = (double)lNum;
double d6 = (double)fNum;
}
数据的加载与存储部分(一)

在这里插入图片描述
⚠️可以看出boolean内部使用的是数值1表示true

数据类型转换为char类型(二)

在这里插入图片描述
⚠️char byte short int 内部类型均为int ,转换成char直接用i2c
⚠️long float double先转换为int (l2i f2i d2i)然后再统一使用 i2c转换为char

数据类型转换为byte类型(三)

在这里插入图片描述
⚠️char byte short int 内部类型均为int ,转换成byte直接用i2b
⚠️long float double先转换为int (l2i f2i d2i)然后再统一使用 i2b转换为byte

数据类型转换为short类型(四)

在这里插入图片描述
⚠️char byte short int 内部类型均为int ,转换成byte直接用i2s
⚠️long float double先转换为int (l2i f2i d2i)然后再统一使用 i2s转换为short

数据类型转换为int类型(五)

在这里插入图片描述
⚠️char byte short 不需要转换
⚠️long float double直接用l2i f2i d2i

数据类型转换为float类型(六)

在这里插入图片描述
⚠️都是两步:char byte short int 直接转换成float;long double也是

数据类型转换成double类型(七)

在这里插入图片描述
⚠️都是两步:char byte short int 直接转换成double;long 也是

类相关指令

单纯对象的创建(一)
class Super{
}
class Sub extends Super{
} 
new Object();
new Super();
Super s = new Super();
new Double(1.5);
new Sub();
Sub sub = new Sub();

在这里插入图片描述 new Object();
new Super();
没有赋值给局部变量 仅仅是创建对象 调用new之后,堆中对象的引用保存在栈顶
然后调用构造方法invokespecial

Super s = new Super();
同上面的,需要调用new
因为还需要保存到局部变量
所以new之后 先copy一个,也就是dup
然后调用构造方法 invokespecial
然后从操作数栈保存到局部变量 store

涉及到父类和子类的对象建立(二)
Super super1 = new Super();
Sub sub = new Sub();
//父类引用可以指向子类
//子类引用不能指向父类
//但是对于指向子类的父类引用 可以通过类型转换为子类
Super subToSuper = sub;
Sub superToSub = (Sub) subToSuper;

在这里插入图片描述
操作过程:
0 创建Spper
3 复制
4 调用构造方法
7 保存到1号局部变量
8 创建Sub
11 复制
12调用构造方法
15 保存到2号局部变量
16 2号加载到操作数栈
17保存到3号局部变量
18加载3号局部变量到栈
19 checkcast 进行校验确认是否可以转换为指定类型 否则报错抛 classCastException
22 再次保存到局部变量

控制转移指令

循环语句(一)
void intWhile() {
int i = 0;
while (i < 100) {
i++;
}
}
void intDoWhile() {
int i = 0;
do {
i++;
}
while (i < 100);
}
void intFor() {
int j = 0;
for(int i =0;i<100;i++) {
j++;
}

在这里插入图片描述
过程说明:

  • intWhile()方法
    0.加载常量0 到操作数栈
    1.保存操作数栈元素到1号局部变量 i= 0;
    2.直接跳转到第8行
    8.1号局部变量加载到操作数栈 也就是i 作为第一个元素
    9.加载常量100到操作数栈 也就是100作为第二个元素
    11.比较大小,如果前者小于后者 也就是如果 i <100 满足 跳转到第5行 否则顺序执行到14 return
    5.给1号局部变量以增量1 增加
    然后 8–>9–>11–>5–>8–>9–>11…往复循环 直到条件不满足,从11 跳转到14 结束
  • intDoWhile()
    0.加载常量0到操作数栈
    1.保存常量0 到1号局部变量
    2.给1号局部变量以增量1 进行自增
    5.1号局部变量加载到操作数栈
    6.常量100加载到操作数栈
    8,比较大小 如果前者小于后者也就是 1号局部变量 i<100 跳转到第2行
    然后进行往复循环,直到条件不满足,然后顺序到return
  • intFor()
    0.加载常量0 到操作数栈
    1.保存栈顶元素到1号局部变量 j=0;
    2.加载常量0到操作数栈
    3.保存栈顶元素到2号局部变量i=0;
    4.跳转到13行
    13.加载2号局部变量到操作数栈
    14.加载常量100到操作数栈
    16.比较大小,如果前者 2号局部变量 i <100 跳转到7
    7.1号局部变量以增量1 自增 j++
    10.2号局部变量以增量1 自增 i++
    13.2号局部变量加载到操作数栈
    14.加载常量100到操作数栈
    16.比较大小,如果前者 2号局部变量 i <100 跳转到7
    往复循环 如果条件不满足 从16 顺序到19 结束方法 return
条件转移语句(二)
public void fun() {
int i = 0;
if(i<2) {
i++;
}else {
i--;
}
}

在这里插入图片描述
过程说明:
0.加载常量0 到栈顶
1.保存栈顶元素 (0) 到1号局部变量
2.加载1号局部变量到栈顶
3.加载常量2 到栈顶
4.比较
如果大于后者等于跳转到13 然后1号局部变量 自增1 然后下一步顺序到16 return
否则就是顺序执行到7 1号局部变量 增量为-1 自增运算 然后到10 ,10为跳转到16 return

方法调用相关指令

public void invoker() {
method(2);
}
 
public void method(int i) {
if(i>5) {
System.out.println(i);
}
}

在这里插入图片描述
过程说明:
invoker()
0,加载0号 局变量到栈 (上面基本都是第一个数据被保存到1号局部变量,0 号其实是被this 占用了)
1,加载常量2 到操作数栈
2.调用实例方法(I)V
5 return

method(int)
0. 加载1号局部变量到操作数栈
1.加载常量5 到操作数栈
2比较如果小于等于:跳转到12行 直接返回;
如果大于L那么顺序执行到5行 out 是类型为PrintStream的 System中的静态变量
8 加载1号局部变量到操作数栈
9 调用实例方法 println 是 PrintStream的实例方法 使用invokevirtual

⚠️先加载在前面出现的局部变量到操作数栈,然后再加载剩下的常量到操作数栈
⚠️把System.out.println分解,首先应该看out这个静态变量,getstatic,然后需要先应该加载函数需要的参数,然后才能实现函数的调用,所以先用iload_1,加载存储在1号局部变量表中存储的I值,然后才能使用invokevirtual调用函数,然后return返回

switch相关

switch中的参数为整型变量(一)
int i = 5;
int j = 6;
switch (i) {
case 1:
j = j + 1;
break;
case 3:
j = j + 2;
break;
case 5:
j = j + 3;
break;
default:
j = j + 4;
}

在这里插入图片描述
过程说明:
0,1,2,4 分别将 5 和 6 加载并存储到1号和2号局部变量
5.加载1号局部变量到栈 对应 switch (i) {
然后根据tableswitch 表 进行跳转
虽然我们只有1,3,5 但是设置了1到5 ,对于2 和 4 直接跳转到default

40: 2号局部变量 +1
顺序到43
43: 跳转到61 return
46: 2号局部变量 +2
顺序到49
49: 跳转到61 return
52: 2号局部变量 +3
顺序到55
55: 跳转到61 return
58 2号局部变量 +4
顺序到61 return

switch中的参数类型为String(二)
int j = 6;
String string = "hehe";
switch (string) {
case "A":
j = j + 1;
break;
case "hehe":
j = j + 2;
break;
case "C":
j = j + 3;
break;
default:
j = j + 4;
}

在这里插入图片描述
过程说明:
0 加载常量6到栈
1 保存到 1 号局部变量
3.加载常量池 #36 到栈
在这里插入图片描述
5 保存到2 号局部变量
6 加载2号局部变量 到栈
7 复制栈顶元素
8 复制的元素保存到3号局部变量
9 调用String实例化方法hashCode
12, lookupswitch表中,不在类似tableswitch 了,那个是连续的
lookupswitch 是不连续的
我们总共有三个case一个default
lookupswitch 总共有4项
“A” 的hashCode 为 65
“C” 的hashCode为 67
“hehe” 的hashCode为 3198650 不信的话,自己写个main打印下

经过12行 路由之后跳转到指定的序列
你会发现三个case他们的过程是一样的
加载3号局部变量 ,然后将常量 A C hehe 也加载到栈
然后调用equal方法进行比较

在这里插入图片描述

典型的例子

int i = 5;
int j = 8;
int k = i+j;
int l = 3+6;

在这里插入图片描述
过程说明:
前一部分:
0. 常量5 加载到栈
1,保存到 1号局部变量
2. 常量8 加载到栈
4 保存到2号 局部变量
5,加载1号局部变量
6, 加载2号局部变量
7 执行iadd 结果会压入栈顶
8 栈顶元素保存到3号局部变量
至此 完成了前三行代码

后一部分:
9.常量9 加载到栈 (3+6 已经被计算好了)
11,保存到4号局部变量
⚠️3+6是会被提前计算好再加入的!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值