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学习笔记字节码指令集解析的主要内容,如果未能解决你的问题,请参考以下文章