1 字节码与类加载
1.1类文件结构
1.2字节码指令
javap工具
Oracle 提供了 javap 工具来反编译 class 文件
javap -v xxx\xx.class
图解方法执行流程
代码
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
常量池载入运行时常量池
常量池也属于方法区,只不过这里单独提出来了
方法字节码载入方法区
(stack=2,locals=4) 对应操作数栈有2个空间(每个空间4个字节),局部变量表中有4个槽位
执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore 1
将操作数栈栈顶元素弹出,放入局部变量表的slot 1中
对应代码中的
a = 10
ldc #3
读取运行时常量池中#3,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore 2
将操作数栈中的元素弹出,放到局部变量表的2号位置
iload1 iload2
将局部变量表中1号位置和2号位置的元素放入操作数栈中
因为只能在操作数栈中执行运算操作
iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中
istore 3
将操作数栈中的元素弹出,放入局部变量表的3号位置
getstatic #4
在运行时常量池中找到#4,发现是一个对象
在堆内存中找到该对象,并将其引用放入操作数栈中
iload 3
将局部变量表中3号位置的元素压入操作数栈中
invokevirtual 5
找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
生成新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码
执行完毕,弹出栈帧
清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧,程序结束
通过字节码指令来分析问题
public class Demo2 {
public static void main(String[] args) {
int i=0;
int x=0;
while(i<10) {
x = x++;
i++;
}
System.out.println(x); //结果为0
}
}
为什么最终的x结果为0呢? 通过分析字节码指令即可知晓
Code:
stack=2, locals=3, args_size=1 //操作数栈分配2个空间,局部变量表分配3个空间
0: iconst_0 //准备一个常数0
1: istore_1 //将常数0放入局部变量表的1号槽位 i=0
2: iconst_0 //准备一个常数0
3: istore_2 //将常数0放入局部变量的2号槽位 x=0
4: iload_1 //将局部变量表1号槽位的数放入操作数栈中
5: bipush 10 //将数字10放入操作数栈中,此时操作数栈中有2个数
7: if_icmpge 21 //比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21。这里的比较是将两个数做减法。因为涉及运算操作,所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
10: iload_2 //将局部变量2号槽位的数放入操作数栈中,放入的值是0
11: iinc 2, 1 //将局部变量2号槽位的数加1,自增后,槽位中的值为1
14: istore_2 //将操作数栈中的数放入到局部变量表的2号槽位,2号槽位的值又变为了0
15: iinc 1, 1 //1号槽位的值自增1
18: goto 4 //跳转到第4条指令
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
构造方法
cinit()V
public class Demo3 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(i); //结果为30
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
init()V
public class Demo4 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo4(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo4 d = new Demo4("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
//原始构造方法在最后执行
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
方法调用
public class Demo5 {
public Demo5() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.test1();
demo5.test2();
demo5.test3();
Demo5.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
- 私有、构造、被final修饰的方法,在调用时都使用invokespecial指令
- 普通成员方法在调用时,使用invokespecial指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
- 静态方法在调用时使用invokestatic指令
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/nyima/JVM/day5/Demo5
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
- dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init:()V” (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
- 终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
- 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
多态原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令
在执行invokevirtual指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的Class
- Class结构中有vtable
- 查询vtable找到方法的具体地址
- 执行方法的字节码
异常处理
try-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
对应字节码指令
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
- 可以看到多出来一个 Exception table 的结构,[from, to)是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的2号位置(为e)
多个single-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) {
i = 20;
}catch (Exception e) {
i = 30;
}
}
}
对应的字节码
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
finally
public class Demo2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
对应字节码
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
//try块
2: bipush 10
4: istore_1
//try块执行完后,会执行finally
5: bipush 30
7: istore_1
8: goto 27
//catch块
11: astore_2 //异常信息放入局部变量表的2号槽位
12: bipush 20
14: istore_1
//catch块执行完后,会执行finally
15: bipush 30
17: istore_1
18: goto 27
//出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow //抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次
finally中的return
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//结果为20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
return i;
} finally {
i = 20;
return i;
}
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0
3: iload_0
4: istore_1 //暂存返回值
5: bipush 20
7: istore_0
8: iload_0
9: ireturn //ireturn会返回操作数栈顶的整型值20
//如果出现异常,还是会执行finally块中的内容,没有抛出异常
10: astore_2
11: bipush 20
13: istore_0
14: iload_0
15: ireturn //这里没有athrow了,也就是如果在finally块中如果有返回操作的话,且try块中出现异常,会吞掉异常!
Exception table:
from to target type
0 5 10 any
- 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
- 至于字节码中第 2 行,似乎没啥用,留个伏笔,看下个例子
- 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
所以不要在finally中进行返回操作
被吞掉的异常
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//最终结果为20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
//这里应该会抛出异常
i = i/0;
return i;
} finally {
i = 20;
return i;
}
}
}
会发现打印结果为20,并未抛出异常
finally不带return
public class Demo4 {
public static void main(String[] args) {
int i = Demo4.test();
System.out.println(i);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 //赋值给i 10
3: iload_0 //加载到操作数栈顶
4: istore_1 //加载到局部变量表的1号位置
5: bipush 20
7: istore_0 //赋值给i 20
8: iload_1 //加载局部变量表1号位置的数10到操作数栈
9: ireturn //返回操作数栈顶元素 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2 //加载异常
15: athrow //抛出异常
Exception table:
from to target type
3 5 10 any
1.3编译期处理
所谓的 语法糖 ,其实就是指 java 编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
默认构造函数
public class Candy1 {
}
经过编译期优化后
public class Candy1 {
//这个无参构造器是java编译器帮我们加上的
public Candy1() {
//即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
super();
}
}
自动拆装箱
基本类型和其包装类型的相互转换过程,称为拆装箱
在JDK 5以后,它们的转换可以在编译期自动完成
public class Demo2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
转换过程如下
public class Demo2 {
public static void main(String[] args) {
//基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
//包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
泛型集合取值
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Demo3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer x = list.get(0);
}
}
对应字节码
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//这里进行了类型转换,将Object转换成了Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
所以调用get函数取值时,有一个类型转换的操作
Integer x = (Integer) list.get(0);
如果要将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作
int x = (Integer) list.get(0).intValue();
可变参数
foreach
switch字符串
在编译期间,单个的switch被分为了两个
- 第一个用来匹配字符串,并给x赋值
- 字符串的匹配用到了字符串的hashCode,还用到了equals方法
- 使用hashCode是为了提高比较效率,使用equals是防止有hashCode冲突(如BM和C.)
- 第二个用来根据x的值来决定输出语句
switch枚举
枚举类
匿名内部类