
所有的 JVM 全部遵守 Java 虚拟机规范,也就是说所有的 JVM 环境都是一样的, 这样一来字节码文件可以在各种 JVM 上进行。
想要让一个 Java 程序正确地运行在 JVM 中,Java 源码就是必须要被编译为符合 JVM 规范的字节码。
前端编译器的主要任务就是负责将符合 Java 语法规范的 Java 代码转换为符合 JVM 规范的字节码文件。
javac 是一种能够将 Java 源码编译为字节码的前端编译器。
javac 编译器在将 Java 源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法分析、语法分析、语义分析以及生成字节码。

Oracle 的 JDK 软件包括两部分内容:

前端编译器 VS 后端编译器
Java 源代码的编译结果是字节码,那么肯定需要有一种编译器能够将 Java 源码编译为字节码,承担这个重要责任的就是配置在 path 环境变量中的 javac 编译器。javac 是一种能够将 Java 源码编译为字节码的前端编译器。
HotSpot VM 并没有强制要求前端编译器只能使用 javac 来编译字节码,其实只要编译结果符合 JVM 规范都可以被 JVM 所识别即可。在 Java 的前端编译器领域,除了 javac 之外,还有一种被大家经常用到的前端编译器,那就是内置在 Eclipse 中的 ECJ (Eclipse Compiler for Java)编译器。和 javac 的全量式编译不同,ECJ 是一种增量式编译器。
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot 的 JIT 编译器负责。
① 类文件结构有几个部分?
② 知道字节码吗?字节码都有哪些? Integer x = 5; int y = 5;比较×==y都经过哪些步骤?
public class IntegerTest {
public static void main(String[] args) {
Integer x = 5;
int y = 5;
System.out.println(x == y); //
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 ==i2); // true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false
}
}
Integer的valueOf方法,IntegerCache是一个静态内部类,用于创建-128~127范围内的Integer数组。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
下面是IntegerCache的静态内部类



实例二:使用Sting来看
public class StringTest {
public static void main(String[] args) {
String str = new String("hello") + new String("world");
String str2 = "helloword";
System.out.println(str2 == str); // 输出:false
}
}

实例三:
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}
class Son extends Father {
int x = 30;
public Son() {
this.print();
x = 40;
}
@Override
public void print() {
System.out.println("Son.x = " + x);
}
}
public class _03_SonTest {
public static void main(String[] args) {
Father f = new Father();
System.out.println(f.x);
}
}
查看Father类的字节码文件

查看son类的字节码文件

类文件结构的官方文件位置:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
任何一个 Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件形式存在。Class 文件是一组以8位字节为基础单位的二进制流。
Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。

表用于描述有层次关系的复合结构的数据

整个 Class 文件本质上就是一张表

Class文件结构

也就回答了上面的面试题目

| 类型 | 名称 | 说明 | 长度 | 数量 |
|---|---|---|---|---|
| u4 | magic | 魔数,识别Class文件格式 | 4个字节 | 1 |
| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 |
| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 |
| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 |
| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 |
| u2 | access_flags | 访问标识 | 2个字节 | 1 |
| u2 | this_class | 类索引 | 2个字节 | 1 |
| u2 | super_class | 父类索引 | 2个字节 | 1 |
| u2 | interfaces_count | 接口计数器 | 2个字节 | 1 |
| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count |
| u2 | fields_count | 字段计数器 | 2个字节 | 1 |
| field_info | fields | 字段表 | n个字节 | fields_count |
| u2 | methods_count | 方法计数器 | 2个字节 | 1 |
| method_info | methods | 方法表 | n个字节 | methods_count |
| u2 | attributes_count | 属性计数器 | 2个字节 | 1 |
| attribute_info | attributes | 属性表 | n个字节 | attributes_count |

加载(load):就是将局部变量压栈到操作数栈中,局部变量可能来自于局部变量表(存储指令),也可能来自于常量池。
存储(store):保存到栈帧的局部变量表中
这下面的i、l、d、a标识的类型分别是int(4个字节),float(4个字节),double(8个字节),a是引用类型(4个字节)(其实像byte、short、char、boolean也是用的int一个槽位4个字节来表示的)
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。
常用指令
【局部变量压栈指令】将一个局部变量加载到操作数栈: xload、xload_<n>(其中x为i、1、f、d、a,n为0到3)【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_<n>(其中x为i、1、f、d、a,n为0到3) ; xastore(其中x为i、l、f、d、a、b、c、s)(其实x就是表示类型,n就是表示存储的位置)上面所列举的指令助记符(也就是字节码指令)中,有一部分是以尖括号结尾的(例如iload_<n>)。这些指令助记符实际上代表了一组指令(例如 iload_<n>代表了iload_0、iload_1、iload_2和iload_3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
除此之外,它们的语义与原生的通用指令完全一致(例如 iload_0的语义与操作数为0时的 iload 指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表double类型。
注意:iload_0和iload 0其实是一个意思,但是iload_0只占用一个字节(只有操作码,操作数包含在操作码中了),二iload 0占用三个字节(操作码一个字节+操作数两个字节);还有像short、byte、char、boolean底层也是int的指令,这样做的目的是减少指令的条数,因为指令条数总共只有2^8,两百多条,后面jdk更新的时候,可能会又出现新的指令

操作码后面的#4 #5这些就叫做操作数

字节码执行过程
//1.局部变量压栈指令Z
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.println(flag);
System.out.println(arr);
}

操作数栈(Operand Stacks)
我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。
在解释执行的过程中,每当为Java方法分配栈帧时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,用来存放计算的操作数以及返回的结果。
具体来说便是:执行每条指令之前,Java虚拟机要求该指令的操作数已经被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需要的操作数弹出,并且将指令的结果重新压入栈中。
//3.出栈装入局部变量表指令
public void store(int k, double d) {
int m = k + 2;
long l = 12;
String str = "atguigu";
float f = 10.0F;
d = 10;
}

解释上面的字节码指令,iload_1的意思就是将局部变量表中下标为1的变量的值放入到操作数栈中,iconst_2的意思是将局部变量表中的下标为2的变量的值压入操作数栈中,iadd是弹出操作数栈中的两个变量,然后将它们求和,istore4存储到局部变量表索引为4的位置(m),将k+2的值加入到操作数栈中。ldc #15就是到常量池中取出索引为15的变量的值(atguigu),放入到操作数栈中。astore 7就是弹出操作数栈中的栈顶的元素存储到局部变量下标为7的位置,ldc #16的意思是从局部变量表中取出下标为16的变量的值(10.0)放入到操作数栈中,fstore 8 就是从操作数栈中弹出栈顶的元素然后将其存到局部变量表下标为8的位置ldc2_w #17的意思是将常量池中下标为17的变量(10.0)的值放入到操作数栈中,dstore_2的意思是从栈顶弹出10.0然后加入到局部变量表中索引为2的位置,return然后终止方法的执行。
0 iload_1
1 iconst_2
2 iadd
3 istore 4
5 ldc2_w #13 <12>
8 lstore 5
10 ldc #15 <atguigu>
12 astore 7
14 ldc #16 <10.0>
16 fstore 8
18 ldc2_w #17 <10.0>
21 dstore_2
22 return
如果不涉及到其他的运算的情况下,i++和++i的字节码是一样的。
3 iinc 1 by 1字节码指令的意思是直接将局部变量表中的索引为1的变量的值+1

//关于(前)++和(后)++
public void method6(){
int i = 10;
// i++;
++i;
}
后加加的代码以及字节码
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 return

前加加的代码和字节码

public void method7(){
int i = 10;
int a = i++;
int j = 20;
int b = ++j;
}

解释下面的字节码指令
0 bipush 10:将10压入到操作数栈中,istore_1:将操作数栈中的栈顶的元素弹出,放入到局部变量表中的索引为1的位置(为0的位置的值是this),iload_1:将局部变量表中的索引为1的变量的值加载到操作数栈中,iinc 1 by 1:将局部变量表中索引为1的变量的值+1(也就是变量i),istore_2:将操作数栈中的栈顶元素(10)弹出,放在局部变量表中索引为2的位置(也就是变量a),bipush 20:将20压入操作数栈中,istore_3:从操作数栈顶弹出元素(20)放到局部变量表中索引为3的位置(也就是j所对应的值),iinc 3 by 1 :将局部变量表中的索引为3的变量的值(j的值)加1,就变成21了,iload_3:加载局部变量表中索引为3的变量的值(21)放到操作数栈顶。istore 4:从栈顶弹出21存到局部变量表索引为4的位置,return:然后结束方法。
局部变量表和操作数栈的简图
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_2
8 bipush 20
10 istore_3
11 iinc 3 by 1
14 iload_3
15 istore 4
17 return
代码
public void method8(){
int i = 10;
i = i++;
System.out.println(i);//10
}
字节码指令
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println>
15 return


i+=10的情况下
源码
public void method3(int j) {
int i = 100;
i += 10;
}
字节码
0 bipush 100
2 istore_2
3 iinc 2 by 10 // 这里是直接将局部变量表中索引为2的变量的值+10
6 return
i = i + 10的情况
public void method3(int j) {
int i = 100;
i = i + 10;
}
0 bipush 100
2 istore_2
3 iload_2 // 加载局部变量表中索引为2的变量的地址值,放到操作数栈当中
4 bipush 10 // 往操作数栈当中压入10
6 iadd // 弹出操作数栈中的两个变量,相加,压入操作数栈
7 istore_2 // 存到局部变量表索引为2的位置 (值为110)
8 return
其实主要区别就是:
i+=10;是直接操作局部变量表
i=i + 10;是先将i的值加载到操作数栈当中,然后再对i进行操作
宽化类型转化:从小范围向大范围转换。(也叫做自动类型提升,自动的就会有个转换了)
简化为:int --> long --> float --> double(int可以转换成long、float、double,long转换成float、double。float转换成double)

宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int转换到long,或者从int转换到double,都不会丢失任何信息,转换前后的值是精确相等的。
从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失——可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。
尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常。
//举例:精度损失的问题
@Test
public void upCast2(){
int i = 123123123;
float f = i;
// 输出结果1.2312312E8,这里就相当于发生了精度丢失变成了123123120
System.out.println(f);//123123120
long l = 123123123123L;
l = 123123123123123123L;
double d = l;
// 1.2312312312312312E17这里也发生了精度丢失,变成了123123123123123120
System.out.println(d);//123123123123123120
}
byte、short类型在底层都是用int来进行存的
//针对于byte、short等转换为容量大的类型时,将此类型看做int类型处理。
public void upCast3(byte b){
int i = b;
long l = b;
double d = b;
}

注意:从float、double、long等类型往byte、short、char类型转换的时候,需要先把前面几种类型转换成int类型,然后在从int类型转换到后面这几种类型,所以int类型相等于一种过渡类型


窄化类型转换的精度丢失,将int i = 128;强转成 byte b;会将前面的24位给丢失,也就是变成了10000000,也就是-128了
//窄化类型转换的精度损失
@Test
public void downCast4(){
// 128是32位的前24位都是0,后8位数是10000000
// 将int转换成byte之后就会砍掉前面的24位,然后后面就变成-128了
int i = 128;
byte b = (byte)i;
System.out.println(b);
}
补充说明
当一个浮点值转化转换为整数类型T(T限于int或long类型之一)的时候,将遵循一下转换规则:
如果浮点值是NaN,那抓换的结果就是int或者long类型的0
如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入的模式取整,获得整数值v
如果v在目标类型T(int或long)的表示范围之内,那转化的结果就是v。否则,根据v的符号,转换为T所能表示的最大或者最小整数
当将一个double类型转换成float类型时,将遵循以下转换规则:
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。
一、创建指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作是用了不同的字节码指令;
创建类实例的指令
创建数组指令:
上述创建指令可以用创建对象或者数组,由于对象和数组在java中广泛使用,这些指令的使用频率也非常高。


可以按照是否含有static来划分
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。
访问类字段(static字段,或者称为类变量)的指令: getstatic、putstatic,
访问类实例字段(非static字段,或者称为实例变量)的指令: getfield、putfield

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

数组操作指令主要有:xastore和xaload指令。具体为:
baload、caload、saload、iaload、laload、faload、daload、aaloadbastore、 castore、sastore、iastore、lastore、fastore、 dastore、aastore可以从下面看到bzyte和boolean的加载和存储指令都是使用baload和bastore(其实boolean也是用数字来表示的,非0表示正数,0表示负数)

说明:
下面那张图中的标红的部分就使用到了这两条指令
//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;
}

将boolean数组注释打开之后
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
该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。

检查类实例或数组类型的指令: instanceof、checkcast。
指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常
指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。
//4.类型检查指令
public String checkCast(Object obj) {
if (obj instanceof String) {
return (String) obj;
} else {
return null;
}
}

方法调用指令:invokevirtual、invokeinterface、invokespecial、invokestatic 、invokedynamic
以下5条指令用于方法调用:
invokevirtual 体现为晚期绑定
invokeinterface 也体现为晚期绑定
invokespecial 体现为早期绑定
注意:
- invokedynamic老师不讲,估计是很少遇到吧
- invokeinterface是对接口而言的,用属于接口类型的对象调用方法的时候就是这个(但是如果调用的是接口中的静态方法字节码指令是invokestatic)
- invokespecial只有构造器、私有方法、super.方法名()调用父类方法这几种情况,其中调用父类方法这种情况可能出现其直接父类没有该方法,那就可以调用其父类继承的父类中的该方法,最终找到一个方法调用就是了
- invokestatic是调用static静态方法,无论是使用对象.静态方法名()还是类名.静态方法名()都是invokestatic,也不难理解
- invokevirtual是调用类中的非静态普通方法,而这种实例方法可能调用的是子类重写的非静态普通方法,比如A a = new B();a.hello(),其中B类继承A类,并且B类重写了A类中的hello()方法,这种情况下就是invokevirtual了,但是有可能该类没有子类,调用的就是本类中的非静态普通方法,这种情况也是invokevirtual了(其实也就是除了上面的几种都属于invokevirtual)
这下面有几个例子用来解释说明invokespecial指令(其实这些也就是在编译器就确定好了方法的调用,而不是在运行期间再确定的)
/**
* @author shkstart
* @create 2020-09-08 9:35
*
* 指令5:方法调用与返回指令
*/
public class _5MethodInvokeReturnTest {
//方法调用指令:invokespecial:静态分派
public void invoke1(){
//情况1:类实例构造器方法:<init>()
Date date = new Date();
Thread t1 = new Thread();
//情况2:父类的方法
super.toString();
//情况3:私有方法
methodPrivate();
}
private void methodPrivate(){
}
//方法调用指令:invokestatic:静态分派
public void invoke2(){
methodStatic();
}
// 如果使用private + static来进行修饰的化,
// 底层的字节码指令还是invokestatic:编译的时候就确定好了的
// 我们叫做静态分派
private static void methodStatic(){
}
//方法调用指令: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();
}
//方法的返回指令
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;
}
public float returnFloat(){
int i = 10;
return i;
}
public byte returnByte(){
return 0;
}
public void methodReturn(){
int i = returnByte();
}
}
方法调用指令的补充说明,可以看到下面的aa.method2();的字节码指令是invokeinterface,执行method2的字节码指令是invokestatic(因为在接口AA中method2是用static修饰的,所以是invokestatic)
/**
* @author shkstart
* @create 2020-09-10 17:26
* 补充:方法调用指令的补充说明
*/
public class _6InterfaceMethodTest {
public static void main(String[] args) {
AA aa = new BB();
// 9 invokeinterface #4 <com/atguigu/java/AA.method2>
aa.method2();
// 14 invokestatic #5 <com/atguigu/java/AA.method1>
AA.method1();
}
}
interface AA {
public static void method1() {
}
public default void method2() {
}
}
class BB implements AA {
}
虚方法与非虚方法的区别
不管子类父类他们的那个方法能被重写,那这方法就是虚方法。
子类对象的多态的使用前提:
虚拟机中调用方法的指令
普通指令:
<init>方法、私有及父类方法,解析阶段确定唯一方法版本 (非虚方法)动态调用指令
5. invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预。而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
方法返回指令:
方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。

举例:
通过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;
}
public float returnFloat() {
int i = 10;
return i;
}
public byte returnByte() {
return 0;
}
public boolean returnBoolean(){
return false;
}
public char returnChar(){
return 'a';
}
public void methodReturn() {
int i = returnByte();
}

如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。
这类指令包括如下内容:
这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
说明:
不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。
带_x的 指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令, dup_x1, dup2_x1,dup_x2, dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此
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 foo(){
bar();
}
public long bar(){
return 0;
}

示例三
代码:
public long nextIndex() {
return index++;
}
private long index = 0;
图示分析上面代码的操作数栈的调用过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cBad03KY-1656240361662)(C:/Users/losser/AppData/Roaming/Typora/typora-user-images/image-20220617160354229.png)]
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为
比较指令的说明
举例:
指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若v1>v2则压入1;若v1<v2则压入-1。
两个指令的不同之处在于,如果遇到**NaN值**,fcmpg会压入1,而fcmpl会压入-1。
注意:
数值类型的变量才可以比较大小!
boolean、引用数据类型不能比较大小
注意:NaN(Not a Number)表示不是一个数字,比如0.0/0.0得到的可能是1.0(两个数相等),也可能是0.0(0.0是分子),也可能是无穷大(0.0是分母),所以老师给出的解释是NaN代表无法确定是什么数字,只有double和float类型中可能出现NaN的情况,而long类型不会出现NaN,所以只有lcmp,而没有lcml
条件**跳转指令通常和比较指令结合使用**。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。
条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,,ifnull, ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。
说明:这里的eq表示的就是equals,l表示的是less,le表示的是less equals,g表示的是greater

说明:这里的eq表示的就是equals,l表示的是less,le表示的是less equals,g表示的是greater
注意:
//1.条件跳转指令
public void compare1(){
int a = 0;
if(a != 0){
a = 10;
}else{
a = 20;
}
}

//结合比较指令
public void compare2() {
float f1 = 9;
float f2 = 10;
System.out.println(f1 < f2);//true
}
上方代码对应的字节码指令
0 ldc #2 <9.0>
2 fstore_1
3 ldc #3 <10.0>
5 fstore_2
6 getstatic #4 <java/lang/System.out>
9 fload_1
10 fload_2
11 fcmpg
12 ifge 19 (+7) // 这里是判断栈顶的元素是否大于等于0,是的话就跳转到19
15 iconst_1
16 goto 20 (+4)
19 iconst_0
20 invokevirtual #5 <java/io/PrintStream.println>
23 return

就是这个方法

public void compare3() {
int i1 = 10;
long l1 = 20;
System.out.println(i1 > l1);
}
字节码指令
0 bipush 10 // 往操作数栈中压入10
2 istore_1 // 存到局部变量表中的索引为1的位置
3 ldc2_w #6 <20> // 从常量池中加载20,放到操作数栈
6 lstore_2 // 存到局部变量表索引为1的位置,同时出栈
7 getstatic #4 <java/lang/System.out> // 获取System.out,放入操作数栈
10 iload_1 // 加载局部变量表中索引为1的变量,也就是10
11 i2l // 将int 10转成long 10,然后再压入操作数栈
12 lload_2 // 加载局部变量表中索引为2的变量
13 lcmp // 然后将两个long类型的变量进行比较,也就是弹出两个元素,由于下面的10是v1,20是v2,所以这里是得到-1,然后再压入操作数栈
14 ifle 21 (+7) // 如果小于等于0,就跳转到21
17 iconst_1 // 从常量池中加载1
18 goto 22 (+4)
21 iconst_0 // 从常量池中加载0,这里boolean的0表示false,非0表示true
22 invokevirtual #5 <java/io/PrintStream.println> // 然后再打印
25 return
注意:
指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们作比较,设栈顶的元素为v2,栈顶的顺位第2位元素为v1,若v1=v2,则压入0;若v1>v2则压入1;若v1<v2则压入-1.
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符**“a”开头的指令表示对象引用的比较**。
具体说明:

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。




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

从助记符上看,两者都是switch语句的实现,它们的区别:
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。

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

//jdk7新特性:引入String类型
public void swtich3(String season){
switch(season){
case "SPRING":break;
case "SUMMER":break;
case "AUTUMN":break;
case "WINTER":break;
}
}
它在底层其实会使用equals和hashCode进行判断,下边截取了部分代码
5 invokevirtual #11 <java/lang/String.hashCode>
8 lookupswitch 4
-1842350579: 52 (+44)
-1837878353: 66 (+58)
-1734407483: 94 (+86)
1941980694: 80 (+72)
default: 105 (+97)
52 aload_2
53 ldc #12 <SPRING>
55 invokevirtual #13 <java/lang/String.equals>
//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;
}
}
注意这里下面操作完了之后都回跳转到49(goto 49),因为我们上面的代码上面加上了break
0 iload_1
1 tableswitch 1 to 3 1: 28 (+27)
2: 34 (+33)
3: 40 (+39)
default: 46 (+45)
28 bipush 10
30 istore_2
31 goto 49 (+18)
34 bipush 20
36 istore_2
37 goto 49 (+12)
40 bipush 30
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return
这个是不加break的代码,会造成穿透,也就说,它会执行下面的代码知道遇到了break
public void swtich1(int select){
int num;
switch(select){
case 1:
num = 10;
break;
case 2:
num = 20;
// break; 这里没有加break
case 3:
num = 30;
break;
default:
num = 40;
}
}
字节码指令
0 iload_1
1 tableswitch 1 to 3 1: 28 (+27)
2: 34 (+33)
3: 37 (+36)
default: 43 (+42)
28 bipush 10
30 istore_2
31 goto 46 (+15)
34 bipush 20
36 istore_2
37 bipush 30
39 istore_2
40 goto 46 (+6)
43 bipush 40
45 istore_2
46 return
这里从第9行一直运行到了第13行,这是由于穿透造成的。
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。
指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于 try-finally语句,且已经被虚拟机逐渐废弃,故不在这里介绍这两个指今。

实例一
Java源代码
//4.无条件跳转指令
public void whileInt() {
int i = 0;
while (i < 100) {
String s = "atguigu.com";
i++;
}
}
编译之后生成的字节码,简单分析一下
0 iconst_0 // 从常量池中加载0,放入操作数栈
1 istore_1 // 从操作数栈中取出1,存到局部变量表
2 iload_1 // 从操作数中加载1
3 bipush 100 // 往操作数栈中压入100
5 if_icmpge 17 (+12) // 条件判断语句,判断v1是否大于等于v2,是的话就直接跳转到17,终止方法
8 ldc #17 <atguigu.com> // 加载atguigu.com
10 astore_2 // 存到局部变量表的索引为2的位置
11 iinc 1 by 1 // 然后将局部变量表中的1直接加
14 goto 2 (-12) // 直接返回2,然后又跳转到字节码为2的语句
17 return
可以看到上面的while循环只不过是条件判断指令和无条件跳转指令
这是每一次的步骤

Java源代码,自己尝试解析一下double类型的循环
示例二
public void whileDouble() {
double d = 0.0;
while(d < 100.1) {
String s = "atguigu.com";
d++;
}
}
字节码指令如下:
0 iconst_0 // 从常量池中加载0到操作数栈中
1 istore_1 // 取出操作数栈中变量的值,放入到局部变量表中的索引为1的位置
2 iload_1 // 从局部变量表中加载1,放到操作数栈中
3 bipush 100 // 往操作数栈中压入100
5 if_icmpge 17 (+12) // 弹出操作数中的两个元素进行比较,判断是否大于等于(其实也就是判断v1 >= v2 false),所以直接返回-1,压入到操作数栈当中,如果是大于等于的话,那直接跳转到17,结束方法了
8 ldc #17 <atguigu.com> // 从常量池中加载atguigu.com
10 astore_2 // 然后将操作数栈弹出元素,存入到局部变量表索引为2的位置
11 iinc 1 by 1 // 将局部变量表中索引为1的位置的元素的值+1
14 goto 2 (-12) // 跳转到2
17 return
示例四
再一个代码实例,这里我就不再赘述了,只贴一下代码和字节码,其他的应该都能看懂。
public void printFor() {
short i;
for (i = 0; i < 100; i++) {
String s = "atguigu.com";
}
}
0 iconst_0
1 istore_1
2 iload_1
3 bipush 100
5 if_icmpge 19 (+14)
8 ldc #17 <atguigu.com>
10 astore_2
11 iload_1
12 iconst_1
13 iadd
14 i2s
15 istore_1
16 goto 2 (-14)
19 return
实例五,其实这下面两个都是差不多的,非要说区别,其实就是它俩int i的作用域不同
//思考:如下两个方法的操作有何不同?
public void whileTest(){
int i = 1;
while(i <= 100){
i++;
}
//可以继续使用i
}
public void forTest(){
for(int i = 1;i <= 100;i++){
}
//不可以继续使用i
}

(1)athrow指令
在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。
除了使用throw语句显示抛出异常情况之外,**JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。**例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv或ldiv指令中抛出ArithmeticException异常。
(2)注意
正常情况下,操作数栈的压入弹出都是由一条条指令完成的。唯一的例外是在**抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者的操作数栈上。**(其实也就是谁调用就跑给谁)’
异常及异常的处理
过程一:异常对象的生成过程—>throw (手动 / 自动)—> 指令:athrow
过程二:异常的处理:抓抛模型。try-catch-finally —> 使用异常表(之前看字节码的时候看过)
注意:系统自动抛出的异常如ArithmeticException,系统会自动处理,不用手动throw或者try-catch-finally,在异常表中也不会生成

Java代码如下
public void throwZero(int i) {
if (i == 0) {
throw new RuntimeException("参数值为0");
}
}
字节码指令如下
0 iload_1
1 ifne 14 (+13)
4 new #2 <java/lang/RuntimeException>
7 dup
8 ldc #3 <参数值为0>
10 invokespecial #4 <java/lang/RuntimeException.<init>>
13 athrow
14 return
Java代码
public void throwOne(int i) throws RuntimeException, IOException {
if (i == 1) {
throw new RuntimeException("参数值为1");
}
}
字节码指令,这里稍微有一点不同,这里的字节码指令是if_icmpne(是比较指令),而上面是ifne(条件跳转指令)
0 iload_1
1 iconst_1
2 if_icmpne 15 (+13)
5 new #2 <java/lang/RuntimeException>
8 dup
9 ldc #5 <参数值为1>
11 invokespecial #4 <java/lang/RuntimeException.<init>>
14 athrow
15 return

其实这个code属性刻画的是方法体里面的内容,throws是在方法的申明处
java代码
public void throwArithmetic() {
int i = 10;
int j = i / 0;
System.out.println(j);
}
字节码指令,因为10/0会出现异常的,但是这里不会出现athrow的,系统已经定义好的异常,它会自动抛出异常。(如:ArithmeticException)
0 bipush 10
2 istore_1
3 iload_1
4 iconst_0
5 idiv
6 istore_2
7 getstatic #6 <java/lang/System.out>
10 iload_2
11 invokevirtual #7 <java/io/PrintStream.println>
14 return=
在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成,而是采用异常表来完成的。
如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:
当一个异常被抛出时,JVN会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(再调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标
Java源代码如下
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();
}
}
字节码如下。
0 new #8 <java/io/File>
3 dup
4 ldc #9 <d:/hello.txt>
6 invokespecial #10 <java/io/File.<init>>
9 astore_1
10 new #11 <java/io/FileInputStream>
13 dup
14 aload_1
15 invokespecial #12 <java/io/FileInputStream.<init>>
18 astore_2
19 ldc #13 <hello!>
21 astore_3
22 goto 38 (+16)
25 astore_1
26 aload_1
27 invokevirtual #15 <java/io/FileNotFoundException.printStackTrace>
30 goto 38 (+8)
33 astore_1
34 aload_1
35 invokevirtual #16 <java/lang/RuntimeException.printStackTrace>
38 return

try-catch字节码指令结合局部变量表和操作数栈分析如下。

下面的try-catch-finally执行的结果是hello
//思考:如下方法返回结果为多少?
public static String func() {
String str = "hello";
try {
return str;
} finally {
str = "atguigu";
}
}
public static void main(String[] args) {
System.out.println(func());//hello
}
字节码指令如下
0 ldc #17 <hello>
2 astore_0
3 aload_0
4 astore_1
5 ldc #18 <atguigu>
7 astore_0
8 aload_1
9 areturn
10 astore_2
11 ldc #18 <atguigu>
13 astore_0
14 aload_2
15 athrow

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

下面的是没有加synchronized关键字的方法,其实可以看到他们的字节码是没有区别的,但是方法的访问标志信息是不一样的。因为它是隐式级的同步机制。

其Java代码和字节码如下
private int i = 0;
public void add(){
i++;
}
0 aload_0
1 dup
2 getfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
5 iconst_1
6 iadd
7 putfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
10 return
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
翻译如下:
每个对象都与一个监视器相关联。当且仅当监视器具有所有者时,它才会被锁定。执行 monitorenter 的线程尝试获取与 objectref 关联的监视器的所有权,如下所示:
其实这部分内容和JUC是重合的,后续学JUC的时候,再回顾来看。或许有不一样的收获。
摘自官网
The objectref must be of type reference.
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
翻译如下:
objectref 必须是引用类型。
执行 monitorexit 的线程必须是与 objectref 引用的实例关联的监视器的所有者。
该线程减少与 objectref 关联的监视器的条目计数。如果结果条目计数的值为零,则线程退出监视器并且不再是它的所有者。允许其他阻塞进入监视器的线程尝试这样做。
回忆上篇的第7章:对象的实例化内存布局与访问定位,对象头里面就包含线程锁持有锁的信息
对象头中含有的信息:

同步一段指令集序列:通常是由java中的synchronized语句块来表示的。jvm的指令集有 monitorenter 和**monitorexit**两条指令来支持synchronized关键字的语义。
当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,(因为是可重入的缘故)如果是,则进入;否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。
当线程退出同步块时,需要使用**monitorexit**声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。
指令**monitorenter和monitorexit**在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。
下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。
其实吧,这种字节码文件的分析,还是要画图才来的直观,字节码,操作数栈,局部变量表。
public class _3SynchronizedTest {
private int i = 0;
public void add(){
i++;
}
private Object obj = new Object();
public void subtract(){
// obj 就是同步监视器
synchronized (obj){
i--;
}
}
}
0 aload_0 // 加载局部变量表中的索引下标为0的变量(this)的地址值,加入到操作数栈当中
1 getfield #4 <com/atguigu/java1/_3SynchronizedTest.obj> // 获取对象的某个字段的地址值,加入到操作数栈当中
4 dup // 复制一份
5 astore_1 // 弹出一个对象的地址值,放到局部变量表索引为1的位置
6 monitorenter // 将锁对象的同步监视器的计数器的值从0改成1
7 aload_0 // 把this的地址值拿过来,放到操作数栈当中
8 dup // 然后将操作数栈顶上的元素的地址值复制一份
9 getfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
12 iconst_1
13 isub
14 putfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
17 aload_1
18 monitorexit
19 goto 27 (+8)
22 astore_2
23 aload_1
24 monitorexit
25 aload_2
26 athrow
27 return