参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
一、运行时数据区
1.1、程序计数器
程序计数器(Program Counter Register
)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java
虚拟机的概念模型里
,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java
虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“
线程私有
”
的内存。
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
1.2、Java虚拟机栈
虚拟机栈描述的是Java
方法执行的线程内存模型:每个方法被执行的时候,
Java
虚拟机都会同步创建一个栈帧(
Stack Frame
)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
其中局部变量表存放了编译期可知的各种Java
虚拟机基本数据类型(
boolean
、
byte
、
char
、
short
、
int
、float、
long
、
double
)、对象引用(
reference
类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
1.3、本地方法栈
本地方法栈则是为虚拟机使用到的本地(Native
)方法服务。
1.4、Java堆
对于Java
应用程序来说,
Java
堆(
Java Heap
)是虚拟机所管理的内存中最大的一块。
Java
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“
几乎
”
所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作
“GC
堆
”
(
Garbage Collected Heap)。
1.5、方法区
方法区(Method Area
)与
Java
堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java
虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“
非堆
”
(
Non-Heap
),目的是与
Java
堆区分开来。
1.6、运行时常量池
运行时常量池(Runtime Constant Pool
)是
方法区
的一部分。
Class
文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table
),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
1.7、直接内存
直接内存(Direct Memory
)并不是虚拟机运行时数据区的一部分,也不是《
Java
虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError
异常出现,所以我们放到这里一起讲解。
二、内存移除异常
2.1、Java堆溢出
Java堆用于储存对象实例,我们只要不断地创建对象,并且保证
GC Roots
到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
测试代码如下:
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
* 将堆的最小值-Xms参数与最大值-Xmx参数 设置为一样即可避免堆自动扩展
* 通过参数-XX:+HeapDumpOnOutOf-MemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
**/
public static void main(String[] args) {
List<MonitorVO> list = new ArrayList<MonitorVO>();
while (true) {
list.add(new MonitorVO());
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid15712.hprof ...
Heap dump file created [15662569 bytes in 0.092 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
处理方法:
首先通过内存映像分析工具(如Eclipse Memory Analyzer)对
Dump
出来的堆转储快照进行分析(如图)。第一步首先应确认内存中导致
OOM
的对象是否是必 要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak
)还是内存溢出(
Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots
的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots
相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots
引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java
虚拟机的堆参数(-Xmx
与
-Xms
)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

2.2、虚拟机栈和本地方法栈溢出
栈容量只能由-Xss
参数来设定。
关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError
异常。
2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
结论:
·
使用
-Xss
参数减少栈内存容量。
结果:抛出
StackOverflowError
异常,异常出现时输出的堆栈深度相应缩小。
·
定义了大量的本地变量,增大此方法帧中本地变量表的长度。
结果:抛出
StackOverflowError
异常,异常出现时输出的堆栈深度相应缩小。
2.3、方法区和运行时常量池溢出
JDK 8中完全使用元空间来代替永久代。
String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此
String
对象的字符串,则返回代表池中这个字符串的String
对象的引用;否则,会将此
String
对象包含的字符串添加到常量池中,并且返回此String
对象的引用。在
JDK 6
或更早之前的
HotSpot
虚拟机中,常量池都是分配在永久代中,我们可以通过-XX
:
PermSize
和
-XX
:
MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。
在JDK 7中继续使用-XX:MaxPermSize参数
在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
eg:
java.lang.OutOfMemoryError: PermGen space
2.4、本机直接内存溢出
直接内存(Direct Memory
)的容量大小可通过
-XX
:
MaxDirectMemorySize
参数来指定,如果不去指定,则默认与Java
堆最大值(由
-Xmx
指定)一致。