笔记参考尚硅谷宋红康:
JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
5.对象的创建与访问指令
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持,有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访向指令、数组操作指令、类型检查指令。
1、创建指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:
1. 创建类实例的指令:
创建类实例的指令:new
它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
2. 创建数组的指令:
创建数组的指令:newarray、anewarray、multianewarray
- newarray:创建基本类型数组
- anewarray:创建引用类型数组
- multianewarray:创建多维数组
上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。
public void newInstance() {
Object obj = new Object();
File file = new File("yutian.txt");
}




下面与其类似,不再赘述。
注意一下invokespecial会弹出与其相对应的两个元素就行

再看数组:
public void newArray() {
int[] intArray = new int[10];
Object[] objArray = new Object[10];
int[][] mintArray = new int[10][10];
String[][] strArray = new String[10][];
}

此时,未指定二维的,就还是使用一维的指令


multianewarray指令:
示例代码:
package jvmgo.book.ch06;
public class ArrayDemoTest {
public void test(){
int[][][] x = new int[3][4][5];
}
}上面的Java方法创建了一个三维数组,编译之后的字节码如下:

multianewarray指令的第一个操作数是2,是个类引用,类名是[[[I,说明要创建的是int[][][]数组。第二个操作数是3,说明要创建三维数组。当方法执行时,三条iconst_n指令先后把整数3、4、5推入操作数栈顶。
multianewarray指令在解码时就已经拿到常量池索引#2和数组维度3。在执行时,它先查找运行时常量池索引,知道要创建的是int[][][]数组,接着从操作数栈中弹出三个int值,依次是5、4、3。现在multianewarray指令拿到了全部信息,从最外维开始创建数组实例即可。
2.字段访问指令
二、字段访问指令
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。
- 访问类字段(static字段,或者称为类变量)的指令: getstatic、 putstatic
- 访问类实例字段(非static字段,或者称为实例变量)的指令: getfield, putfield
举例:以getstatic指令为例,它含有一个操作数,为指向常量池的Fieldref索引,它的作用就是获取Fieldref指定的对象或者值,并将其压入操作数栈。
例如:
//2.字段访问指令
public void sayHello() {
System.out.println("hello");
}out是一个static字段,源码:

字节码指令:

上图:



public void setOrderId(){
Order order = new Order();
order.id = 1001;
System.out.println(order.id);
Order.name = "ORDER";
System.out.println(Order.name);
}
class Order{
int id;
static String name;
}

3.数组操作指令
三、数组操作指令
数组操作指令主要有: xastore和xaload指令。具体为:
- 把一个数组元素加载到操作数栈的指令: baload、 caload、 saload、 iaload、 laload、faload、daload, aaload
- 将一个操作数栈的值存储到数组元素中的指令(修改堆中):bastore、 castore、 sastore、 iastore、lastore、fastore, dastore, aastore
即(如下图:byte和boolean用同一个指令):

- 取数组长度的指令: arraylength,该指令弹出栈顶的数组元素(索引),获取数组的长度,将长度压入栈。
2. 说明
- 指令xaload表示将数组的元素压栈,比如saload、caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈。
- xastore则专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置。
//3.数组操作指令
public void setArray() {
int[] intArray = new int[10];
intArray[3] = 20;
System.out.println(intArray[1]);
boolean[] arr = new boolean[10];
arr[1] = true;
}
如图:三个元素

取数组长度的指令: arraylength,该指令弹出栈顶的数组元素(索引),获取数组的长度,将长度压入栈。
public void arrLength(){
double[] arr = new double[10];
System.out.println(arr.length);
}
4.类型检查指令
四、类型检查指令
检查类实例或数组类型的指令: instanceof、checkcast.
- 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常。
- 指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。
//4.类型检查指令
public String checkCast(Object obj) {
if (obj instanceof String) {
return (String) obj;
} else {
return null;
}
}ifeq是流程控制指令,之后会讲,这里知道满足执行,否则跳转至12( aconst_null)

6.方法调用与返回指令
1.方法调用指令
1.方法调用指令: invokevirtual、 invokeinterface、 invokespecial、invokestatic、invokedynamic
以下5条指令用于方法调用:
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) ,支持多态。这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法(super调用)。(注意,这三种方法不存在方法重写)这些方法都是静态类型绑定的,不会在调用时进行动态派发。
- invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。
- invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令,用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
//方法调用指令:invokespecial:静态分派
public void invoke1(){
//情况1:类实例构造器方法:<init>()
Date date = new Date();
Thread t1 = new Thread();
//情况2:父类的方法
super.toString();
//情况3:私有方法
methodPrivate();
}
private void methodPrivate(){
}
注意:
如果改为public,字节码会变化。因为可能会被子类重写,


//方法调用指令:invokestatic:静态分派
public void invoke2(){
methodStatic();
}
public static void methodStatic(){
}
如果改为private,字节码也不会变,因为通过当前类调用,优先考虑static

//方法调用指令:invokeinterface
public void invoke3(){
Thread t1 = new Thread();
((Runnable)t1).run();
Comparable<Integer> com = null;
com.compareTo(123);
}
//方法调用指令:invokeVirtual:动态分派
public void invoke4(){
System.out.println("hello");
Thread t1 = null;
t1.run();
}如果不强转,子类可能重写了,使用invokeVirtual:动态分派

注意:接口方法的调用
package com.yutian.java;
public class InterfaceMethodTest {
public static void main(String[] args) {
AA aa = new BB();
aa.method2();
AA.method1();
}
}
interface AA{
public static void method1(){
}
public default void method2(){
}
}
class BB implements AA{
public void method2(){
System.out.println("Hello");
}
}将接口的methon2方法改为abstract, 依然是invokeinterface

上述代码改为类
package com.yutian.java;
public class InterfaceMethodTest {
public static void main(String[] args) {
AA aa = new BB();
aa.method2();
AA.method1();
}
}
class AA{
public static void method1(){
}
public void method2(){
}
}
class BB extends AA{
public void method2(){
System.out.println("Hello");
}
}
2.方法返回指令:
方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。
- 包括ireturn (当返回值是boolean、 byte、 char、 short和int类型时使用) 、lreturn、 freturn、dreturn和areturn
- 另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

举例:
通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。
如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。
最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
//方法的返回指令
public int returnInt(){
int i = 500;
return i;
}public double returnDouble(){
return 0.0;
}public String returnString(){
return "hello,world";
}public int[] returnArr(){
return null;
}注意:int的i会转型成float
public float returnFloat(){
int i = 10;
return i;
}public byte returnByte(){
return 0;
}public void methodReturn(){
int i = returnByte();
}
7.操作数栈管理指令
操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。
这类指令包括如下内容:
- 将一个或两个元素从栈顶弹出,并且直接废弃: pop, pop2;
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2;
- 将栈最顶端的两个Slot数值位置交换: swap,Java虚拟机没有提供交换两个64位数据类型(long、 double)数值的指令。
- 指令nop,是一个非常特殊的指令,它的字节码为0x00,和汇编语言中的nop一样,它表示什么都不做,这条指令一般可用于调试、占位等。
这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
说明:
1.不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令, dup和dup2。dup的系数代表要复制的Slot个数。
- dup开头的指令用于复制1个Slot(4个字节)的数据。例如1个int或1个reference类型数据
- dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个float类型
2.带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1, dup2_x1,dup_x2, dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此
- dup_x1插入位置:1+1=2,即栈顶2个Slot下面
- dup_x2插入位置: 1+2=3,即栈顶3个Slot下面
- dup2_x1插入位置: 2+1=3,即找顶3个Slot下面
- dup2_x2插入位置: 2+2=4,即栈顶4个Slot下面
3.pop:将栈顶的1个Slot数值出栈。例如1个short类型数值。pop2:将栈项的2个slot数值出栈。例如1个double类型数值,或者2个int类型数值。
举例:
public void print(){
Object obj = new Object();
// String info = obj.toString();
obj.toString();
}
public void print(){
Object obj = new Object();
String info = obj.toString();
//obj.toString();
}
//类似的
public void foo(){
bar();
}
public long bar(){
return 0;
}

详细说一下:
public long nextIndex() {
return index++;
}
private long index = 0;







如上图:最后返回的index值为0
8.控制转移指令
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为:
1)比较指令、2)条件跳转指令、3)比较条件跳转指令、4)多条件分支跳转指令、5)无条件跳转指令等
比较指令的说明
- 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
- 比较指令有: dcmpg, dcmpl、fcmpg、fcmpl、lcmp.与前面讲解的指令类似。首字符d表示double类型,f表示float,l表示long.
- 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。
- 指令dempl和dempg也是类似的,根据其命名可以推测其含义,在此不再赘述。
- 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。
举例:指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设栈项的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0:若v1>v2则压入1:若v1<v2则压入-1.
两个指令的不同之处在于,如果遇到NaN值, fcmpg会压入1,而fcmpl会压入-1.
1.条件跳转指令
一、条件跳转指令
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈项元素的准备,然后进行条件跳转。
条件跳转指令有: ifeq,iflt,ifle, ifne,ifgt, ifge, ifnull,ifnonnull.这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。
具体说明:

注意:
1. 与前面运算规则一致:
- 对于boolean、 byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
- 对于long、float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
2.由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。
举例:
public void compare1(){
int a = 0;
if(a != 0){
a = 10;
}else{
a = 20;
}
}
public boolean compareNull(String str){
if(str == null){
return true;
}else{
return false;
}
}
//结合比较指令
public void compare2() {
float f1 = 9;
float f2 = 10;
System.out.println(f1 < f2);//true
}
注意:
虽然栈顶的是1,但是调用的是传参为boolean的方法:
源码:

public void compare3() {
int i1 = 10;
long l1 = 20;
System.out.println(i1 > l1);
}
public int compare4(double d) {
if (d > 50.0) {
return 1;
} else {
return -1;
}
}
2.比较条件跳转指令
二、比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有: if_icmpeq、 if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne.
其中指令助记符加上”if_”后,以字符”i”开头的指令针对int型整数操作(也包括short和byte类型),以字符”a”开头的指令表示对象引用的比较。

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。
举例:
//2.比较条件跳转指令
public void ifCompare1(){
int i = 10;
int j = 20;
System.out.println(i > j);
}public void ifCompare2() {
short s1 = 9;
byte b1 = 10;
System.out.println(s1 > b1);
}public void ifCompare3() {
Object obj1 = new Object();
Object obj2 = new Object();
System.out.println(obj1 == obj2);//false
System.out.println(obj1 != obj2);//true
}

3.多条件分支跳转
三、多条件分支跳转指令
多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。

从助记符上看,两者都是switch语句的实现,它们的区别:
- tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。
- 指令lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。

指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。

举例:
//3.多条件分支跳转
public void swtich1(int select){
int num;
switch(select){
case 1:
num = 10;
break;
case 2:
num = 20;
break;
case 3:
num = 30;
break;
default:
num = 40;
}
}
缺少一个break情况:
//3.多条件分支跳转
public void swtich1(int select){
int num;
switch(select){
case 1:
num = 10;
break;
case 2:
num = 20;
//break;
case 3:
num = 30;
break;
default:
num = 40;
}
}public void swtich2(int select){
int num;
switch(select){
case 100:
num = 10;
break;
case 500:
num = 20;
break;
case 200:
num = 30;
break;
default:
num = 40;
}
}
调用hashCode方法,将哈希值与下面字符串的哈希值比较,哈希值也大小排序,升序
//jdk7新特性:引入String类型
public void swtich3(String season){
switch(season){
case "SPRING":break;
case "SUMMER":break;
case "AUTUMN":break;
case "WINTER":break;
}
}


4.无条件跳转
四、无条件跳转指令
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。
指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于try-finally语句,且己经被虚拟机逐渐废弃,故不在这里介绍这两个指令。

//4.无条件跳转指令
public void whileInt() {
int i = 0;
while (i < 100) {
String s = "崩坏三天下第一";
i++;
}
}
注意:int类型的++,使用iinc 1 by 1;如下,double类型的++,使用dadd
public void whileDouble() {
double d = 0.0;
while(d < 100.1) {
String s = "崩坏三天下第一";
d++;
}
}
注意:会多一步向下转,所以还是使用int类型就好。
public void printFor() {
short i;
for (i = 0; i < 100; i++) {
String s = "崩坏三天下第一";
}
}
//思考:如下两个方法的操作有何不同?
public void whileTest(){
int i = 1;
while(i <= 100){
i++;
}
//可以继续使用i
}
public void forTest(){
for(int i = 1;i <= 100;i++){
}
//不可以继续使用i
}字节码指令一样


do-while字节码指令变化,至少执行一次i++
//更进一步
public void doWhileTest(){
int i = 1;
do{
i++;
}while(i <= 100);
}
9.异常处理指令
1.抛出异常指令
1、抛出异常指令:
(1)athrow指令
在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。除了使用throw语句显示抛出异常情况之外, JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv或 ldiv指令中抛出ArithmeticException异常。
(2)注意
正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时, Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
#######异常及异常的处理:
过程一:异常对象的生成过程 —>throw(手动 / 自动)指令 athrow
过程二:异常的处理:抓抛模型。 try-catch-finally 使用异常表
清晰明了,不再赘述。
public void throwZero(int i){
if(i == 0){
throw new RuntimeException("参数值为0");
}
}
对比一下,下面的两个代码
public void throwOne(int i){
if(i == 1){
throw new RuntimeException("参数值为1");
}
}
public void throwOne(int i) throws RuntimeException,IOException{
if(i == 1){
throw new RuntimeException("参数值为1");
}
}
没什么变化,但多了Exceptions属性

注意一下,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。这样是看不到对应的字节码指令。
public void throwArithmetic() {
int i = 10;
int j = i / 0;
System.out.println(j);
}
2.异常处理与异常表
- 起始位置
- 结束位置
- 程序计数器记录的代码处理的偏移地址
- 被捕获的异常类在常量池中的索引
举例:
public void tryCatch(){
try{
File file = new File("d:/hello.txt");
FileInputStream fis = new FileInputStream(file);
String info = "hello!";
}catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (RuntimeException e){
e.printStackTrace();
}
}
可以看到,到 goto 38 (+16),会跳转出,那22到38的代码呢?
异常表:

如图:如果在Start PC 到 End PC 中出现异常就跳转到 对应的Handler PC
思考:
//思考:如下方法返回结果为多少?
public static String func() {
String str = "hello";
try{
return str;
}
finally{
str = "崩坏三天下第一";
}
}
public static void main(String[] args) {
System.out.println(func());//hello
}
如图,在finally前已经return了

10.同步控制指令
组成
java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。
1.方法级的同步
方法级的同步:是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法;

当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置。
- 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。
- 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
- 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。
private int i = 0;
public synchronized void add(){
i++;
}
字节码指令与不加synchronized的一样,但会有方法表结构中的ACC_SYNCHRONIZED访问标志

说明:
这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorenter(进入锁)和monitorexit(释放锁)进行同步区控制。这是因为,对于同步方法而言,当虚拟机通过方法的访问标示符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言,monitorenter 和monitorexit指令是隐式存在的,并未直接出现在字节码中。
2.方法内指定指令序列的同步
同步一段指令集序列:通常是由java中的synchronized语句块来表示的。jvm的指令集有 monitorenter 和monitorexit两条指令来支持synchronized关键字的语义
当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。
当线程退出同步块时,需要使用monitorexit声明退出,在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。
指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。
下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。

private int i = 0;
private Object obj = new Object();
public void subtract(){
synchronized (obj){
i--;
}
}
可以看到,19 goto 27 (+8) 到return之间还有代码,也是异常表:什么异常?any任何异常

参考资料:
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)












