JVM 运行时内存结构
1.JVM内存模型
JVM运行时内存=共享内存区+线程内存区
1).共享内存区
共享内存区=持久带+堆
持久带=方法区+其他
堆=Old Space+Young Space
Young Space=Eden+S0+S1
(1)持久带
JVM用持久带(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池等等。可通过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。Permanent Space并不等同于方法区,只不过是Hotspot JVM用Permanent Space来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区。
(2)堆
堆,主要用来存放类的对象实例信息。
堆分为Old Space(又名,Tenured Generation)和Young Space。
Old Space主要存放应用程序中生命周期长的存活对象;
Eden(伊甸园)主要存放新生的对象;
S0和S1是两个大小相同的内存区域,主要存放每次垃圾回收后Eden存活的对象,作为对象从Eden过渡到Old Space的缓冲地带(S是指英文单词Survivor Space)。
堆之所以要划分区间,是为了方便对象创建和垃圾回收,后面垃圾回收部分会解释。
2).线程内存区
线程内存区=单个线程内存+单个线程内存+.......
单个线程内存=PC Regster+JVM栈+本地方法栈
JVM栈=栈帧+栈帧+.....
栈帧=局域变量区+操作数区+帧数据区
在Java中,一个线程会对应一个JVM栈(JVM Stack),JVM栈里记录了线程的运行状态。JVM栈以栈帧为单位组成,一个栈帧代表一个方法调用。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。
(1)局部变量区
局部变量区,可以理解为一个以数组形式进行管理的内存区,从0开始计数,每个局部变量的空间是32位的,即4字节。基本类型byte、char、short、boolean、int、float及对象引用等占一个局部变量空间,类型为short、byte和char的值在存入数组前要被转换成int值;long、double占两个局部变量空间,在访问long和double类型的局部变量时,只需要取第一个变量空间的索引即可。
例如:
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) { return 0; } public int runInstanceMethod(char c,double d,short s,boolean b) { return 0; }
对应的局域变量区是:
runInstanceMethod的局部变量区第一项是个reference(引用),它指定的就是对象本身的引用,也就是我们常用的this,但是在runClassMethod方法中,没这个引用,那是因为runClassMethod是个静态方法。
(2)操作数栈
操作数栈和局部变量区一样,也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。操作数栈是临时数据的存储区域。
例如:
int a= 100; int b =5; int c = a+b;
对应的操作数栈变化为:
从图中可以得出:操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。
PS:JVM实现里,有一种基于栈的指令集(Hotspot,oracle JVM),还有一种基于寄存器的指令集(DalvikVM,安卓 JVM),两者有什么区别的呢?
基于栈的指令集有接入简单、硬件无关性、代码紧凑、栈上分配无需考虑物理的空间分配等优势,但是由于相同的操作需要更多的出入栈操作,因此消耗的内存更大。 而基于寄存器的指令集最大的好处就是指令少,速度快,但是操作相对繁琐。
示例:
package com.demo3; public class Test { public static void foo() { int a = 1; int b = 2; int c = (a + b) * 5; } public static void main(String[] args) { foo(); } }
基于栈的Hotspot的执行过程如下:
基于寄存器的DalvikVM执行过程如下所示:
上述两种方式最终通过JVM执行引擎,CPU接收到的汇编指令是:
(3)帧数据区
帧数据区存放了指向常量池的指针地址,当某些指令需要获得常量池的数据时,通过帧数据区中的指针地址来访问常量池的数据。此外,帧数据区还存放方法正常返回和异常终止需要的一些数据。