JVM Runtime Data Area and JVM Instructions
Java运行时数据区以及JVM指令
i=i++
结果为8
i=++i
结果为9
一个class的生命周期
以下面的规范为准:
运行时数据区的构成
PC:peogram counter 程序计数器
DirectMemory:直接内存,JVM可以直接访问内核空间的内存(OS管理的内存),零拷贝(不需要拷贝),NIO用到了,提高效率
MethodArea:方法区,里面有常量池
PC 程序计数器
JVM 栈空间
在每一个线程创建的时候,线程会有自己独立的JVM栈空间,栈中存放的是栈帧
堆空间
所有线程共享同一个堆空间。
堆空间是用来存放所有类实例和数组空间分配的运行时数据区。
方法区
所有线程共享同一个方法区。
方法区用来存放 per-class structors
Perm Space 与 Meta Space容易混淆,他们只是Method Area的两的不同版本的实现。
运行时常量池
栈帧
- 局部变量
- 操作数栈
- 动态链接
指向运行时常量池里面的符号链接,看有没有解析,如果没有解析,就动态解析,如果已经解析了,就拿过来使用。例如,A方法要调用B方法,B方法在哪儿呢?就要去常量池里面找。 - 返回值地址
A方法调用了B方法,如果有返回值,要记录返回值返回到那个地方,也就是记录继续执行的位置。
回到面试题
- 理解局部变量表
- 理解操作数栈
- 理解一些常用的指令
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;
public class TestIPulsPlus {
public static void main(String[] args) {
int i = 8;
i = i++;
// i = ++i;
System.out.println(i);
}
}
i=i++ 生成的指令
执行过程分析
i=++i 生成的指令
下面这个 i 因为超过了 127,所以用的是sipush,而不是上图中的bipush
为什么我们可以在非static方法中使用this?因为this在局部变量表中是已经存在的。
(局部变量表中,0位置是this,1位置是k,2位置是i)
下面这个例子,之前有一道面试题:DCL 单例为什么要加 volitile?因为你看下面的第一条指令,我们知道,刚new出来对象是半初始化的对象,只是赋一个默认值,而involespecial才是调用构造方法,给变量赋初始值,而这两条指令之间是可能会发生指令重排的。
返回值
递归的调用
下面是m方法的执行,没有把main方法放上来
在这个3层递归中,使用到的是3个栈
另外,我们看到指令前面的数字 0,1,2,5,6,…,没有3,4的原因,是2指令的字节数比较多,占用了后面的字节数
总结
invokeXXX指令
invokestatic
调用一个静态方法
invokevirtual
new一个对象,调用一个非静态方法
自带多态:new 的是哪个对象,调用的就是哪个对象的方法
invokespecial
调用实例方法;对超类、私有和实例初始化方法调用的特殊处理
可以直接定位的,不需要多态的方法
- private方法
- 构造方法
final方法不是invokespecial的,它是invokevirtual的。
invokeinterface
调用接口方法
invokedynamic
JVM最难的指令
invokedynamic是lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
每一个lambda表达式都有一个自己的内部类,java没有纯粹的函数。匿名内部类每次都是动态产生的。
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;
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不回收)
}
@FunctionalInterface
public interface I {
void m();
}
public static class C {
static void n() {
System.out.println("hello");
}
}
}
关于Lambda表达式的一个坑
如果你用Lambda表达式写出了这样的代码:
for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)
在1.8之前有一个巨大的bug,就是你在里面产生了很多对象,但是Perm Space在FGC的时候是不会回收的。
Perm Space和Meta Space的区别?
- Perm Space (<1.8)
字符串常量位于PermSpace
FGC不会清理
大小启动的时候指定,不能变 - Meta Space (>=1.8)
字符串常量位于堆
会触发FGC清理
不设定的话,最大就是物理内存
思考:
如何证明1.7字符串常量位于Perm,而1.8位于Heap?
提示:结合GC, 一直创建字符串常量,观察堆,和Metaspace