JVM运行时数据区
JVM运行时数据区分为几块区域,分别有:
- 虚拟机栈:线程私有,存放执行方法的栈帧
- 本地方法栈:线程私有,存放执行本地方法的栈帧
- 程序计数器:线程私有,存放线程下一步要执行的指令
- 堆:线程共享,存放对象
- 方法区:线程共享,在jdk1.8之前,方法区是指堆区中的永久代;在jdk1.8之后,方法区是指堆区外的元空间。方法区用于存放字节码文件、常量池、静态变量引用等
- 直接内存:线程共享,直接内存是操作系统内核中的一块内存,被JVM和系统内核程序共享。主要是NIO网络传输时使用。jdk1.8之后,元空间就放在直接内存中,字符串常量池和静态变量引用放在堆区中
字符串常量在jdk1.7是放在永久代中,那个时候永久代必须要设置一个上限。在jdk1.8后,字符串常量放在堆内存中,不是元空间中,而且会被垃圾回收器回收。
虚拟机栈
虚拟机栈是线程私有的空间,其中存放着一个个的栈帧。当线程调用一个方法时,就会生成这个方法的栈帧存到虚拟机栈中,以便方法的执行。
虚拟机栈结构
- 局部变量表:调用的方法中使用到的局部变量存在局部变量表中,方法执行结束,表中的变量就没用了
- 操作数栈:使用局部变量进行运算时,局部变量的值以及运算出来的中间值会存放在操作数栈中
- 动态链接:方法中调用另一个方法时,另一个方法的地址引用
- 返回地址:方法执行结束后,结果返回的地址
以下的代码
public class Test {
public static void main(String[] args) {
int i = 8;
i = i++;
System.out.println(i);
}
}
局部变量表
对于静态方法和对象方法,局部变量表有一个差别:对象方法的局部变量表的0号槽始终放的是this,静态方法的局部变量表中没有this。这也是为什么对象方法中可以直接使用this。
方法执行指令
指令的含义
- bipush 8:将8放到操作数栈中
- istore_1:将操作数栈中的8拿出来,赋值给局部变量表1号槽的i变量
- iload_1:将局部变量表1号槽的i变量的值放到操作数栈中
- iinc 1 by 1:将局部变量表1号槽的i变量的值加1
- istore_1:将操作数栈中的8拿出来,赋值给局部变量表1号槽的i变量
剩下的指令就是打印i的值。
我们将上述的代码稍加变化
public class Test {
public static void main(String[] args) {
int i = 8;
// 此处代码改成i = ++i
i = ++i;
System.out.println(i);
}
}
局部变量表与上述代码相同,执行指令有所差异
iinc 1 by 1这条指令跑到iload_1指令前执行了。这也导致上述两个代码中,第一个代码执行结果是8,第二个代码的执行结果是9。
常见指令介绍
例1
代码
public class Test {
public static void main(String[] args) {
int m = 100;
}
}
局部变量表
指令
0 bipush 100
2 istore_1
3 return
- bipush 100:将100放到操作数栈中。因为100没有超过一个byte代表的整数大小,所以用bi(即一个byte来存储),不过放入操作数栈时会类型转换成int
- istore_1:将操作数栈中的100拿出来,赋值给局部变量表1号槽的m变量
- return:返回,方法执行结束
例2
代码
public class Test {
public static void main(String[] args) {
int m = 200;
}
}
局部变量表
指令
0 sipush 200
3 istore_1
4 return
- sipush 200:将200放到操作数栈中。由于200超过了一个byte代表的整数大小,所有用short来存储,所以是si。不过放入操作数栈时会类型转换成int
例3
代码
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.add(3, 4);
}
public void add(int a, int b) {
int c = a + b;
}
}
main方法局部变量表
main方法指令
0 new #2 <Test>
3 dup
4 invokespecial #3 <Test.<init> : ()V>
7 astore_1
8 aload_1
9 iconst_3
10 iconst_4
11 invokevirtual #4 <Test.add : (II)V>
14 return
- new:做了两件事:1.在堆区开辟一块内存空间,对象属性赋默认值;2.将空间的地址放到操作数栈中
- dup:将操作数栈顶的数据复制一份,并且放到栈顶
- invokespecial:从完整指令看,是调用init方法,即Test的构造方法。不过这条指令需要一个参数,就是堆区中对象的内存空间地址。所以执行这条指令,先将操作数栈顶的地址弹出栈,然后根据地址找到构造方法并执行。此时,操作数栈中还有一个地址。
- astore_1:将操作数栈中的地址弹出,赋值给test
- aload_1:将test变量的值放到操作数栈中
- iconst_3:将常量3放到操作数栈中
- iconst_4:将常量4放到操作数栈中
- invokevirtual:从完整指令看,是调用add方法。指令需要三个参数,一个是对象地址,另外两个是add方法的入参值,分别是3和4。所以,操作数栈顶的3个数都会弹出来。
- return:等add方法执行结束,main方法就执行结束
add方法局部变量表
add方法指令
0 iload_1
1 iload_2
2 iadd
3 istore_3
4 return
- iload_1:将局部变量表1号槽的a变量值3放到操作数栈中
- iload_2:将局部变量表2号槽的b变量值4放到操作数栈中
- iadd:将操作数栈顶的两个int整数值3和4弹出栈,然后相加得到7,并且将7放到操作数栈中
- istore_3:将操作数栈顶的int整数值7弹出栈,赋值给局部变量表3号槽的c
- return:方法结束
例4:
代码
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.add();
}
public int add() {
return 100;
}
}
main方法的局部变量表
main方法的指令
0 new #2 <Test>
3 dup
4 invokespecial #3 <Test.<init> : ()V>
7 astore_1
8 aload_1
9 invokevirtual #4 <Test.add : ()I>
12 pop
13 return
- pop:invokevirtual指令调用完add方法后,由于返回一个int类型数,会将这个数放到main方法的操作数栈中。pop指令会将操作数栈顶的数弹出栈,但是不做其他操作。
例5
代码
public class Test {
public static void main(String[] args) {
Test test = new Test();
int i = test.add();
}
public int add() {
return 100;
}
}
main方法的局部变量表
main方法的指令
0 new #2 <Test>
3 dup
4 invokespecial #3 <Test.<init> : ()V>
7 astore_1
8 aload_1
9 invokevirtual #4 <Test.add : ()I>
12 istore_2
13 return
invokevirtual指令调用add方法返回后,会将add方法的返回结果放到main方法的操作数栈中,istore_2指令会将操作数栈顶的数弹出栈,并且赋值给局部变量表2号槽的i变量。
invoke相关指令
invokestatic
调用静态方法时,字节码指令就是invokestatic
invokespecial
调用构造方法,或者被private修饰的方法时,字节码指令就是invokespecial
invokevirtual
调用对象方法时,字节码指令就是invokevirtual
invokeinterface
使用接口类型变量调用方法时,字节码指令就是invokeinterface
List<Integer> list = new ArrayList<>();
list.add(1);
invokedynamic
使用lambda表达式、动态代理等等语法时,字节码指令就是invokedynamic