Runtime Data Area 运行时数据区
最权威的参考文档:Java Virtual Machine Specification
Java运行时数据区以及JVM指令
i=i++结果为8
i=++i结果为9
class的生命周期
运行时数据区的构成
PC:peogram counter 程序计数器
存放指令位置
虚拟机的执行类似下面的循环:
while(not end){
取pc中的位置,找到对应位置的指令
执行该指令
PC++;
}
堆heap
所有线程共享同一个堆空间。
堆空间是用来存放所有类实例和数组空间分配的运行时数据区。
重点,后面GC详细学
栈stacks
在每一个线程创建的时候,线程会有自己独立的JVM栈空间,栈中存放的是栈帧(每个方法对应一个栈帧)
本地方法栈native method stacks
对应c、c++编写的native方法,实际上和stacks类似
直接内存Direct Memory
从JVM内可以访问OS管理的内存(内核空间),可以提高IO效率(零拷贝),JDK1.4新增的.
比如一个网络请求传过来一个数据到OS内核空间中,1.4以前使用这个数据时需要把内核空间中的数据拷贝到JVM内存中;
1.4之后,通过NIO可以使用直接内存,直接访问内核空间的该数据,不需拷贝.
简单来说就是JVM可以直接访问的内核空间的内存(OS管理的内存)
NIO,提高效率,实现zero copy零拷贝
方法区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/88379473 - return address
a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方
Instruction栈的指令集
常见的几个指令:
: 静态语句块
: 构造方法
_store : 出栈,并赋值给一个局部变量
_load : 压栈
invoke_XXX : 调用方法,这个指令很复杂,下面单独拎出来说
dup : 把栈顶的东西复制一份,然后把其压栈,一般是调用实例方法前会dup
面试题理解栈的指令集
- 理解局部变量表
- 理解操作数栈
- 理解一些常用的指令
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);
}
}
具体原因我们来通过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
总结:
i++ 是先把i的值压栈,然后把局部变量i自增,代码层面可以理解为先使用i的当前值,用完了让i自增;
++i 是先把局部变量i自增,然后把i的值压栈,代码层面可以理解为先给i+1,然后使用i自增后的值.
设计一台机器的指令集,有两种做法:
- 基于栈的指令集(JVMStack选择的方式)
- 基于寄存器的指令集(汇编语言)(HotSpot的局部变量表类似于寄存器)
比较复杂但是比较快- 最终在硬件层面都是基于寄存器的指令集
单条指令也不一定是原子性的
例子理解
下面这个i因为超过了127,所以用的是sipush,而不是上图中的bipush
为什么我们可以在非static方法中使用this?因为this在局部变量表中是已经存在的。
(局部变量表中,0位置是this,1位置是k,2位置是i)
调用add(3,4)
dup,复制操作数栈的栈顶元素
之前有一道面试题:DCL 单例为什么要加 volitile?因为你看下面的第一条指令,我们知道,刚new出来对象是半初始化的对象,只是赋一个默认值,而involespecial才是调用构造方法,给变量赋初始值,而这两条指令之间是可能会发生指令重排的。
返回值
递归的调用
下面是m方法的执行,没有把main方法放上来
在这个3层递归中,使用到的是3个栈
另外,我们看到指令前面的数字 0,1,2,5,6,…,没有3,4的原因,是2指令的字节数比较多,占用了后面的字节数
invoke指令
- InvokeStatic 调静态方法
- InvokeVirtual 调一般的实例方法,自带多态
- InvokeInterface 通过接口调用的方法
- InvokeSpecial
可以直接定位,不需要多态的方法
具体有:private 方法 , 构造方法
final方法不是invokeSpecial - InvokeDynamic
JVM最复杂的指令,>=JDK1.7
lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
每一个lambda表达式都有一个自己的内部类,java没有纯粹的函数。匿名内部类每次都是动态产生的。
lambda表达式,其实就是一个语法糖,匿名内部类的简化写法,但是具体细节应该有很多不同,
关于Lambda表达式的一个坑
如果你用Lambda表达式写出了这样的代码:
for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)
在1.8之前有一个巨大的bug,就是你在里面产生了很多对象,但是Perm Space在FGC的时候是不会回收的。
思考:
如何证明1.7字符串常量位于Perm,而1.8位于Heap?
提示:结合GC, 一直创建字符串常量,观察堆,和Metaspace