摘要: JVM内存的划分,导致内存溢出异常的可能区域。
1. JVM运行时内存区域
JVM在执行Java程序的过程中会把它所管理的内存划分为以下几个区域:
1.1 程序计数器
程序计数器是一块较小的内存空间,在Java虚拟机规范中是唯一一个未规定OutOfMemoryError的内存区域。
程序计数器可看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变计数器的值来选取下一条待执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。
每个线程均有一个独立的程序计数器。若线程执行的是一个Java方法,计数器记录的值是正在执行虚拟机字节码地址;若执行的是Native方法,计数器值为空(Undefined)。
1.2 Java虚拟机栈
Java虚拟机栈也就是我们经常说的"栈"(其实这个说法不够严谨<^_^>),它是线程私有的,生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法执行时都会创建一个栈帧,用于存储局部变量栈、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,都对应者一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的8种基本数据类型、对象引用(reference类型,指向对象起始地址的引用指针或代表对象的句柄或其他与此对象相关的位置)、returnAddress类型(指向一条字节码指令的地址)。
Java虚拟机规范对这块区域规定了两种异常情况:
- StackOverflowError
线程请求的栈深度大于虚拟机所允许的深度,将抛出此异常;
- OutOfMemoryError
若虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存,将抛出此异常;
1.3 本地方法栈
与虚拟机栈类似,但它执行的Native方法服务。
1.4 Java堆
"GC"堆,虚拟机启动时创建,为所有线程共享的一块区域。在Java虚拟机规范描述中,所有对象实例和数组都要在堆上分配(随着JIT编译器的发展和逃逸技术的逐渐成熟,所有对象在堆上分配也变得不是这么绝对)。
如果堆中剩余内存不足以完成实例的分配,且无法再扩展,将抛出OutOfMemoryError。
1.5 方法区
"Non-Heap",存储已被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码等数据。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。
1.6 运行时常量池
方法区的一部分,在类加载后进入方法区时,存放编译期生成的各种字面量和符号引用。
在运行期间,也可能将新的常量放入池中,如String常用的intern()方法。
当无法满足内存分配需求时,将抛出OutOfMemoryError。
1.7 直接内存
这部分区域并不是虚拟机运行时数据区的一部分,也非Java虚拟机规范中定义的内存区域,但也被频繁使用,亦可能抛出OutOfMemoryError。
直接内存主要为NIO(JDK1.4 加入的类)使用,通过存储在Java堆中的DirectByteBuffer对象对这块内存的引用进行操作。在某些场合能显著提高性能,避免了Java堆和Native堆中来回复制数据。
2. HotSpot虚拟机对象
讨论Java堆中普通对象分配、布局和访问的过程。
2.1 对象的创建
- 虚拟机遇到一条new命令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,若没有,则先执行相应的类加载过程;
- 在类加载检查通过后,虚拟机将为新生对象分配内存:
- 指针碰撞
若Java堆中的内存是绝对规整的,所有用过的和空闲的内存都各放一边,中间放着一个指针作为分界点的指示器,那分配内存仅仅是将指针向空闲空间方向移动一段与对象大小相等的距离;
- 空闲列表
Java堆中的内存并不规整,此时虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表上找到一块足够大的空间划分给对象实例,并更新列表上的记录。
并发情况下,在堆上分配内存,可能出现内存分配的同步问题,解决方案有两个,一个就是同步内存(CAS)分配动作;另一个就是采用TLAB(本地线程分配缓冲),即在Java堆中针对每个线程先预先分配一小块内存。这样当线程需要分配内存时就在自己的TLAB上进行,从而避免同步的开销。但是当TLAB分配满重新分配TLAB时仍需要同步。在内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
- 虚拟机将要对对象进行必要的设置,如这个对象是哪个类的实例,如何找到类的元数据信息、对象的hashCode和GC分代年龄等信息。这些信息存放在对象的对象头中。
最后执行init方法,从而产生真正可用的对象。
2.2 对象的内存布局
在HotSpot虚拟机中,对象在内存的存储的布局可分为3块区域:对象头、实例数据、对齐填充。
- 对象头
- Mark Word
存储对象自身的运行时数据,如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
- 类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
- 实例数据
程序代码中所定义的各种类型的字段内容,包括从父类继承下来的。
- 对齐填充
并不是必然存在,仅仅起着占位符作用,以满足HotSpot VM 自动内存管理系统对对象起始地址必须是8字节的整数倍要求。
2.3 对象的访问定位
Java程序通过栈上的reference数据来操作堆上的具体对象。主流的访问方式:
* 句柄
Java堆中将划分一块内存用作句柄池,reference中存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自的具体地址信息。
优点:当对象移动时,只需要改变句柄的指针即可,而相应的reference则不需要做变动;
- 直接指针
reference存储的是对象地址,通过reference就能访问数据。
优点:访问速度快,相对句柄的方式而言少了一次指针定位的开销。HotSpot VM使用的是此种方式。
参考文献