堆溢出
之前说过,堆中主要存储的是对象实例。
所以如果不断创建对象,并保证GC Roots(之后会说明)到对象间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到堆的容量限制后产生内存溢出。
异常示例代码如下:
/**
* 堆溢出
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* -Xms设置堆的最小值 -Xmx设置堆的最大值
*
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
通过-Xms和-Xmx设置Vm最小、最大堆的值为20m(将最大最小值设为一样可以避免堆的自动扩展)
栈溢出
HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以这里统称为栈溢出。
之前已经讨论过,虚拟机栈会抛出两种异常,StackOverflowError 和 OutOfMemoryError 异常。(见JVM学习笔记(2)---Java内存区域
Java虚拟机规范中,对该区域规定了两种异常:
- StackOverFlowError:线程请求的栈深度大于虚拟机允许的栈深度时
- OverOfMemoryError:动态扩展的线程无法申请到足够的内存时
简单理解来看:
- 对于前者而言,是由于增加过大栈帧深度或限制虚拟机栈内存产生。
在单线程中,我们不断增大栈帧中本地变量表的长度(如定义大量的本地变量),或者限制栈内存容量(通过Vm启动参数),都输出StackOverFlowError。 - 对于后者而言,是由于不断建立线程产生。
这样产生的 OutOfMemoryError 与栈空间大小不存在关系。在内存总量一定时,每个线程的栈分配的内存越大,则越容易产生内存溢出。
异常示例代码如下:
/**
* 栈溢出 OutOfMemoryError
* VM Args:-Xss2m
* -Xss设置栈大小 来减小栈容量
* 运行本代码前请先保存当前电脑环境,可能假死
*/
public class StackOOM {
public int threadCount = 0;
public void addThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
while (true) {
}
}
});
thread.start();
}
}
public static void main(String[] args){
StackOOM stackOOM = new StackOOM();
stackOOM.addThread();
}
}
直接内存溢出
在JDK1.4之后,新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以直接使用Native函数库分配堆外内存,通过一个存储在Java堆中的 DirectByteBuffer对象 作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,避免了在Java堆和Native堆中来回复制数据。
通过建立Unsafe实例,可以申请分配直接内存。通过MaxDirectMemorySize参数,可以指定直接内存容量,当申请的直接内存过大时,会出现直接内存溢出。
异常示例代码如下:
/**
* Vm Args: -Xmx20M -xx:MaxDirectMemorySize=10M
*
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024*1024;
public static void main(String[] args)throws Exception{
//通过反射获取Unsafe实例,来申请内存分配
Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];
unsafeFiled.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeFiled.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}
}
元空间溢出
我们已知,方法区中主要存放的是一些描述性信息,即元数据。元空间是方法区的一种实现方式。
(注意,方法区是一种规范,元空间 和 永久代 都是一种实现。
在JDK1.8之前,使用 永久代(PermGen)来实现方法区,但有以下问题:
- 永久代内存经常不够用或发生内存泄露,爆出异常 OutOfMemoryError: PermGen
- 动态类加载的情况越来越多,这块内存我们变得不太可控。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
在jdk8之后,用元空间(MetaSpace)替代。 元空间是和本地内存相关的。默认上限大小是本地内存。
元空间与永久代之间 最大区别 在于:
元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
异常代码如下:
import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;
import java.util.*;
/**
* VM args: -XX:MaxMetaspaceSize=1M -XX:+PrintGCDetails
*/
public class MetaSpaceOOM {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 80000; i++) {
//动态创建类
Map<Object, Object> propertyMap = new HashMap<Object, Object>();
propertyMap.put("id", Class.forName("java.lang.Integer"));
//建立一个动态生成bean
CglibBean bean = new CglibBean(propertyMap);
//给 Bean 设置值
bean.setValue("id", new Random().nextInt(100));
//打印 Bean的属性id
System.out.println("num=" + i + " id=" + bean.getValue("id"));
}
}
static class CglibBean {
/**
* 实体Object
*/
public Object object = null;
/**
* 属性map
*/
public BeanMap beanMap = null;
public CglibBean() {
super();
}
@SuppressWarnings("unchecked")
public CglibBean(Map propertyMap) {
this.object = generateBean(propertyMap);
this.beanMap = BeanMap.create(this.object);
}
/**
* 给bean属性赋值
* @param property 属性名
* @param value 值
*/
public void setValue(String property, Object value) {
beanMap.put(property, value);
}
/**
* 通过属性名得到属性值
* @param property 属性名
* @return 值
*/
public Object getValue(String property) {
return beanMap.get(property);
}
/**
* 得到该实体bean对象
* @return
*/
public Object getObject() {
return this.object;
}
@SuppressWarnings("unchecked")
private Object generateBean(Map propertyMap) {
BeanGenerator generator = new BeanGenerator();
Set keySet = propertyMap.keySet();
for (Iterator i = keySet.iterator(); i.hasNext();) {
String key = (String) i.next();
generator.addProperty(key, (Class) propertyMap.get(key));
}
return generator.create();
}
}
}
由于代码循环创建class,大量的class元数据存放在元数据区,超过了设置的1M空间,因此报元数据区OOM:
![](https://img.haomeiwen.com/i6802002/18d1c7095ab8d4f2.png)
解决办法也很简单,去掉MaxMetaspaceSize 限制 即可。
在JDK1.8.1之后,错误提示不再报OOM,改为 MaxMetaspaceSize is too small:
![](https://img.haomeiwen.com/i6802002/de6b11cd4529979d.png)
参考与推荐
OOM情况分析
Java Heap dump文件分析工具jhat简介
JVM调优命令-jmap
内存分析工具MAT(Memory Analyzer Tool)从安装到使用
Java was started but returned exit code=13 问题解决
网友评论