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

一、运行时区域数据

Java 虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

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

程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,即记录正在执行的虚拟机字节码指令的地址。如果正在执行的是本地方法则为空,因为该内存区域是唯一在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

其为虚拟机执行Java方法(也就是字节码)服务。。每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

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

可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:

java -Xss=512M HackTheJava

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 *Error 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈

与虚拟机栈相似,但其为虚拟机使用到的Native方法服务。

本地方法栈中方法使用的语言、使用方法与数据结构并没有强制规定,因此具体的虚拟机可以*的实现它。

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

Java堆

对大多数应用来说,Java堆是Java虚拟所管理的内存中最大的一块。此区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也被成为“GC”堆。

现代的垃圾收集器基本都是采用分代收集算法,主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把 Java 堆分成以下三块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)

当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。

新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:

  • Eden(伊甸园)
  • From Survivor(幸存者)
  • To Survivor

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

Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

java -Xms=1M -Xmx=2M HackTheJava

方法区

其用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据

和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。

运行时常量池

运行时常量池时方法区的一部分。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量符号引用

除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,又不是Java虚拟机规范中定义的内存区域。

在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

二、HotSpot虚拟机对象探秘

探讨HotSpot虚拟机在Java堆中的对象分配、布局和访问的全过程

对象的创建过程

以下过程仅限于普通Java对象,不包括数组和Class对象等

  • 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用的代表的类是否已被加载、解析和初始化过。如果有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,虚拟机将为新生对象分配内存。
  • 内存分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。
  • 虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
  • 执行new指令之后会接着执行方法,按程序员的意愿进行对象的初始化,这样真正可用的对象才算完全产生。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头分为两部分:
    • 第一部分用于存储对象自身的运行时数据,如哈希吗(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
    • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
    • 另外,如果对象是一个Java数组,那个对象头中还必须有一块用于记录数组长度的数据。
  • 实例数据:该部分是对象真正存储的有效信息,也是在程序代码中所要定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
  • 对齐填充:起占位符的作用。因为对象的大小必须是8字节的整数倍,而对象头的部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

Java程序通过栈上的reference数据来操作堆上的具体对象。主流的访问方式有使用句柄和直接指针方式。

  • 如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池, reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图2-2所示。

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

  • 如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关类型信息,而refrence中存储的直接就是对象地址,如图2-3所示。

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