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

Posted 九死九歌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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文件格式写的很详细了。


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

  ① 魔数:默认为0xCAFEBABE,用来校验文件是否合法。若某class文件不以咖啡宝贝开头,虚拟机就会抛出如下错误:

  ② 版本号:

  因而如果是jdk8的话,就是十进制数的52,也就是0x0034

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

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

  其中后三项是jdk7版本中才加入的。

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

  常量池计数器中的数字比常量池表中的表项数多一。例如常量池为空,则常量池计数器数值为一。并且不同于数组索引,常量池表的索引是从1开始,例如十个元素的数组,索引是0~9,十个表项的常量池表,索引是1~10。

  如下是会存放到常量池中的东西:



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


  补充说明:


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


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



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



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



  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 (+36jvm中篇-05-字节码指令集与解析

深入理解JVM学习笔记——-9指令

深入理解JVM学习笔记——-9指令

Day350.字节码指令集与解析举例 -JVM

[jvm解析系列][十三]字节码指令小节,从字节码看JVM的栈解释器执行过程。

JVM字节码执行模型及字节码指令集面试+工作