Runtime Data Area运行时数据区
概念
见名知意,就是JVM运行时的数据区,我们需要的数据一般都在这里.
前面已经知道,一个class文件,经过load-link-initialize加载到JVM.
然后,就是经过运行时引擎(run engine)进入运行时数据区(runtime data area).
最权威的参考文档:Java Virtual Machine Specification
结构
总体上分为线程私有和线程共享的两类:
- 每个线程私有的区域: PC,VMStack,NativeMethodStack
所有线程共享的区域:Heap,MethodArea,DirectMemory
(MethodArea的实现:当JDK<1.8时 由PermSpace实现, 当JDK>=1.8时由MetaSpace实现)<br /> 每个区域详情:<br />[点击查看【processon】](https://www.processon.com/embed/5ee0529f07912929cb3704d7)<br />Program Counter(PC)程序计数器: 存放指令位置,虚拟机不断的从PC中取出下一条指令的位置,找到指令去执行
heap堆:对象存放的地方,所有线程共享. 重点,后面GC详细学
JVM stacks: JVM管理的栈,每个线程有自己私有的JVM stack,其中的每个方法对应一个栈帧(frame). 重点
native method stacks本地方法栈:线程私有,通过JNI等调用C和C++方法时用的栈
栈包括JVM stacks和native method stacks
平时一般说到栈,指的都是JVM stacks
Direct Memory直接内存: 从JVM内可以访问OS管理的内存(内核空间),可以提高IO效率(零拷贝),JDK1.4新增的.
比如一个网络请求传过来一个数据到OS内核空间中,1.4以前使用这个数据时需要把内核空间中的数据拷贝到JVM内存中;
1.4之后,通过NIO可以使用直接内存,直接访问内核空间的该数据,不需拷贝.
method area方法区:class结构存放的地方,所有线程共享
方法区中还有块 run-time constant pool 常量池,存放class文件的常量池(constant_pool)
方法区是一个逻辑上的概念
JDK1.8之前,方法区由永久区(Permament Space)实现,字符串常量也在永久区,FGC不会清理;JVM启动时指定永久区空间大小,不能改变
自JDK1.8开始,方法区由Meta Space实现,字符串常量位于堆,会触发FGC 被清理.可以指定大小,如果不指定的话,最大就是物理内存空间.
栈帧Frame
A Frame is used to store data and partial results,as well as to perform dynamic linking,return values for methods,and dispatch exceptions.
主要包括:
局部变量表(local variable table)
注意,形参也在局部变量表;如果是成员方法,this也在局部变量表
变量表从下标0开始,排序优先级为:this > 形参 > 方法中的变量
形参和方法中的变量按出现的顺序排列操作数栈(Operand Stacks)
里面存的是一个个操作数,比如_load指令会把一个值压栈,_store指令会把栈顶的值弹出.
(对于long的处理(store and load),多数虚拟机的实现都是原子的
jls 17.7,没必要加volatile)dynamic linking,
指向运行时常量池
比如a() -> b(),方法a调用了方法b,class文件解析时把方法放在运行时常量池中了,这个就是用来找到b
jvms2.6.3
https://blog.csdn.net/qq_41813060/article/details/88379473return address
a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方
栈的指令集
这个指令集是JVM很底层的东西了,这个看明白了,任何其他语言也就很容易理解了,大家都差不多.
马老师说:别人看山是山,你看山是看到的都是各种分子,所以任何的山都是一样的.哈哈哈
第一个境界是 看山是山;
第二个境界是 看山不是山;
第三个境界是 看山还是山.
JVM学完这节,勉强到第二境界了,奔着第三境界前进!
具体怎么奔?参看jvms!
常见的几个指令:
_store : 出栈,并赋值给一个局部变量
_load : 压栈
invoke_XXX : 调用方法,这个指令很复杂,下面单独拎出来说
dup : 把栈顶的东西复制一份,然后把其压栈,一般是调用实例方法前会dup
看一道面试题,认识指令集,更好的理解栈帧
实际中肯定没人写这种代码了,下面这种写法就是为了理解栈帧
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
i = i++; // 结果是8
//i = ++i; // 结果是9
System.out.println(i);
}
}
具体原因我们来通过JClassLib查看字节码,一探究竟:
无论i = i++;还是i = ++i;,局部变量表是一样的:
查看的是main方法,静态方法,所以0位置是形参String[] args
1位置是方法中的第一个变量 i.
当i = i++; 时,用JClassLib查看字节码:
0 bipush 8 // 把8压栈,用int扩展这个立即数
2 istore_1 // 出栈,把栈顶的数弹出,并赋给局部变量表下标为1的变量,就是变量i; 到此 int i = 8; 这句完事
3 iload_1 // 把i压栈,从(下标1)本地变量表中拿值放到栈中(Operand Stack),此时i=8,所以栈中是8
4 iinc 1 by 1 // 局部变量表1的位置自增1,(第一个1表示局部变量表的下标),此时局部变量表的i值为9
7 istore_1 // 出栈,把栈顶的值赋给局部变量表下标为1的变量,此时栈中仍是8,所以最后i=8. 到此,i=i++; 这句完事
// 下main的就是System.out.println(i);了
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
当i = ++i; 时,用JClassLib查看字节码:
0 bipush 8 // 8压栈
2 istore_1 // 8出栈, int i=8;完事
3 iinc 1 by 1 // i自增1,此时i=9
6 iload_1 // 把i压栈,此时i已经时9了,栈中也是9
7 istore_1 // 9出栈,赋值给i,所以此时i=9, i=++i;完事
// 下main的就是System.out.println(i);了
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
设计一台机器的指令集,有两种做法:
基于栈的指令集(JVMStack选择的方式)
基于寄存器的指令集(汇编语言)(HotSpot的局部变量表类似于寄存器)
最终在硬件层面都是基于寄存器的指令集
单条指令也不一定是原子性的
总结:
i++ 是先把i的值压栈,然后把局部变量i自增,代码层面可以理解为先使用i的当前值,用完了让i自增;
++i 是先把局部变量i自增,然后把i的值压栈,代码层面可以理解为先给i+1,然后使用i自增后的值.
创建对象&调用其实例方法的指令集
Java代码:
public static void main(String[] args) {
Hello_02 h = new Hello_02();
h.m1();
}
public void m1() {
int i = 200;
}
main方法的字节码指令:
0 new #2 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02> // 在堆中创建对象,并把对象的地址压栈,此时该对象各属性为默认值
3 dup // 把栈顶的元素复制一份,压栈,此时栈顶两份该对象的地址
4 invokespecial #3 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.<init>> // 出栈,拿到对象后调用其构造方法
7 astore_1 // 出栈,并赋值给h
8 aload_1 // 把h压栈
9 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.m1> // 出栈,调用其实例方法
12 return
m1方法的字节码指令:
0 sipush 200 // 把200压栈,开头的s代表short,200已经超过byte的范围了,最后会扩展为int
3 istore_1 // 出栈,赋值给i
4 return
带返回值的方法的指令集
Java代码:
public static void main(String[] args) {
Hello_02 h = new Hello_02();
h.m1();
int m1 = h.m1();
}
public int m1() {
int i = 200;
return i;
}
main方法的指令集:
0 new #2 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02>
3 dup
4 invokespecial #3 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.<init>>
7 astore_1
8 aload_1
9 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.m1>
// 上面都一样,不谈了
12 pop // 出栈,因为代码中第一次调用m1()方法是没有接收它的返回值,所以只是出栈
13 aload_1
14 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.m1>
17 istore_2 // 出栈,并赋值给m1变量
18 return
m1方法的指令集:
0 sipush 200
3 istore_1
// 上面都一样,不谈了
4 iload_1 // 这里因为代码有返回值,所以就把返回值压栈了,为了下一步返回做准备
5 ireturn // 自己的栈出栈,拿到返回之后往main方法的栈帧中压栈,由main方法去弹栈拿到返回值
递归求阶乘方法的指令集
Java代码:
public static void main(String[] args) {
Hello_04 h = new Hello_04();
int i = h.m(3);
}
public int m(int n) {
if (n == 1) {
return 1;
}
return n * m(n - 1);
}
main方法的指令集就不谈了,就看看m方法的指令集:
0 iload_1 // 压栈,形参n
1 iconst_1 // 压栈,常量int 1
2 if_icmpne 7 (+5) // 出栈两个(此时栈空),比较他们的值是否相等,如果是相等就转到编号5那行继续执行,否则转到编号为7那行
5 iconst_1 // 压栈,常量 int 1
6 ireturn // 返回结果
7 iload_1 // 压栈,形参n
8 aload_0 // 压栈,this
9 iload_1 //压栈,形参n
10 iconst_1 // 压栈,常量 int i
11 isub // 出栈两个,相减,结果压栈
12 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_04.m> // 出栈两个,第一个是方法参数,第二个是this
15 imul // 出栈两个,相乘,结果压栈
16 ireturn // 返回结果
invoke_XXX
- InvokeStatic 调静态方法
- InvokeVirtual 调一般的实例方法,支持多态
- InvokeInterface 通过接口调用的方法
InvokeSpecial
可以直接定位,不需要多态的方法
具体有:private 方法 , 构造方法
final方法不是invokeSpecialInvokeDynamic
JVM最复杂的指令,>=JDK1.7
lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
(lambda表达式,其实就是一个语法糖,匿名内部类的简化写法,但是具体细节应该有很多不同,回头再学习.)