Java堆溢出
限制Java堆的大小为20M,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dunmp出当前内存堆转储快照以便事后进行分析。
//-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> objects = new ArrayList<OOMObject>();
while (true) {
objects.add(new OOMObject());
}
}
}
异常提示java heap space,首先确认内存中的对象是否是必要的,也就是要分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄露:可以通过辅助工具查看泄露对象到GC Roots的引用链。找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Root引用链的信息,就可以比较准确地定位出泄露代码的位置。
如果是内存溢出:就应该检查虚拟机的堆参数(-Xms与-Xmx),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序的内存消耗。
虚拟机栈和本地方法栈溢出
栈容量只由-Xss参数设定,栈可能会抛出两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在的栈内存在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
StackOverflowError
无论是减少栈内存容量,还是定义大量的本地变量,增大此方法帧中本地方法变量表的长度,都会抛出StackOverflowError,异常时输出的堆栈深度相应缩小。
//减少栈内存容量VM Args:-Xss128k
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF sof = new JavaVMStackSOF();
try {
sof.stackLeak();
} catch (Throwable throwable) {
System.out.println("stack length:" + sof.stackLength);
throw throwable;
}
}
}
实验表明,在单线程下,无论是由于栈太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。OutOfMemoryError
不断地创建线程的方式可以造成OOM,但是这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,因为为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
//VM Args:-Xss2M(不妨设置大些)
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
System.out.println("test");
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(this::dontStop);
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
javaVMStackOOM.stackLeakByThread();
}
}
虚拟机栈和本地方法栈内存 = 总内存 - Java堆 - 方法区,程序计数器消耗内存很小,忽略不计,每个线程分配到的栈容量越大,可以建立的线程数量自然就越小,建立线程时就越容易把剩下的内存耗尽。如果是多线程导致内存溢出,在不减少线程数量或更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
方法区和运行时常量池溢出
String.intern()方法
String.intern()是native方法,作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
StringBuilder.toString创建的字符在Java堆上,“计算机软件”是第一次出现,所以intern会在常量池中记录首次出现的实例引用,因此intern返回的引用和StringBuilder.toString创建的字符串是同一个,而“java”这个字符串在执行StringBuilder.toString之前就已经存在,字符串常量池中已经有它的引用了,不符合“首次出现”的原则。溢出
```java //通过-XX:PermSize=10M -XX:MaxPermSize=10M间接限制方法区大小,从而间接限制其中常量池的容量
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保存着常量池引用,避免FULL GC回收常量池行为
List
![](https://cdn.nlark.com/yuque/0/2019/png/218528/1571317265752-34e6c0e0-bf6c-4c5e-94b6-6dc8242f518c.png?x-oss-process=image%2Fresize%2Cw_999%2Climit_0#from=url&id=LerMH&margin=%5Bobject%20Object%5D&originHeight=351&originWidth=999&originalType=binary&ratio=1&status=done&style=none)<br />方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。经常动态生成大量Class的应用中,容易出现方法区溢出,常见的有CGLib字节码增强和动态语言、大量JSP或动态产生JSP文件的应用、基于OSGi的应用等。
<a name="etTKK"></a>
# 本机直接内存溢出
DirectByteBuffer分配内存也会抛出内存溢出异常,但是这是计算得出的异常,不是向操作系统申请分配内存出的错,真正申请分配内存的方法是Unsafe.allocateMemory()。
```java
//-Xmx20M -XX:MaxDirectMemorySize=10M,不指定默认和Java堆最大值-Xmx一样
public class DirectMemoryOOM {
private static final int _1M = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(_1M);
}
}
}
DirectByteBuffer导致的内存溢出一个明显的特征是Heap Dump文件中不会看见明显的异常。