一、概念
运行时数据区,Runtime Data Area,用于保存java程序运行过程中需要用到的数据和相关信息;经常说的把数据读到内存,包括类加载之后的信息,从磁盘读取文件信息等。
二、内存布局
三、各区域详解
1.程序计数器(Program Counter)
线程私有的一小块内存区域,用于存放执行指令的位置;
由于现代分时操作系统一般都采用时间片轮转执行的方式进行调度,对于单核CPU来说,在某一时刻只能有一个处于就绪状态的线程能获取到CPU并执行,执行完一个时间片或者主动放弃CPU时,就切换到其他线程执行,也就是说在一个时间片内线程不一定运行完成,需要等待下一次调度,所以需要程序计数器来记录下一次被调度时需要执行指令的地址。
java虚拟机循环运行的伪代码可以粗略表示为:
while(not end){ 从PC中取出指针指向的地址; 执行该地址对应的指令; PC中的指针指向下一条将要被执行的指令; if(时间片结束){ PC记录下一条需要执行指令的位置 }else{ //线程结束或主动放弃CPU end } }
2.虚拟机栈(JVM Stacks)
用于描述Java中方法执行的过程,包括方法调用,返回值等,是线程私有的一块内存区域。
当执行或调用一个方法时,都会创建一个栈帧(Stack Frame),官方描述如下:
a frame is used to store data and partial results,as well as to perform dynamic linking,return values for method,and dispatch exceptions.
也就是用于存储局部变量和部分计算结果,包括局部变量表、操作数栈、动态链接,返回值地址等。
(1)局部变量表(Local Variable Table)
保存方法参数和内部使用到的局部变量(必须初始化),作用域仅在这个方法内,方法执行完成结束生命周期;
用jclasslib观察不同的方法编译后的字节码
测试类
public class Test {
//非static带形参的方法 public void add(String prev,int next){ String result = prev + next; System.out.println(result); } //static方法,包括main方法 public static void append(String prev,int next){ String result = prev + next; System.out.println(result); } }
概览
Methods中存放的就是编译后各方法的字节码
无参构造方法局部变量表中只有一个this。
static方法
非static的普通方法
可以看出非static方法(包括构造方法)局部变量表下标为0的位置使用存放的都是this,代表本实例,这也是为什么我们能在这些方法里面直接使用this,调用当前实例的其他方法;其他变量按出现的先后顺序依次存储在局部变量中;
cp_info#xxx,表示符号引用存放在字符串常量池中的xxx下标位置处。
(2)操作数栈(Operand stack)
每一个栈帧都对应着一个操作数栈,用于方法体中操作数运算时的入栈和出栈,代表的是运算的过程。
用一个典型的笔试题来理解操作数栈,++x和x++字节码指令的执行顺序
测试类
public static void main(String[] args) { int x = 8; x = x++; System.out.println(x); } public static void main(String[] args) { int x = 8; x = ++x; System.out.println(x); }
x=x++字节码
//测试x=x++ 0 bipush 8 //把常量8压栈 2 istore_1 //常量8出栈并存储到局部变量表下标为1的位置,也就是把8赋值给x 注:前两条指令代表int x=8执行完成,虽然赋值操作是两条指令,但是由于8压栈是不能被修改的,所以总体也是原子性的。 3 iload_1 //把局部变量表下标为1的位置的变量值拿出来压栈 4 iinc 1 by 1 //局部变量表下标为1的位置执行自增1的操作 注:此处操作的不是压入栈中的数据,而是局部变量表中的数据,也就是局部变量表中的x从8变为了9 7 istore_1 //把栈中的数据8重新存到局部变量表中下标为1的位置,这个时候x又从9变为了8 8 getstatic #2 <java/lang/System.out> //调用System.out进行输出 11 iload_1 12 invokevirtual #3 <java/io/PrintStream.println> 15 return
x=++x字节码
//测试x=++x 0 bipush 8 2 istore_1 注意:这里少了一条iload_1指令,也就是没有将8压栈 3 iinc 1 by 1 //这里同样的是把局部变量表下标为1的位置执行自增1的操作 6 iload_1 //这里取出来的数据是自增之后的值9 注:所以x=++x最后执行的结果是9 7 istore_1 8 getstatic #2 <java/lang/System.out> 11 iload_1 12 invokevirtual #3 <java/io/PrintStream.println> 15 return
总结:可以看出x=x++比x=++x多执行了一条指令,把原数据先压栈,然后自增,最后又把原数据进行压栈,导致结果没变;x=x++是把原数据赋值给x,而x=++x是把自增后的数据赋值给x。
x=1是原子性操作,因为1在压栈的过程中是不能被改变的;而x++和++x都不是原子性操作,是因为自增操作和赋值操作是两条指令,CPU执行这两条指令时可能会重排序(CPU执行指令的效率会比从内存中读取数据高很多)。
(3)动态链接(Dynamic Linking)
指向常量池,用于标识变量,方法名,类名等的符号引用;Java中的类经过加载后会将符号引用进行解析,然后存于常量池中。
(4)返回值地址(Return Address)
被调用方法执行结束后返回值存放的地址以及调用方法应该继续执行的指令位置;比如被调用方法return new Object(),那么会把new Object()在堆中的位置记录下来。
注:JVM的指令集可以到https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html(不同jdk版本)进行查阅。
递归执行(方法调用)的指令
测试代码
字节码指令集
main方法调用
fibo方法递归调用
0 iload_1 1 ifeq 9 (+8) 4 iload_1 5 iconst_1 -> 把1作为常量; 6 if_icmpne 11 (+5) -> 出栈并比较两个值,如果不相等则调到第11条指令; 9 iconst_1 -> 相等,则再次把1压栈; 10 ireturn -> 返回1; 11 aload_0 -> aload表示引用压栈,0的位置是this,把this压栈 12 iload_1 ->下标为1的位置(变量n)压栈 13 iconst_1 ->常量1压栈 14 isub ->两个数相减 15 invokevirtual #4 <cn/merson/jvm/JvmStackTest.fibo> ->从常量池中找到fibo方法并调用,参数为上一步相减的结果 18 aload_0 19 iload_1 20 iconst_2 ->常量2压栈 21 isub 22 invokevirtual #4 <cn/merson/jvm/JvmStackTest.fibo> ->调用fibo方法 25 iadd -> 将15和22执行的结果相加,这里是在15和22都执行完成了之后才会执行,也就是栈桢一直位于虚拟机栈中。 26 ireturn -> 返回到main中 注意:如果main中是用变量接受返回值,这里会把值直接写到main方法中的局部变量表(xstore指令); 指令15和22会分别执行自己的fibo指令,一直执行到递归出口,也就是第六条指令相等的时候。
JVM Stack规定的两种异常
如果线程请求的栈深度大于虚拟机所允许的深度,抛出 StackOverflowError 异常;
如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
3.本地方法栈(Native Method Stacks)
和虚拟机栈类似,只不过虚拟机栈用于执行Java本身的方法,而本地方法栈用于执行Native方法(非Java代码实现),是线程私有的内存空间。
4.堆(Heap)
所有线程共享的内存空间,绝大多数Java对象的分配区域,垃圾回收的主要区域;具体的可以阅读https://www.cnblogs.com/merson1314/p/13680056.html,对象分配的过程及垃圾回收过程;https://www.cnblogs.com/merson1314/p/13673444.html,堆内存逻辑分区。
5.方法区(Method Area)
所有线程共享的内存空间,用于存放Class对象等,是一个逻辑概念,在JDK1.8前后分为PermSpace和MetaSpace两种不同的实现方式。
官方描述:The JVM has a method area that is shared among all JVM threads.It stores per-class structures.
PermSpace,永久代:
JDK1.8之前运行时常量池也存放在永久代中,在程序启动时指定永久代的大小并且不能动态扩容(运行期间也不能修改),垃圾回收不会清理这块内存区域,当永久代内存不够用时会抛出OOM异常。
MetaSpace,元数据区:
JDK1.8之后运行时常量池存放于堆中,便于垃圾回收,如果启动时不指定元数据区大小,理论上来说最大可用内存就是机器的物理内存,当然,MetaSpace支持动态申请内存,Full GC会回收MetaSpace。
运行时常量池:
主要用于存放类,变量等的符号引用和直接引用,通过下标进行识别,符号引用解析之后的直接引用也会存放在运行时常量池中。
方法区回收主要是回收废弃的常量和无用的类;
废弃的常量表示没有任何一个地方引用这个常量,当内存不够用时,会回收。
无用的类必须同时满足:
①Java堆中不存在该类的任何实例,也就是该类的所有实例都已经被回收;
②加载该类的ClassLoader已经被回收;
③该类对应的Class对象在任何地方没有引用了,也不能通过反射访问该类的方法。
6.直接内存(Direct Memory)
直接内存不属于运行时数据区,Java中直接分配堆外内存,也就是用户空间(线程)直接访问操作系统或者内核空间,多用于网络访问提升效率。
NIO使用的就是直接内存,zero copy用把数据从内核空间拷贝到JVM的内存空间。
7.内存溢出和内存泄露
内存溢出(OutOfMemoryError)
当堆内存已经不能再接受分配新的对象,或者老年代不能接受年轻代准备升级到老年代的对象,并且Full GC之后堆内存还是不够用,会抛出OOM。
内存泄露(Memory Leak)
在内存中对象已经不需要的时候(应该被GC),但实际上仍然保留着这块内存和它的访问方式(引用),没有被回收;不一定产生OOM。