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-字节码指令集与解析