Java内存区域简介
JVM拥有自动内存管理机制,对于每一个new操作不需要像C++那样去写配对的delete或者free,这样的机制不容易出现内存泄漏与内存溢出问题,生活看起来很美好的样子。但是现实却是骨干的:由于Java虚拟机拥有内存控制的权利,一旦出现了内存泄漏和内存溢出相关的问题,如果作为程序员的我们不了解虚拟机是怎样使用内存的,那么排查错误将会是一件十分复杂而又艰难的事情。我们下面将要介绍JVM内存的各个区域,介绍这些区域的作用、服务范围和可能存在的问题。
1.1 JVM Runtime Data Area
JVM在执行Java程序时会把其管理的内存划分为几个数据区域,这些数据区都有自己的用途与“生命周期”(这里说生命周期是一个比较形象的说法,主要是有些区域存在建立跟销毁的过程),JVM运行时数据区如图1-1所示[1](文章中的引用都会标注)。
图1-1
1.1.1 Program Counter Register(PCR)
PCR即程序计数器,是一块相对较小的数据空间,计算机、电子相关专业的童鞋一看名字就知道他的功能:记录指令的地址,具体到这里就是JVM字节码指令的位置。这个对于多线程的来说是非常重要的,由于JVM多线程的的特点(通过线程轮流分配处理器运行时间实现),同一时刻每个处理器只能处理一条指令;因此线程在挂起后恢复到之前执行的正确位置需要一个独立的PCR,这块区域显然是每个线程独享的。
此外如果JVM执行的是Native方法的话,这个计数器的值是空,这个区域也是唯一一个在JVM中没有规定任何OutOfMemoryError的区域。
1.1.2 VM Stack(VMS)
虚拟机栈同PCR一样是县城私有的,他的生命周期跟线程相同。Java每个方法在执行的时候会同时创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法从开始调用到执行完成,对应着一个栈帧在虚拟机栈中的入栈与出栈的过程。
虚拟机栈中的局部变量表其实就是大家常说的“栈”的主要部分,这里面存储了编译期可知的各种基本数据类型、对象的引用、和ReturnAddress类型。
64位的double与long类型会占用2个局部变量空间,其余的数据类型占用一个,这个也许大家在面试题里面见到过问占用空间的问题。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时需要在栈帧中分配多大的局部变量空间是确定的,也就是在之后的运行中是不会改变局部变量表的大小的。
在JVM中,这个区域有两种异常情况:
1)线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError。
2)如果虚拟栈可动态扩展,扩展时无法申请到足够的内存,会抛出OutOfMemoryError。
关于以上异常的解决办法这里不给出,下一篇介绍内存溢出的文章会给出。
1.1.3 Native Method Stack(NMS)
NMS即本地方法栈,与虚拟机栈的作用比较类似,从名字也可以看出他是给 native方法提供服务的,这也是我们采用英文来做标题的目的。抛出的异常与VMS也是一样的。
1.1.4 Heap
Heap也就是堆,Java堆是被所有线程共享的一块内存区域,也是JVM管理的最大的内存区域,这个内存区域在JVM启动时创建,唯一的目的就是存储对象的实例,几乎所有的对象实例都在这里分配内存,数组也在这里分配。
Java Heap是GC管理的主要区域,有时候也被称为GCHeap即GC堆,由于很多收集器采用分代收集算法,Java对还可以细分为:新生代跟老年代;再细一点有Eden空间、From Survivor空间、To Survivor空间等。作为线程共享的Java堆,每个线程都有一个私有的分配缓冲区,Thread Local Allocation Buffer,TLAB。
关于堆的空间,只要是逻辑上连续的就行,物理上连续与否没有影响。在实现上,可以是大小固定的区域,也可以扩展内存,这个通过给JVM设置参数就可以实现 (-Xmx和-Xms)。如果没有内存可以完成实例的分配并且无法扩展大小时会抛出OutOfMemoryError。
1.1.5 Method Area(MA)
MA,方法区与Java堆一样,是线程共享的内存区,方法区主要用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JVM把方法区作为堆的一个逻辑部分,另外还有一个别名叫Non-Heap,目的是区分下方法区与堆。
在HotSpot虚拟机上,很多开发者习惯把方法区称为“永久代”,本质商量着并不是完全等价的,这么称主要是在HotSpotJVM上,使用永久代来实现方法区,这么做的目的是为了使GC可以像管理Java堆一样来管理方法区。另外,这个区域可以选择不实现垃圾回收,这个区域很少出现垃圾回收行为。这个区域的内存回收主要是针对常量池的回收和类型的卸载,显然这是很难操作的,尤其是类型的卸载要求很苛刻,因此回收虽然必要,但是容易导致内存泄漏。
1.1.6 Runtime Constant Pool(RCP)
RCP,运行时常量池,是方法区的一部分,在类加载后,用于存储编译期生成的各种字面量跟符号引用。
JVM对于Class文件的每一部分都有严格的规定,每一个字节存储什么样的数据都需要 符合规范与要求才能被JVM认可、装载、执行,对于RCP并没有细节的要求。除了符号引用,RCP还会存储Class文件翻译出来的直接引用。
运行时常量池相对于Class文件常量池的另外一个重要的特征是具备动态性,Java并不要求常量只有编译期才能产生,就是说并非只有预先置入Class文件的常量池中的内容才能进入RCP,这个从名字中的runtime也可以看出。
1.2 直接内存
有部分内存,不是JVM RDA(运行时数据区)的一部分,也不是JVM规范中定义的区域但是频繁的使用被称为直接内存。
在JDK1.4之后引入了New Input/Output类简称NIO,引入了一种基于通道与缓冲区的I/O方式,他可以通过Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样可以显著提高数据读取的效率。
参考文献
[1]深入理解Java虚拟机:JVM高级特性与最佳实践 周志明 机械工业出版社