【JVM学习笔记】字节码指令集解析

一、class的文件结构

1 前端编译器

前端编译器
  AOT效率较高,但只支持Linux平台。

2 透过字节码查看代码执行细节 - 1

  源代码如下:

public class Test {

	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);

		Integer i3 = 128;
		Integer i4 = 128;
		System.out.println(i3 == i4);

	}

}

  这个代码的运行结果会令我们感到困惑,如下:
运行结果
  让我们来查看一下字节码:

#2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>

 0 iconst_5			// 将int型数值5入操作数栈
 1 invokestatic #2	// 调用Integer类的valueOf方法,返回值入栈
 4 astore_1			// 将栈顶引用变量(即valueOf返回的x)存入局部变量表1号索引位置
 5 iconst_5			// 将int型数值5入操作数栈
 6 istore_2			// 将栈顶int型变量存入局部变量表2号位置
 7 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
10 aload_1
11 invokevirtual #4 <java/lang/Integer.intValue : ()I>	// 自动拆箱过程
14 iload_2
15 if_icmpne 22 (+7)
18 iconst_1
19 goto 23 (+4)
22 iconst_0
23 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
26 bipush 10
28 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
31 astore_3
32 bipush 10
34 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
37 astore 4
39 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
42 aload_3
43 aload 4
45 if_acmpne 52 (+7)
48 iconst_1
49 goto 53 (+4)
52 iconst_0
53 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
56 sipush 128
59 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
62 astore 5
64 sipush 128
67 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
70 astore 6
72 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
75 aload 5
77 aload 6
79 if_acmpne 86 (+7)
82 iconst_1
83 goto 87 (+4)
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
90 return

  透过分析字节码,我们可以发现java中存在一个语法糖,即:Integer a = 64;等同于Integer a = Integer.valueOf(64);,那我们来看看valueOf这个函数的内容:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

  其中这个IntegerCache是Integer的一个静态私有内部类:

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }
 
    private IntegerCache() {}
}

  让我们主要着眼观察21~26行,这几行代码把high赋值为127,而low本来就是-128,再把cache长度设为256,然后for循环中把cache中的256个元素赋值为-128~127。

  现在再来看看ValueOf的代码,如果传参为-128~127,则直接返回cache中的值,否则new Integer()。

  因而Integer i1 = 10;获得的是cache数组中的元素,i1和i2地址自然都一样。而Integer i3 = 128,不在-128~127区间内,因此要new一下,i3和i4各申请一份堆空间,自然是不同的地址。

3 透过字节码查看代码执行细节 - 2

  我们来看一段代码:

public class Test {

	public static void main(String[] args) {

		Father obj = new Son();
		System.out.println(obj.x);

	}

}

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;
	}

	public void print() {
		System.out.println("Son.x = " + x);
	}

}

  又是一个就你妈离谱的运行结果:
运行结果
  查看Father类构造器字节码:

 0 aload_0
 1 invokespecial #1 <java/lang/Object.<init> : ()V>
 4 aload_0
 5 bipush 10
 7 putfield #2 <com/spd/jvm/Father.x : I>
10 aload_0
11 invokevirtual #3 <com/spd/jvm/Father.print : ()V>
14 aload_0
15 bipush 20
17 putfield #2 <com/spd/jvm/Father.x : I>
20 return

  由此我们可以看到Father的构造器函数一共有四个操作:① 调用父类Object的构造器。② 将x赋值为10。③ 执行print()方法。④ 将x赋值为20。

  再看看Son类构造器字节码:

 0 aload_0
 1 invokespecial #1 <com/spd/jvm/Father.<init> : ()V>
 4 aload_0
 5 bipush 30
 7 putfield #2 <com/spd/jvm/Son.x : I>
10 aload_0
11 invokevirtual #3 <com/spd/jvm/Son.print : ()V>
14 aload_0
15 bipush 40
17 putfield #2 <com/spd/jvm/Son.x : I>
20 return

  他也一共有四步:① 调用父类Father的构造器。② 将x赋值为30.③ 执行print()方法。④ 将x赋值为40。

  那么我们Father obj = new Son();这一步的过程就是:首先调用Father构造器,Father构造器中调用Object构造器,Object构造器调用结束后继续执行Father构造器,其中将Father.x设置为10,然后调用print(),然而此时print()已被Son重写,输出的应该是Son.x而非Father.x,但Son.x的赋值过程发生在调用父类构造器之后,所以此时Son.x为0,然后执行完print()后继续回到Father构造器中,将Father.x设置为20,调用结束,回到Son构造器中,将Son.x设为30并输出,然后又设为40,调用结束。

  而至于System.out.println(obj.x);这一步,子类可以重写父类方法,但不能覆盖掉父类的成员变量,因而Son.x是30,Father.x仍然是20,输出的也就是20了。

4 class文件结构概述

  这里我就随随便便写一下,《深入了解Java虚拟机》和这篇博客:深入理解Java Class文件格式写的很详细了。
class文件结构
类文件结构
类文件结构

5 class文件结构分析 — 魔数与版本号

  ① 魔数:默认为0xCAFEBABE,用来校验文件是否合法。若某class文件不以咖啡宝贝开头,虚拟机就会抛出如下错误:
魔数错误
  ② 版本号:
版本号
  因而如果是jdk8的话,就是十进制数的52,也就是0x0034
版本兼容问题

6 class文件结构分析 — 常量池

  ③ 常量池:常量池计数器标识了常量池中一共有多少表项。常量池表记录了这些常量池表项,每个常量池表项分为一个一字节的tag byte和具体内容,tag byte用来记录这个元素的类型。如下即是tag byte的内容:
常量池的tag
  其中后三项是jdk7版本中才加入的。

  常量池中的内容再被类加载装入内存后,就进入了方法区的运行时常量池。

  常量池计数器中的数字比常量池表中的表项数多一。例如常量池为空,则常量池计数器数值为一。并且不同于数组索引,常量池表的索引是从1开始,例如十个元素的数组,索引是0~9,十个表项的常量池表,索引是1~10。
常量池表的特殊索引
  如下是会存放到常量池中的东西:
常量
常量
表格
动态链接

7 class文件结构分析 — 访问标志

访问标志
  补充说明:
补充说明
补充说明
补充说明

8 class文件结构分析 — 类索引、父类索引、接口索引集合

索引集合
索引集合

9 class文件结构分析 — 字段表集合

字段表集合
字段表
属性表姐和

10 class文件结构分析 — 方法表集合

方法表集合
方法表
方法表访问标志

12 class文件结构分析 — 属性表集合

属性表集合
属性表
  Code属性的格式:
Code属性

二、字节码指令集与解析举例

1 字节码指令集的概述

字节码指令集
  执行模型:
执行模型

2 字节码与数据类型

数据类型
数据类型
  指令的分类:
指令分类

3 加载与存储指令

加载与存储指令
常量入栈指令
  范围在[-1, 5]内,使用iconst_n,否则若范围在[-128, 127]内,使用bipush n,否则若范围在[-32768, 32767]内,使用sipush n,否则用ldc从常量池中获取。

  如下源码:

public class Test {

	public static void main(String[] args) {
		int a = -1;
		int b = 5;
		int c = 6;
		int d = 127;
		int e = 128;
		int f = 32767;
		int g = 32768;
	}

}

  解析字节码信息:

 0 iconst_m1
 1 istore_1
 2 iconst_5
 3 istore_2
 4 bipush 6
 6 istore_3
 7 bipush 127
 9 istore 4
11 sipush 128
14 istore 5
16 sipush 32767
19 istore 6
21 ldc #2 <32768>
23 istore 7
25 return

常量入栈
  如下源码:

public class Test {

	public static void main(String[] args) {
		long l1 = 1;
		long l2 = 2;
		float f1 = 2.0f;
		float f2 = 3.0f;
		double d1 = 1.0;
		double d2 = 2.0;
		Test test = null;
	}

}

  解析字节码信息:

 0 lconst_1
 1 lstore_1
 2 ldc2_w #2 <2>
 5 lstore_3
 6 fconst_2
 7 fstore 5
 9 ldc #4 <3.0>
11 fstore 6
13 dconst_1
14 dstore 7
16 ldc2_w #5 <2.0>
19 dstore 9
21 aconst_null
22 astore 11
24 return

  存储指令与加载指令大同小异。

  如下源码:

public class Test {

	public void store(int k, double d) {
		int m = k + 2;
		long l = 12;
		String str = "Hello world!";
		float f = 10.0f;
		d = 10;
	}

}

  解析字节码信息:

 0 iload_1
 1 iconst_2
 2 iadd
 3 istore 4
 5 ldc2_w #2 <12>
 8 lstore 5
10 ldc #4 <Hello world!>
12 astore 7
14 ldc #5 <10.0>
16 fstore 8
18 ldc2_w #6 <10.0>
21 dstore_2
22 return

4 算术运算指令

算数运算指令
算数运算指令
算数运算指令
  如下源码:

public class Test {

	public void method1() {
		int a = 100;
		a = a + 10;
	}

	public void method2() {
		int a = 100;
		a += 10;
	}

}

  分析字节码信息:

method1():
0 bipush 100
2 istore_1
3 iload_1
4 bipush 10
6 iadd
7 istore_1
8 return


method2():
0 bipush 100
2 istore_1
3 iinc 1 by 10
6 return

  对于~,即取反操作,是使用ixor指令令变量与-1异或。

  比较指令:
比较指令

5 类型转换指令

类型转换指令
类型转换指令
类型转换指令
窄化类型转换
补充说明

6 对象的创建与访问指令

对象的创建指令
对象的访问指令

7 数组操作指令

数组操作指令
数组操作指令

8 类型检查指令

类型检查指令

9 方法调用指令

方法调用指令
  最后一个不必充分理解。

10 方法返回指令

方法返回指令
方法返回指令

11 操作数栈管理指令

操作数栈管理指令
操作数栈管理指令

12 比较指令

  注意没有icmp
比较指令

13 条件转移指令

条件转移指令
条件转移指令

14 比较跳转指令

条件转移指令

15 多条件分支跳转

多条件分支跳转
  无论是连续值还是离散值,编译时switch会自动把他们从小到大排序。而如果switch判断的是引用类型,则是如下方法:

public class Test {

	public int test(String str) {
		switch (str) {
			case "一": return 1;
			case "二": return 2;
			case "三": return 3;
			default: return -1;
		}
	}

}
  0 aload_1
  1 astore_2
  2 iconst_m1
  3 istore_3
  4 aload_2
  5 invokevirtual #2 <java/lang/String.hashCode : ()I>
  8 lookupswitch 3
	19968:  44 (+36)
	19977:  72 (+64)
	20108:  58 (+50)
	default:  83 (+75)
 44 aload_2
 45 ldc #3 <>
 47 invokevirtual #4 <java/lang/String.equals : (Ljava/lang/Object;)Z>
 50 ifeq 83 (+33)
 53 iconst_0
 54 istore_3
 55 goto 83 (+28)
 58 aload_2
 59 ldc #5 <>
 61 invokevirtual #4 <java/lang/String.equals : (Ljava/lang/Object;)Z>
 64 ifeq 83 (+19)
 67 iconst_1
 68 istore_3
 69 goto 83 (+14)
 72 aload_2
 73 ldc #6 <>
 75 invokevirtual #4 <java/lang/String.equals : (Ljava/lang/Object;)Z>
 78 ifeq 83 (+5)
 81 iconst_2
 82 istore_3
 83 iload_3
 84 tableswitch 0 to 2
	0:  112 (+28)
	1:  114 (+30)
	2:  116 (+32)
	default:  118 (+34)
112 iconst_1
113 ireturn
114 iconst_2
115 ireturn
116 iconst_3
117 ireturn
118 iconst_m1
119 ireturn

16 无条件跳转指令

无条件跳转指令

17 异常抛出指令

异常抛出指令
异常处理过程

18 异常处理与异常表

异常处理与异常表
  分析如下代码:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class Test {

   public void tryCatch() {

   	try {
   		File file = new File("Hello world!");
   		FileInputStream fis = new FileInputStream(file);
   		String info = "你好世界!";
   	} catch (FileNotFoundException e) {
   		e.printStackTrace();
   	} catch (RuntimeException e) {
   		e.printStackTrace();
   	}

   }

}

  字节码如下:

 0 new #2 <java/io/File>
3 dup
4 ldc #3 <Hello world!>
6 invokespecial #4 <java/io/File.<init> : (Ljava/lang/String;)V>
9 astore_1
10 new #5 <java/io/FileInputStream>
13 dup
14 aload_1
15 invokespecial #6 <java/io/FileInputStream.<init> : (Ljava/io/File;)V>
18 astore_2
19 ldc #7 <你好世界!>
21 astore_3
22 goto 38 (+16)
25 astore_1
26 aload_1
27 invokevirtual #9 <java/io/FileNotFoundException.printStackTrace : ()V>
30 goto 38 (+8)
33 astore_1
34 aload_1
35 invokevirtual #11 <java/lang/RuntimeException.printStackTrace : ()V>
38 return

  我们再来看看异常表:
在这里插入图片描述
  这个异常表的意思就是说如果在pc = 0到pc = 22之间捕获到FileNotFoundException,就跳转到pc = 25处,如果在pc = 0到pc = 22之间捕获到RuntimeException,就跳转到pc = 33处。

  通过异常表分析下面这个情况:

public class Test {

	public static void main(String[] args) {
		System.out.println(func());
	}

	private static String func() {

		String str = "hello";

		try {
			return str;
		} finally {
			str = "world";
		}

	}

}

  输出结果是:hello

  字节码如下:

 0 ldc #5 <hello>
 2 astore_0
 3 aload_0
 4 astore_1
 5 ldc #6 <world> 
 7 astore_0
 8 aload_1
 9 areturn
10 astore_2
11 ldc #6 <world>
13 astore_0
14 aload_2
15 athrow

  再看看异常表:
异常表
  只要在pc = 3到pc = 5之间捕获到任何错误,就跳转到pc = 10处。

20 同步控制指令

  ① 方法级的同步:
方法级的同步
方法级的同步
方法级的同步
  ② 方法内指定指令序列的同步
方法内指定指令序列的同步
方法内指定指令序列的同步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

九死九歌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值