JVM内存区域划分
转载请标明地址:http://www.cnblogs.com/boothsun/p/8110204.html
前言
在了解JVM内存区域划分之前,先看下java程序的执行过程:
如上图所示,首先java源代码文件(.java)会被java编译器编译为java字节码文件(.class),然后由JVM类加载器加载各个类的字节码文件,加载完毕后交由JVM执行引擎执行。
在程序执行过程中,JVM会用一段空间存储运行过程中需要用到的数据和信息,这段空间称为Runtime Data Area(运行时数据区),也就是JVM内存。
我们常说的JVM内存管理,就是指对这段空间的管理(如何分配和回收内存空间)。
运行时数据区
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
1、程序计数器
-
程序计数器 = 当前线程所执行的字节码行号指示器。
-
线程私有的,生命周期与线程相同。(在JVM中,多线程是由线程轮流切换来获取CPU的执行时间片,在某一个确定的时间,一个CPU只会处理一个线程中的指令,因此,为了线程切换后能回到正确的执行位置,每条线程都需要一个独立的线程计数器,各条线程计数器互不影响,独立存储)
-
线程执行
Java
方法时,记录其正在执行的虚拟机字节码指令地址。 -
线程执行
Native
方法时,计数器记录为空(Undefined
)。(原因是:程序计数器记的是字节码的指令地址,而对于本地方法,是操作系统层面的方法,JVM无法记录执行过程)。 -
所占空间小且固定,唯一在
Java
虚拟机规范中没有规定任何OutOfMemoryError
情况区域。
2、java虚拟机栈
Java栈是Java方法执行过程的内存模型:
每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈和出栈的全过程。(线程当前执行的方法所对应的栈帧必定位于Java栈的顶部)
线程执行期间,每个方法执行时都会创建一个栈帧(Stack Frame) ,用于存储 局部变量表、操作数栈、指向运行时常量池的引用、方法返回地址等信息。
1、局部变量表
用于存储方法参数和局部变量;
局部变量表在编译期间分配内存空间,可以存放编译期的各种变量类型:
-
基本数据类型 :
boolean
,byte
,char
,short
,int
,float
,long
,double
等8
种; -
对象引用类型 :
reference
,指向对象起始地址的引用指针; -
返回地址类型 :
returnAddress
,返回地址的类型。
2、操作数栈
和局部变量表不同的是,它不是通过索引来访问,而是通过标准的栈操作 — 压栈和出栈来访问。
栈最典型的一个应用就是用来对表达式求值。
想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作栈来完成的。
3、指向运行时常量池的引用
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
4、方法返回地址
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
当一个方法返回时,可能依次进行以下 3
个操作:
-
恢复上层方法的局部变量表和操作数栈。
-
把返回值压入调用者栈帧的操作数栈。
-
将
PC
计数器的值指向下一条方法指令位置。
小结:
注意:在Java虚拟机规范中,对这个区域规定了两种异常。
其一:如果当前线程请求的栈深度大于虚拟机栈所允许的深度,将会抛出 StackOverflowError
异常(在虚拟机栈不允许动态扩展的情况下);
其二:如果扩展时无法申请到足够的内存空间,就会抛出 OutOfMemoryError
异常。
3、本地方法栈
本地方法栈和 Java
虚拟机栈发挥的作用非常相似,主要区别是 Java
虚拟机栈执行的是 Java
方法服务,而本地方法栈执行 Native
方法服务。
4、堆
堆是被所有线程共享的、堆是用来存储对象本身的以及数组
在 Java
中,堆被划分成两个不同的区域:新生代 ( YoungGeneration
) 、老年代 ( OldGeneration
) 。
新生代 ( Young
) 又被划分为三个区域:一个 Eden
区和两个 Survivor
区 - FromSurvivor
区和 ToSurvivor
区。
简要归纳:新的对象分配是首先放在年轻代 ( YoungGeneration
) 的 Eden
区, Survivor
区作为 Eden
区和 Old
区的缓冲,在 Survivor
区的对象经历若干次收集(15)仍然存活的,就会被转移到老年代 Old
中。
5、方法区
方法区和 Java
堆一样,为多个线程共享,它用于存储类信息、常量、静态常量和即时编译后的代码等数据。
运行时常量池是方法区的一部分, Class
文件中除了有类的版本、字段、方法和接口等描述信息外, 还有一类信息是常量池,用于存储编译期间生成的各种字面量和符号引用。
在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
为什么将永久代转为元数据?
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
其他:
直接内存不属于虚拟机运行时数据区的一部分,也不是 Java
虚拟机规范中定义的内存区域。
JavaNIO
允许 Java
程序直接访问直接内存,通常直接内存的速度会优于Java堆内存。因此,对于读写频繁、性能要求高的场景,可以考虑使用直接内存。