本文默认基于64位的JDK 8(其虚拟机实现是HotSpot),除非特别说明。
一、前言
1.1、进程使用内存上限
进程所能使用内存上限受到硬件和操作系统限制,比如32位Windows操作系统下进程所能使用内存上限为2G。
1.2、虚拟机进程
我们平时说的Java进程其实是虚拟机进程——JVM进程。通过执行java
命令触发运行JVM进程,比如java com.dslztx.Main
和java -version
等。
1.3、JVM进程内存管理
如“1.1、进程使用内存上限”所述,JVM进程使用内存上限受到硬件和操作系统限制。根据虚拟机规范,JVM进程内存管理采取内存分类、内存动态分配、垃圾收集等技术。采用该种内存管理机制的优点是内存管理更加方便简单,缺点是一旦出现内存泄漏和溢出等问题,较难排查问题。
二、JVM进程内存管理之内存分类
分配给进程使用的一块内存区域被称为Native Memory
(有时候也被称为Native Heap
),如“1.1、进程使用内存上限”所述,Native Memory
上限受到硬件和操作系统限制。
Native Memory
如何被使用由进程自身决定,虚拟机规范人为对JVM进程使用的Native Memory
进行了划分,虽然其实底层都使用了相同的方式操作和管理内存,比如都调用C/C++语言的malloc/free
函数。JVM进程内存管理对所使用的Native Memory
的具体分类为:程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,显式调用JNI方法直接分配内存,JVM进程运行自身所需内存。
关于上述内存分类有两点说明:
- 基于是否由业务线程共享(这个共享是狭义的,基本可认为只有“Java堆”和“方法区”才算是线程共享的)可对上述内存分类进行再划分:
- 线程私有:程序计数器、Java虚拟机栈、本地方法栈
- 线程共享:Java堆、方法区
- 其他类型:JVM进程运行自身所需内存、显式调用JNI方法直接分配内存(深究起来,该部分内存也是广义线程共享的,但这里只讨论狭义线程共享)
- 虽然底层都使用C/C++语言的内存分配/回收函数,比如“malloc/free”,但是除了“显式调用JNI方法直接分配内存”之外的内存分类,内存操作没有JNI方法调用中间层
上述具体分类示例如图1。
图1
结合“作用”、“使用过程中可能遇到问题”和“其他描述”等信息,对内存分类进行深入介绍,如表1。
表1
内存分类 | 英文名称 | 线程私有/线程共享/其他类型 | 作用 | 使用过程中可能遇到问题 | 其他描述 |
---|---|---|---|---|---|
程序计数器 | Program Counter Register | 线程私有 | 执行Java方法时,存储线程当前执行字节码指令地址;执行本地方法(Native Method)时,存储空值 | 理论上不会抛出任何异常 | 内存空间小 |
Java虚拟机栈 | Java Virtual Machine Stack | 线程私有 | 存储Java方法执行时创建的栈帧 | 可能抛出StackOverflowError 和OutOfMemoryError 异常 |
一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用生成栈帧,结束释放栈帧 |
本地方法栈 | Native Method Stack | 线程私有 | 存储本地方法(Native Method)执行时创建的栈帧 | 可能抛出StackOverflowError 和OutOfMemoryError 异常 |
本地方法栈跟Java虚拟机栈非常类似,只是:一个针对Java方法,另外一个针对本地方法(Native Method) |
Java堆 | Java Heap | 线程共享 | 存储几乎所有的对象实例 | 可能抛出OutOfMemoryError 异常 |
内存空间较大 |
方法区 | Method Area | 线程共享 | 存储已被虚拟机加载的类信息,即时编译器编译以后的代码等数据 | 可能抛出OutOfMemoryError 异常 |
JDK 6使用“永久代”实现方法区,JDK 8使用“元空间”实现方法区:实现策略升级解决一些已知存在问题,具体原因没有必要深究[4] |
JVM进程运行自身所需内存 | / | 其他类型 | JVM进程运行自身所需 | 理论上不会抛出任何异常 | / |
显式调用JNI方法直接分配内存 | / | 其他类型 | 直接使用Native Memory | 可能抛出OutOfMemoryError 异常 |
有两类: 1)JDK提供的JNI API,比如 sun.misc.Unsafe 类下的public native long allocateMemory(long var1) 方法;2)自己实现的JNI方法 使用过程中需要显式声明和释放内存,否则会有内存泄漏。 有必要再介绍一个特殊的内存区域——直接内存,其英文名为Direct Memory,它是NIO引入的新机制,本质是调用 sun.misc.Unsafe.allocateMemory JNI API而直接使用Native Memory,再作了一定封装,被特别命名为直接内存 |
三、内存溢出异常实验
3.1、Java虚拟机栈和本地方法栈OutOfMemoryError
和StackOverflowError
异常
3.1.1、测试代码
测试OutOfMemoryError
异常代码如下:
1 | /** |
测试StackOverflowError
异常代码如下:
1 | /** |
3.1.2、执行
测试OutOfMemoryError
异常执行命令如下:
1 | javac JavaVMStackOOM.java |
执行结果:
1 | Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread |
测试StackOverflowError
异常执行命令如下:
1 | javac JavaVMStackSOF.java |
执行结果:
1 | stack length: 3167 |
3.1.3、说明
根据上述可知,一个Java线程最多可关联两个栈——Java虚拟机栈和本地方法栈,分别存储“Java方法执行时创建的栈帧”和“本地方法执行时创建的栈帧”,但是在HotSpot虚拟机实现中,不区分Java虚拟机栈和本地方法栈,故此时一个线程只会关联一个栈,该栈同时存储以上两种栈帧。
本文是基于HotSpot虚拟机实现进行说明的,故后续统一以“栈”作为称呼。
关于栈的StackOverflowError
和OutOfMemoryError
异常作几点说明如下:
- 栈的大小通过“-Xss”虚拟机参数设定
- 栈是线程私有的,因此当N个线程申请栈时,就需要
N *(Xss虚拟机参数设定值)
内存,如果内存不足,则会抛出OutOfMemoryError
异常 - 假定一个栈帧大小为S,当一个栈内需要分配K个栈帧(栈帧数量只跟方法调用深度有关)时,如果
S * K > Xss虚拟机参数设定值
,则抛出StackOverflowError
异常
3.2、Java堆OutOfMemoryError
异常
3.2.1、测试代码
1 | import java.util.ArrayList; |
3.2.2、执行
执行命令:
1 | javac HeapOOM.java |
执行结果:
1 | java.lang.OutOfMemoryError: Java heap space |
3.2.3、说明
通过“-Xms20m”和“-Xmx20m”虚拟机参数设定Java堆大小为固定的20MB,避免自动扩展;通过“-XX:+HeapDumpOnOutOfMemoryError”虚拟机参数设定使用Java堆出现OutOfMemoryError
异常时,自动Dump出堆转储快照,以便事后分析。
Java堆的OutOfMemoryError
异常后面还跟有提示信息:Java heap space,表明异常发生于使用“Java堆”的过程中。
3.3、方法区OutOfMemoryError
异常
3.3.1、测试代码
1 | import net.sf.cglib.proxy.Enhancer; |
3.3.2、执行命令
执行命令:
1 | javac -cp ".:./cglib-3.2.4.jar:./asm-5.1.jar" JavaMethodAreaOOM.java |
执行结果:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Metaspace |
3.3.3、说明
由以上描述可知,方法区存储已被虚拟机加载的类信息,即时编译器编译以后的代码等数据,通过CGLIB库提供的字节码操作技术,我们可以在运行时动态生成大量的新类,从而使得方法区的内存不够分配,最终导致抛出OutOfMemoryError
异常。
JDK 8使用“元空间”实现方法区,可通过JVM参数“-XX:MaxMetaspaceSize”配置元空间的最大值。以上例子中,抛出的OutOfMemoryError
异常后面还跟有提示信息:Metaspace,表明异常发生于使用“元空间”的过程中。
3.4、直接内存OutOfMemoryError
异常
3.4.1、测试代码
1 | import java.nio.ByteBuffer; |
3.4.2、执行
执行命令:
1 | javac DirectMemoryOOM.java |
执行结果:
1 | Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory |
3.4.3、说明
可通过JVM参数“-XX:MaxDirectMemory”配置直接内存的最大值,以上例子中,抛出的OutOfMemoryError
异常后面还跟有提示信息:Direct buffer memory,表明异常发生于使用“直接内存”的过程中。
参考文献
[1]http://www.ibm.com/developerworks/library/j-nativememory-linux/
[2]https://geosmart.github.io/2016/03/07/JVM%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E4%B8%8E%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E5%BC%82%E5%B8%B8/
[3]http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html
[4]https://openjdk.java.net/jeps/122