Java虚拟机-Java内存区域&内存溢出异常

Java的HotSpot虚拟机是如何管理内存的呢?对于c++程序员来说他们是“The King of The Memory”,然而对于Java开发者来说他们全然不知。这一部分就来探讨下Java虚拟机的各个区域,这些区域的作用、服务对象以及可能产生的问题。

运行时数据区域

一图以说明:

Java运行数据区域
Java运行数据区域

:该区域划分是由《Java虚拟机规范》规定。

程序计数器

可以看成当前线程所执行的字节码的行号指示器。是程序控制流的指示器:分支、循环、跳转、异常处理、线程恢复都需要这个指示器。
Java的多线程机制是通过线程轮流切换、分配处理器执行时间实现的,在任何一个时刻一个处理器都会执行一条线程中的指令。
因此为了线程切换到达需要的位置,每条线程都需要一个独立的程序计数机(在上面的图像中也能看到这一点)。我们把这种线程之间互不影响,独立存储的内存称为“线程私有”的内存。
> 注意:如果线程执行的是Java方法,计数器指的就是正在执行的虚拟机字节码指令地址。但是如果是本地方法1,这个计数器为空(Undefinde),这是惟一一个在虚拟机规范中没有规定任何OutOfMemoryError的区域。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型: 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用来存储方法的信息(局部变量表、操作数栈、动态连接、方法出口等)。
Java虚拟机栈也是线程私有的。在线程将方法从开始运行到完毕的时候,其实就是栈帧从入栈到出栈的过程。

局部变量表

在很多Java教程中都会把Java虚拟机的内存管理描述为堆区或者栈区2, 实际上远比这复杂的多。当然,这里的栈区就可以理解为局部变量表。 局部变量表存放了各种编译时可以知道Java虚拟机的基本数据类型3 、对象引用4 和returnAddress类型5
上述数据类型都是用局部变量槽表示,除了64位的long和double占有两个槽之外,其余变量均占有一个槽。
局部变量所需空间在编译期就已经完成分配,这里的空间大小都是用槽来衡量。

该内存区域规定了两类异常
  1. 如果线程申请的栈申请的深度大于虚拟机所容许的最大深度,报StackOverFlow错误。
  2. 如果栈扩展时无法申请到足够的内存,报OutOfMemory错误。6

本地方法栈

本地方法栈和Java虚拟机栈基本一样,只不过Java虚拟机栈服务Java方法,本地方法栈服务本地(native)方法,在HotSpot中甚至直接将两者合二为一了。
本地方法栈的错误类型和Java虚拟机栈一样。

Java堆

和前面的内存结构不同,Java堆是所有线程公有的,在虚拟机启动的时候创建,它也是虚拟机管理的内存中最大的一块。
Java堆的主要作用是存放对象实例7 。因此Java堆也变成了垃圾回收机制所管理的内存区域8
> 注意:现在的垃圾回收机制主要是基于分代理论设计,因此在关于Java堆的资料中常常出现“新生代”、“老年代”、“永久代”、“Eden空间”、“From Survivor空间”、“To Survivor空间”等名词。不过这些只是垃圾回收的设计风格,不是Java虚拟机规范对Java虚拟机的划分,且现在HotSpot中也存在着不使用分代理论的垃圾回收器。

Java堆的空间分配方式

在空间分配上,Java堆可以在物理上不连续,但是在逻辑上要连续。对于大对象,多数虚拟机也要求了连续的内存空间。

Java堆可能出现的异常

Java堆既可以是固定大小,也可以是可扩展的(当前主流Java虚拟机均是可扩展,通过-Xmx和-Xms确定)。如果在Java堆中空间不足,无法完成实例分配,或者堆无法扩展,会抛出OutOfMemoryError异常。

方法区

方法区和Java堆类似,在《Java虚拟机规范》中将其描述为Java堆的一个逻辑部分,当然其职责是不同的,所以方法区的小名叫做“非堆”,目的就是和Java堆区分。
方法区本身和Java堆一样,也是所有线程公有的。

作用

方法区主要存储已被虚拟机加载的类型信息常量静态变量、即时编译器编译后的代码缓存等数据。 > 虽然叫方法区,但可能叫永久代9更贴切,虽然这样叫是错误的。

对方法区的管理

方法区是约束非常宽松的区域。和Java堆一样,它的内存分配也不需要连续,也是可以选择固定区域或者可扩展。另外,方法区还可以选择不实现垃圾收集。
方法区主要针对常量池和的回收和类型的卸载,当然由于条件非常苛刻(尤其是类型的卸载),所以别指望该区域能够节省多大的空间。但是这并不表明对该区域进行回收是没有必要的:以前Sun公司公布的Bug列表中,若干严重Bug就是因为HotSpot对该区域未完全回收造成的内存泄漏。

方法区可能出现的错误

当方法区无法满足新的内存分配需求时,报OutOfMemoryError异常。

运行时常量池

运行时常量池是方法区的一部分,所以在图中并没有标注出来。运行时常量池主要在类加载后存放其常量池表,该表存在于Class文件中10,其重要用途是存放编译器生成的各种字面量和符号引用。

特点

运行时常量池有着其他内存区域所不具有的特点:

  1. 《Java虚拟机规范》没有对该区域做任何细节要求,Java虚拟机对Class文件的每一部分包括常量池的格式都有严格规定11。不过一般说来运行时常量池还是保存Class文件中描述的符号引用和由符号引用翻译出来的直接引用。
  2. 具有动态性,Java语言并不要求常量一定在编译期生成,运行期间也可以将新的常量放入池中,常用的Sting的 intern() 方法便是使用该特性的例子。
常量池可能出现的错误

既然是方法区的一部分,其错误自然和方法区一样,当无法分配新的空间时,虚拟机挥发出OutOfMemoryError的异常。

直接内存

首先要说明的是,直接内存并不在《Java虚拟机规范》中定义,在图中我们也没有看到直接内存。
这一部分内存的出现主要是由于从JDK1.4开始加入了NIO(New Input Output)类。这个类可以使用Native数据库直接分配堆外内存,然后通过Java堆中的一个DirectByBuffer对象作为这块内存的引用直接进行操作。
这样做的目的是为了避免在Java堆和Native数据中来回地粘贴数据,极大的提高了性能。

直接内存出现的错误

直接内存不受Java堆大小的限制,但是仍然会收到机器总的内存和处理器寻址空间的限制。所以当各个内存的和大于物理内存限制的时候(其他区域有-Xmx限制,直接内存不受限制),会抛出OutOfMemoryError异常。

思维导图

总结一下Java内存区域的总体情况 思维导图


  1. native关键字定义的方法,非Java语言编写,主要是调用由C或者C++编译生成的高效dll或者so库

  2. 这种说法实际上来自于C和C++

  3. boolean、byte、char、short、int、float、long、double

  4. reference类型,一个对象起始地址的指针或者代表对象的句柄(老虚拟机使用句柄)

  5. 指向了一条字节码指令地址,用于函数返回

  6. HotSpot不会动态申请栈容量,只有当申请时就失败才会报该错误。而老旧的Classic虚拟机使用动态扩展

  7. 《Java虚拟机规范》中描述为:“所有对象实例以及数组都应该在堆上分配。”

  8. 在一些资料中也被称为GC堆(garbage collected heap) 不过千万别翻译成垃圾堆

  9. 和Java堆一样,分代设计只是一种设计风格,之所以称之为永久代是由于HotSpot使用收集器来管理方法堆而节省代码,但是由于这种方式存在bug,所以到了Java8已经完全废弃了这种做法

  10. Class文件还存放有类的版本、字段、方法、接口等描述信息

  11. 这里先说明下动态常量池和静态常量池不是一个东西。JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。静态常量池用于存放编译期生成的各种字面量和符号引用,而当类加载到内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中。而字符串常量池存的是引用值,其存在于运行时常量池之中。