运行时数据区(Runtime Data Area)
参考jvms(java virtual machine specification)和jls(java language specification)
从一则面试题说起:
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
//此方式输出8
i = i++;
//此方式输出9
// i = ++i;
System.out.println(i);
}
}
PC(Program Counter 程序计数器):
1、存放指令位置
2、虚拟机的运行,类似于这样的循环:
while(not end) {
取PC中的位置,找到对应位置的指令
执行该指令:
PC++
}
JVMStack【线程独有的】:
java运行中写的每个线程对应的一个栈,每个方法对应一个栈帧
native method stacks:指的是本地方法(C/C++),调用的JNI
Direct Memory(直接内存):属于NIO的内容,java 1.4新增的
,为了增加IO的效率。归操作系统管【从JVM里面可以直接访问操作系统
管理的内存的,用户空间直接访问内核空间的内存】,实现了0拷贝
Heap【线程共享的】:
Method Area【线程共享的】:
存储每一个类的结构
方法区实现【1.8以前和以后是两个不同的实现】:
1、perm Space(<1.8)永久区
字符串常量位于PermSpace
FGC不会清理
大小启动的时候指定,不能变
2、Meta Space(>=1.8) 元空间
字符串常量位于堆
会触发FGC清理
Runtime Constant Pool:class文件里面的常量池,运行的时候会扔到这里
不设定的话,最大就是物理内存
如何证明1.7字符串常量位于perm,而1.8位于Heap?
结合GC,一直创建字符串常量,观察堆和MetaSpace的情况,看溢出时的报错也可以。
总结:
VMS:virtual machine stack
nms:native method stack
栈帧【每个方法对应一个栈帧】
dynamic linking:参照jvms2.6.3
一个线程有自己的线程栈,每个线程栈里面装着一个个的栈帧,每个栈帧里面有个dynamic linking,这个linking指向的是
运行时常量池里面的符号链接,看是否解析了,如果解析了则直接用,如果没解析则动态解析。如a方法调用了b方法,b的内容得去运行时常量池里面去找。
return address(返回值地址):a()->b(),方法a调用了方法b,b方法的返回值放什么地方。
回到面试题目:
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
//此方式输出8
i = i++;
//此方式输出9
// i = ++i;
System.out.println(i);
}
}
查看它的指令:
首先看局部变量表:里面有两个变量分别是args,和i
首先执行bipush 8,将它当成一个int类型扔到栈里面
istore_1
把栈顶元素出栈,放在下标值为1的局部变量表里面。
至此i=8完成
iload_1
将局部变量表1位置上的数压栈,就是8压栈
iinc 1 by 1; 将局部变量表为1的位置上面的数加1,此时变成9
istore_1:将操作数栈中的8拿回去赋值到局部变量表1位置所对应的值,此时将8
赋值回去局部变量表1对应的值修改为8
因此最后的返回结果为8。
指令集补充: 1、基于栈的指令集(JVM)
2、基于寄存器的指令集(汇编涉及)
hotspot的local variable table 类似于寄存器
栈的执行流程
Java虚拟机栈(Java栈)每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。是线程私有的。
操作数栈:每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称为表达式栈。操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈,操作数栈是为了存计算的临时中间结果的,而局部遍历表一般是固定的,用来放方法的参数和方法内部定义的变量
当大于127的时候采用sipush
注意下图的this,只要不是static方法的情况,这个方法的调用是需要对象的,然后这个对象
被扔进了局部变量表里面了
如图首先局部变量表0-3位置上对应的变量依次是this,a,b,c。
依次把3压栈
依次吧4压栈
然后再把3+4的结果压栈
然后弹出栈顶元素 7
压在c里面
public class Hello_02 {
public static void main(String[] args) {
Hello_02 h = new Hello_02();
h.m1();
}
public void m1() {
int i = 200;
}
}
m1字节码
main方法字节码:
new完对象之后会把这个对象的地址压栈,对象此时是一个默认值
dup命令是将栈里面的那个地址再复制一份,现在栈里面就有两个元素指向了那个对象了,
再调用invokespecial执行默认构造方法,会将栈顶的元素给弹出来,做计算,这个过程中
对象里面的值就不再是默认值而是初始值了,并且也调用了初始化方法。
此时栈里面只有一个对象地址了。然后执行astore_1,将栈顶元素赋值给本地变量表的1位置上
。aload1是将h入栈,然后调用invokevirtual将这个h弹出栈顶,然后调用m1方法。
注意指令重排是:调用invokespecial和astore_1进行指令重排
main本地变量表:
由于main调用了m1方法,当main要调用m1方法的时候,main停止执行,等m1栈帧执行弹出完毕再执行
递归调用:
递归的阶乘。
这里我们来分析上图的m方法的字节码:
本地变量表是0位置this,1位置n
1、iload_1 首先将n=3入栈
2、iconst_1,将常量值1压栈
3、if_icmpne 如果不等于1【注意这里是load_1和iconst_1都弹出去才能比较】,
就跳到第七条指令去执行 iload_1又将3压栈,再执行aload_0,将this压栈,iload_1
又将3压栈,iconst_1,将常量1压栈,isub再将两数相减。再调用m方法【注意此时栈中包含元素
为3 this 2 】,执行m方法的时候需要两个参数,第一个参数是n就是栈顶的2,还要把this传给它,因为
调用的时候还需要知道是哪个对象在调用【此时弹出this 和 2】,此时n的值变成2。
直到当n=1的时候则返回,执行m(2)的后续逻辑,最后再执行m3的后续逻辑【imul】执行相乘的逻辑。
小总结
clinit :静态语句块的初始化【没有显示的调用看不出来】
init:构造方法初始化
invoke指令:
- InvokeStatic:调用静态方法
public class T01_InvokeStatic {
public static void main(String[] args) {
m();
}
public static void m() {}
}
- InvokeVirtual:自带多态的根据对象来调用方法,看压栈的是哪个对象
public class T02_InvokeVirtual {
public static void main(String[] args) {
new T02_InvokeVirtual().m();
}
public void m() {}
}
- InvokeInterface
public static void main(String[] args) {
//作为interface来调用的
List<String> list = new ArrayList<String>();
list.add("hello");
//这个是invokevirtual
ArrayList<String> list2 = new ArrayList<>();
list2.add("hello2");
}
- InovkeSpecial
可以直接定位,不需要多态的方法
private 方法 , 构造方法
注意final也是invokevirtual的,不是invokespecial的。
public class T03_InvokeSpecial {
public static void main(String[] args) {
T03_InvokeSpecial t = new T03_InvokeSpecial();
t.m();
t.n();
}
public final void m() {}
private void n() {}
}
- InvokeDynamic
JVM最难的指令
lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ,ASM,
运行时动态产生的class,会用到的指令
类似于一个函数式的指针,C::n指向的时n这个函数
public class T05_InvokeDynamic {
public static void main(String[] args) {
I i = C::n;
I i2 = C::n;
I i3 = C::n;
I i4 = () -> {
C.n();
};
System.out.println(i.getClass());
System.out.println(i2.getClass());
System.out.println(i3.getClass());
//for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)小于1.8会产生OOM
}
@FunctionalInterface
public interface I {
void m();
}
public static class C {
static void n() {
System.out.println("hello");
}
}
}