Java内存区域与内存溢出异常

Java内存区域与内存溢出异常

概述

对于Java而言,垃圾回收技术和内存动态分配是它的一大特点,本文将介绍Java虚拟机内存的各个区域。

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它管理的内存划分为多个不同的数据区域——方法区(Method kArea)、堆(Heap)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stacj)、程序计数器(Program Counter Register)。前面两个是所有线程共享的,后者则是线程独立的。

程序计数器

每个线程都有自己的程序计数器,它可以看做是当前线程执行的字节码的行号指示器,通过改变这个计数器的值就可以选取下一条需要执行的字节码指令。

如果正在执行的是Native方法,这个计数器就是undefined。此内存区域没有规定任何outOfMemoryError情况。

Java虚拟机栈

同样是线程私有的,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法入口等。

对于局部变量而言,它存放着各种编译器可知的基本数据类型、对象引用和returnAddress类型。其中64位的long和double会占据2个局部变量空间。

如果线程请求的栈深度大于虚拟机允许的深度,会抛出stackOverflow异常;如果虚拟机动态扩展时无法申请到足够的内存,则会抛出outOfMemoryError。

本地方法栈

这个数据区的作用与虚拟机栈类似,只不过本地方法栈是为Native方法服务的。但由于虚拟机规范对此没有强烈的限制,因此例如Sun的HotSpot虚拟机直接把两个方法去合二为一。

Java堆

对于多数应用,heap是Java虚拟机管理内存中最大的一块,这是被所有线程共享的一块内存区域,存放的就是对象实例

根据虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,当前主流的虚拟机都是按照可扩展来实现的——通过-Xmx和-Xms控制。

方法区

方法区同样是线程共享的,存放的内容包括已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虚拟机规范在对这个区域的限制比较宽松,除了跟堆一样可以处于物理上不连续的内存空间中,还允许不实现垃圾回收。

  • 运行时常量池

这是方法区的一部分,在Class文件中,除了有类的版本字段方法接口这些信息之外,还有一项信息是常量区。

运行时常量池还有另外一个重要特征——动态性。这个特性被利用的比较多的就是String#intern()方法。

直接内存

在JDK1.4中新引入了NIO类,引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存放在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

HotSpot虚拟机对象探秘

对象的创建

在Java的运行过程中,无时无刻都有对象被创建出来。那么在虚拟机中,这是一个怎么样的过程呢?

当虚拟机遇到一个new指令时:

  • 去常量池中检查是否能够定位到这个类的符号引用,并且检查这个类是否已被加载、解析和初始化过;
  • 在类加载检查通过后会去分配内存,这里有两种情况:
    • Java堆中内存规整,就会把所有用过的内存放一边,空闲的在另一边,中间使用指针作为分界点的指示器。每次分配内存的时候就移动指针——Bump the Pointer;
    • 非规整内存,则是维护一个空闲列表,记录哪些内存块可用;

Java堆是否规整与采用的垃圾回收器有关,CMS这种基于Mark-Sweep算法的收集器则是采用空闲列表。

另外,为了解决线程安全的问题,有两种解决方法:一是采用CAS配上失败重试的方式保证更新内存操作的原子性;二是将内存分配的操作按照线程划分在不同的空间中,则提取为线程分配缓存Thread Local Allocation Buffer——TLAB;可以通过-XX:+/-UseTLAB参数进行设定

  • 分配完内存后,会将内存空间中除对象头外都初始化为零值;
  • 接下来,虚拟机对对象进行必要的设置,将一些元数据信息,对象的哈希码,GC分代年龄等记录在Object Header中。
  • 最后,会执行init方法,进行对象初始化。

对象的内存布局

在HotSpot虚拟机中,对象的内存布局可以划分为三块区域——对象头、实例数据和对齐填充。

  1. 对象头

对象头包含两部分信息,一是存储对象自身的运行时数据,如对象的哈希码,GC分代年龄等,并且这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,即Mark Word。例如Mark Word的32bit空间中25bit存储对象哈希码,4bit存储对象分代年龄,2bit存储锁标志位,1bit固定为0;

二是类型指针,即对象指向它的类元数据的指针,这样就可以通过这个指针确定对象是哪个类的实例。

  1. 实例数据

这就是程序代码中定义的字段内容,包括从父类继承下来的。这部分的存储策略收到虚拟机分配策略参数和字段类型在Java源码中定义的顺序影响。

  1. 对齐填充

不是必然存在的,若对象实例数据部分不是8字节的整数倍则,需要对其填充进行补全。

对象的访问定位

创建对象是为了使用对象,Java程序是通过栈的reference数据来操作堆上对象的,因此这里有两种访问方式:

  • 通过句柄访问,Java堆会划分句柄池,reference存放的就是句柄的地址。这个方法的好处就是句柄地址稳定,对象被移动只会改变句柄的数据,reference本身不会被改变;
  • 直接访问,reference存放的是对象地址。这个方法的好处就是访问速度更快,这也是HotSpot采用的方式;