Java内存区域
JVM系列随笔主要是对《深入理解Java虚拟机:JVM高级特性与最佳实践 第2版》的学习总结
概述
Java虚拟机自动内存管理机制,能够让程序员不必为每个对象new/delete,不容易出现内存泄露和内存溢出。
运行时数据区域
根据Java虚拟机规范,运行时数据区域如下图所示:
程序计数器
- 当前线程锁执行的字节码的行号指示器,用来指示下一条字节码指令。
- 线程私有。
- 唯一一个Java虚拟机规范没有规定任何
OutOfMemoryError
的区域
Java虚拟机栈
- 描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等。局部变量表存放了编译器可知的各种基础数据类型、对象引用和returnAddress类型。当进入一个方法时,方法所需帧中分配的局部变量空间完全确定。
- 线程私有。
- 虚拟机规范定义了两种异常:
StackOverFlow
-线程请求深度大于允许值;OutOfMemoryError
-无法申请到更多内存
本地方法栈
- 与虚拟机栈类似,区别是虚拟机栈执行Java方法,而本地方法栈执行Native方法。
- 线程私有
- 抛出
StackOverFlow
和OutOfMemoryError
两种异常
Java堆
- Java堆是JVM管理内存中最大的一块,几乎所有的对象都在这里分配。是垃圾回收管理的主要区域,也被称为GC堆。可以处于物理上不连续的内存空间,只要逻辑上连续即可。
- 线程共享
- 抛出
OutOfMemoryError
方法区
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JVM规范描述为堆的一个逻辑部分,别名Non-Heap。习惯Hotspot上叫做永久带,原因是设计团队把GC分带收集扩展至方法区,使用永久带来实现罢了。
- 运行时常量池是方法区的一部分,用于存放Class文件编译期生成的各种字面量和符号引用。常量池具有动态性,可以运行时期间将新的常量放入池中。用于存放JDK1.7中把字符串常量池移除。
- 线程共享
- 抛出
OutOfMemoryError
直接内存
- 直接内存不是JVM运行时数据区的一部分,但是也常被使用,例如NIO的
DirectByteBuffer
操作方式。也可导致OutOfMemoryError
。
HotSpot对象
对象的创建
在语言层面,创建对象仅需要一个new
关键字。
虚拟机层面,当遇到一个new
时:
- 首先检查常量池中能否定位到一个符号引用。符号引用所代表的类如果没有被加载,需要先加载、解析、初始化改类。
- 然后,类加载同构后,在堆上为新对象分配内存。内存分配又分为“指针碰撞”和“空闲列表”两种方式,取决于Java堆是否规整,而Java堆是否规整又取决于垃圾回收器是否带有压缩整理功能。
- 然后,将分配的内存空间初始化为零值(不包括对象头)。
- 接着,对这个对象进行设置,比如是哪个类的实例,如何找到类元数据、对象哈希码、GC分代年龄信息等。这些对象信息放在对象头中。
- 最后,一般情况下都会执行
方法,按照程序员的意愿进行初始化。至此,虚拟机层面对象创建完成。
对象的内存分布
HotSpot中,对象的内存中存储布局可以划分为3块区域:对象头,实例数据和对齐填充。
-
对象头
对象头包含两部分数据:
-
自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,也称为Mark Word。
-
类型指针,即对象指向它的元数据的指针,JVM通过这个指针确定这个对象是哪个类的实例。
-
-
实例数据
也即程序代码中定义的各种类型的字段内容,包括父类继承下来的和子类中定义的。
-
对齐填充
不是必然存在的,也没有特别含义,仅起占位符作用。由于HotSpot VM自动内存管理要求对象起始地址必须是8字节的倍数。因此当对象数据没对齐时,需要填充补全。
对象的访问定位
Java程序通过栈上的reference数据来操作堆上的具体对象。目前主流reference定位对象的方式包括以下两种:
-
句柄方式
Java堆中会划分出来一块内存作为句柄池。reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体地址。
-
直接指针访问
Java堆对象中放置访问类型数据的相关信息,而reference存储的就是对象地址。
直接指针访问方式的好处是速度更快,比句柄访问方式减少了一次指针定位时间开销。Sun HotSpot使用的这种方式
OutOfMemoryError异常
通过手动产生溢出的方式,加深对运行时数据区的理解
Java堆溢出
Java堆用于存放实例对象,因此制造溢出的思路是限制堆大小的情况下,不断生成对象进行填充,直至溢出。设置参数-Xms
最小值,-Xmx
最大值,当两个值相同时表示不可扩张。
另外,通过设置-XX:+HeapDumpOnOutOfMemoryError
可以让JVM在内存溢出时Dump出当前的内存堆转储快照便于事后分析。
/**
* -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
static class OOMObject {
}
}
运行几秒种后输出下面的结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid8328.hprof ...
Heap dump file created [2474990 bytes in 0.009 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
...
虚拟机栈和本地方法栈溢出
栈中以线程为单位,存放的方法调用的各类数据。HotSpot中并不区分虚拟机栈和本地方法栈,因此虽然存在设置本地方法栈的参数-Xoss
,但是实际上无效。栈容量只由参数-Xss
设定。JVM规范中定义了两种异常:
- 如果线程请求的栈深度大于JVM允许的最大深度,抛出
StackOverFlowError
异常 - 如果JVM在扩展栈空间时无法申请到足够的空间,抛出
OutOfMemoryError
异常
首先测试StackOverFlowError
,代码如下:
/**
* -Xss128k
*/
public class VmSOF {
int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
VmSOF sof = new VmSOF();
try {
sof.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + sof.stackLength);
throw e;
}
}
}
输出:
stack length:981
Exception in thread "main" java.lang.StackOverflowError
at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:11)
at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
at edu.uestc.l08.VmSOF.stackLeak(VmSOF.java:12)
...
上述代码只会抛出StackOverFlowError
异常,而调整-Xss
参数变大变小只影响方法栈调用深度变多变少,而并不能产生出OutOfMemoryError
。
通过分析,运行时区域中虚拟机栈和本地方法栈是线程隔离的,当栈空间一定时,支持的线程数量是一定的。因此OutOfMemoryError
可以通过不断生成线程来制造出来。
测试OutOfMemoryError
的代码:
/**
* -Xss128k
*/
public class VmsOOM {
public static void main(String[] args) {
while (true) {
Thread t = new Thread(new Unstoppable());
t.start();
}
}
static class Unstoppable implements Runnable {
@Override
public void run() {
while (true);
}
}
}
结果如下
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Unknown Source)
at edu.uestc.l08.VmsOOM.main(VmsOOM.java:15)
注:上述代码容易造成系统假死,需要慎重测试
方法区溢出
方法区用于存放Class的类型元数据,测试的思路是在限定方法区大小的情况下,产生大量的类去填充直至溢出。
本机测试使用JDK1.8,此版本不在使用永久带来实现方法区,有运行提示为证:
Java HotSpot(TM) Client VM warning: ignoring option PermSize=10m; support was removed in 8.0
Java HotSpot(TM) Client VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0
JDK1.8后使用称为元空间的MetaSpace来实现,因此JVM启动参数设置为-XX:MaxMetaspaceSize=10m -XX:MaxMetaspaceSize=10m
,测试使用CGLib
填充方法区,代码如下:
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* -XX:MaxMetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
输出为:
Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at edu.uestc.l08.MethodAreaOOM.main(MethodAreaOOM.java:22)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:413)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
... 11 more
另外,由于运行时常量池作为方法区中特殊的一块,也有存在OOM的可能。在JDK1.6及之前的版本中,通常使用String.intern()
的例子来制造溢出。主要代码为:
while (true) {
list.add(String.valueOf(i++).intern());
}
在JDK1.7之后,intern()
方法被修改,不在复制实例,而是在常量首次出现时仅保留堆中对象的引用,从而上例比较难制造溢出。具体不在赘述,详情可参考:http://blog.csdn.net/seu_calvin/article/details/52291082
本机直接内存溢出
直接内存通过-XX:MaxDirectMemorySize
指定,如果不指定则与堆大小相同。周总的例子如下:
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
* -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final long _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
输出结果如下:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at edu.uestc.l08.DirectMemoryOOM.main(DirectMemoryOOM.java:17)
由本地内存导致的内存溢出有一个特征,就在Heap Dump中不会看到明显的异常。如果OOM后dump文件很小,而程序使用了NIO,则可以考虑是这方面原因。