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文件结构分析 — 常量池(1)

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

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

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

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

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



以上是关于JVM学习笔记字节码指令集解析的主要内容,如果未能解决你的问题,请参考以下文章

jvm中篇-05-字节码指令集与解析

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

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

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

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

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