Java内存区域与内存溢出异常
运行时数据区域
JVM执行java程序的时候有一个运行时数据区,每个区域有自己的作用,了解这些区域有助于我们理解JVM。JVM运行时数据区如图所示:
程序计数器
该区域是线程私有的,字节码解释器通过改变程序计数器来获取下一条指令来执行程序,每一个线程都有一个独立的程序计数器。在执行java方法的时候,计数器记录的是虚拟机字节码指令的地址,执行本地Native方法的时候,计数器的值为空,程序计数器是没有OutOfMemoryError的现象的。
Java虚拟机栈
虚拟机栈也是线程私有的,是Java方法执行的内存模型,方法执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的执行就是一个栈帧在虚拟机栈中入栈出栈的过程。
虚拟机栈有两种异常情况:
- 线程请求的栈深度大于JVM允许的深度,会抛出*Error异常
- 如果虚拟机可以动态扩展,那么扩展到无法申请足够的内存时,会抛出OutOfMemoryError异常
本地方法栈
和虚拟机栈差不多,一样有虚拟机栈的两种异常情况
Java堆
所有线程共享,内存管理中最大的一块,用于存放对象实例,几乎所有的对象实例都在这里分配,垃圾收集器管理的重点区域,当Java堆无法再扩展时,将抛出OutOfMemoryError异常。
方法区
所有线程共享,存储已经被虚拟机加载的类信息、常量、静态变量等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
直接内存
不是运行时数据区的一部分,但是值得注意一下,尤其是配置内存的时候。
HotSpot虚拟机对象
对象的创建
平常我们创建对象一般使用New这个关键字,实际上是一个很复杂的过程
- New指令发出来后,虚拟机会去检查这个指令参数能否在常量池找到一个类的符号引用
- 检查这个符号引用代表的类是否已被加载、解析、初始化、没有就执行相应的类加载过程
- 虚拟机为新生对象分配内存
- 对分配的内存空间初始化为零值
- 执行init方法,按照程序员的意愿进行初始化
分配内存的方式:
指针碰撞:内存规整,只需从空闲内存中分配一块
空闲列表:内存不是规整的,从空闲列表中找到足够大内存空间进行分配
PS:内存是后规整和垃圾收集器的方式有关
对象的内存布局
对象内存布局可以分为3块区域:对象头、实例数据、对齐填充
对象头分为两部分,一部分用于存储对象自身的运行时数据:
储存内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空、不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
另外一部分是类型指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
对象的访问定位
Java程序通过栈上的引用来操作堆上的具体对象。
目前主流的访问方式有使用句柄和直接指针:
- 句柄访问的方式,java堆会划分出一块内存来作为句柄池,栈上的引用储存的就是对象的句柄地址。句柄地址中包括了对象的实例数据与类型数据的具体地址信息
- 直接指针访问,栈上的引用储存的就是对象的地址
句柄的优点:对象被移动,只会修改句柄中的实例数据的指针,引用不需要修改
直接访问的优点:速度更快,减少时间开销
测试内存异常
Java堆溢出
Java堆用于存储对象实例,不断创建对象,就可以测试到现象
/**
* VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
* @author Axe
**/
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid5488.hprof ...
Heap dump file created [13371418 bytes in 0.049 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at HeapOOM.main(HeapOOM.java:17
堆内存的OOM异常是最常见的,出现的时候我们可以使用Memory Analyzer工具分析hprof文件,具体的使用方式,网上很多教程、如果都是正常的,就要考虑是不是堆的空间给得不够大。
虚拟机栈和本地方法栈溢出
直接测试:
/**
* VM Args: -Xss128k
*/
public class JavaVMStacksOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStacksOF of = new JavaVMStacksOF();
try {
of.stackLeak();
}catch (Throwable e){
System.out.println("stack length:"+ of.stackLength);
throw e;
}
}
}
运行结果:
stack length:981
Exception in thread "main" java.lang.*Error
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:8)
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:9)
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:9)
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:9)
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:9)
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:9)
at JavaVMStacksOF.stackLeak(JavaVMStacksOF.java:9)
出现*Error可以阅读堆栈信息查找问题
方法区和运行时常量池溢出
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
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 o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,objects);
}
});
enhancer.create();
}
}
static class OOMObject{
}
}
很遗憾,我在java1.8里面没有复制到现象
本地直接内存溢出
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
try {
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at DirectMemoryOOM.main(DirectMemoryOOM.java:17)
直接内存溢出后Dump文件会很小,如果程序中使用了NIO,就需要往这方面思考。