JVM内存区域(JVM内存结构,JVM内存布局)是Java虚拟机运行时数据的位置划分,其主要包括:程序计数器、虚拟机栈、本地方法栈、堆、方法区。

后面将介绍Java内存模型,JVM内存区域和Java内存模型是不同层次对内存的划分,JVM内存区域侧重描述内存数据的结构布局,一般关注垃圾回收(GC)及OOM问题。

一、前言

Java虚拟机将内存划分为几个区域:程序计数器、虚拟机栈、本地方法栈、堆、方法区。其中程序计数器、虚拟机栈、本地方法栈都是线程私有的区域,而堆、方法区都是线程共享的区域。

除了以上的内存区域,Java虚拟机还可以使用“直接内存”,Java8中使用元空间替换了Java7位于方法区的永久代,元空间就是在“直接内存”中分配。

本文中JVM的实现都是以HotSpot为例,如:永久代就是HotSpot的专属概念。

二、JVM内存区域的数据内容

接下来将分别介绍这几个区域的部分数据内容:

  • 程序计数器(Program Counter Register):程序计数器保存了当前程序所处的位置,通过改变程序计数器的值可以实现流程控制(分支、循环、跳转、异常处理、线程恢复等)。

  • 虚拟机栈(VM Stack):虚拟机栈保存了方法出口、局部变量表等数据,局部变量表又保存了对象引用等数据,如对于Object obj = new Object()来说,obj这个变量就作为Object对象的引用就存放在局部变量表中。

  • 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈功能类似,其区别是虚拟机栈适用于Java方法,而本地方法栈适用于Native方法。

  • 堆(Heap):堆保存了对象实例等数据,如对于之前虚拟机栈描述中的Object obj = new Object()来说,new出来的Object对象的实例数据就存放在堆中。Java堆是垃圾回收(GC)的重点关注位置,常说的“分代收集”(老年代,新生代)也大部分发生在堆中(这里之所以说是大部分是因为永久代是在方法区中实现)。

  • 方法区(Method Area):方法区保存了类型信息、常量、静态变量等数据,运行时常量池是方法区的一部分。Java7在方法区上实现了“永久代”,Java8后就没有“永久代”这个概念了,而是使用“元空间”替代之。

三、JVM内存区域的参数配置

我们可以在执行Java程序之前指定参数来改变JVM内存区域的大小及行为,常用的参数如下:

  • -Xmnsize:设置堆的新生代(nursery)的最大空间与初始空间,如:java -Xmn256m Hello。它等价于-XX:NewSize
  • -Xmssize:设置堆的最小空间与初始空间,如:java -Xms6m Hello。它部分等价于-XX:InitalHeapSize(设置堆的初始空间)。
  • -Xmxsize:设置堆的最大空间,如:java -Xmx80m Hello。它等价于-XX:MaxHeapSizeXmsXmx一般设置成相同的值。
  • -Xsssize:设置线程栈的空间,如:java -Xss1m Hello。它等价于-XX:ThreadStackSize

四、JVM内存区域的常见异常

当Java程序无法申请到足够的内存时,便会抛出内存溢出异常(OutofMemoryError,OOM)。OOM可以发生在Java虚拟机栈、本地方法栈、Java堆及方法区,换言之,除了程序计数器的其它所有JVM内存区域都可能发生OOM异常。

当Java程序请求的栈深度大于虚拟机允许的深度,便会抛出栈溢出异常(StackOverflowError)。根据描述可以知道,栈溢出异常一般只会发生在Java虚拟机栈和本地方法栈。

各JVM内存区域可能会遇到异常的场景如下所示:

场景异常原因
无节制地创建对象堆的内存溢出异常对象在堆中分配空间,对象过多可能会导致堆空间不足
递归调用,并且永不退出栈的栈溢出异常方法调用会执行入栈操作,递归调用而不退出会无限入栈,导致栈深度过大
无节制地定义大量局部变量栈的内存溢出异常局部变量在局部变量表中分配空间,局部变量表位于栈,局部变量过多可能会导致栈空间不足
无节制地创建线程栈的内存溢出异常创建线程所需的栈空间固定,线程越多,所需的栈空间也就越大,线程过多可能会导致栈空间不足
无节制地分配直接内存直接内存的内存溢出可使用的直接内存为物理总内存减去虚拟机分配的内存,直接内存可能会被用尽

参考文档

  1. 《深入理解Java虚拟机第3版 周志明 著》