Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的几个运行时区域都可能发生OutOfMemoryError(OOM)。
堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出。
package org.hbin.oom;
import java.util.ArrayList;
import java.util.List;
/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author Haley
* @version 1.0
* 2024/8/31
*/
public class HeapOOMTest {
public static void main(String[] args) {
List<MyObject> list = new ArrayList<>();
while(true) {
list.add(new MyObject());
}
}
static class MyObject {}
}
# 运行结果
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid18439.hprof ...
Heap dump file created [27854978 bytes in 0.235 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:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at org.hbin.oom.HeapOOMTest.main(HeapOOMTest.java:17)
要解决这种瓿,一般是先通过内存分析工具对Dump出来的堆转储快照文件进行分析,确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确的定位出泄露代码的位置。
如果不存在泄露,就是内存中的对象确实还必须存活,那就应当检查虚拟机的堆参数(-Xms和-Xmx)与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过
长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
虚拟机栈和本地方法栈溢出
HotSpot虚拟机中并不区分虚拟机栈和本地方法栈。因此,-Xoss参数无效,-Xss参数可以设定栈容量。Java虚拟机规范中描述了栈相关的两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
在单线程中,尝试定义大量的本地变量、大量的方法参数均未产生OutOfMemoryError异常,运行结果都是StackOverflowError异常。代码如下:
package org.hbin.oom;
/**
* VM args: -Xss256k
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class VMStackTest {
private int stackLength;
public void test() {
stackLength ++;
test();
}
public static void main(String[] args) {
new VMStackTest().test();
}
}
运行结果:
Exception in thread "main" java.lang.StackOverflowError
at org.hbin.oom.VMStackTest.test(VMStackTest.java:14)
at org.hbin.oom.VMStackTest.test(VMStackTest.java:14)
at org.hbin.oom.VMStackTest.test(VMStackTest.java:14)
……
结论:在单线程中,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
如果不限于单线程,通过不断地建立线程的方式可以产生内存溢出异常。但是这样产生内存溢出与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
package org.hbin.oom;
/**
* VM args: -Xss256k
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class VMStackTest2 {
private void busy() {
while(true);
}
private void test() {
while(true) {
new Thread(() -> busy()).start();
}
}
public static void main(String[] args) {
new VMStackTest2().test();
}
}
运行结果:
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(Thread.java:719)
at org.hbin.oom.VMStackTest2.test(VMStackTest2.java:17)
at org.hbin.oom.VMStackTest2.main(VMStackTest2.java:22)
Windows平台中,该代码执行时可能会导致操作系统假死。如果尝试运行该代码,请务必先保存好当前的工作。
方法区和运行时常量池溢出
String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。
在JDK1.6及之前版本,常量池位于永久代中,可以通过-XX:PermSize 和 -XX:MaxPermSize限制方法区大小,从而间接限制常量池的容量。
在JDK8中,字符串常量池位于堆中,上述参数无效了,可以通过-Xmx指定最大堆空间。
package org.hbin.oom;
import java.util.ArrayList;
import java.util.List;
/**
* JDK1.6 VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
* JDK8 VM args: -Xms20m -Xmx20m
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class ConstantPoolTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:401)
at java.lang.String.valueOf(String.java:3099)
at org.hbin.oom.ConstantPoolTest.main(ConstantPoolTest.java:22)
关于常量池,这里有一个有意思的现象:
package org.hbin.oom;
/**
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class ConstantPoolTest2 {
public static void main(String[] args) {
String s1 = new StringBuilder("hello").append("world").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern() == s2);
}
}
这段代码在不同版本的JDK运行结果可能有差异:
- JDK 1.6:两个false
- JDK8:true和false
产生上述差异的原因是:
- JDK1.6中,intern会把首次遇到的字符串实例复制到永久代中,并返回永久代中这个字符串的引用。
- JDK8中,调用intern时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。
至于str2比较返回false是因为java这个字符串已经存在于字符串常量池中。测试发现,字符串常量池中还存在其他字符串:java,jar,true,false,main,void,byte,short,int,long,char,boolean,float,double
。
使用CGLib操作字节码时也可能产生类似问题,代码如下:
package org.hbin.oom;
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: -Xms20m -Xmx20m
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class MethodAreaTest {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, objects);
}
});
enhancer.create();
}
}
}
本机直接内存溢出
DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,默认是Java堆最大值。
package org.hbin.oom;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class DirectMemoryTest {
private static final int _1MB = 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(_1MB);
}
}
}
package org.hbin.oom;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class DirectMemoryTest2 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
while(true) {
ByteBuffer bf = ByteBuffer.allocateDirect(_1MB);
list.add(bf);
System.out.println(i ++);
}
}
}
运行结果:
0
1
2
3
4
5
6
7
8
9
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at org.hbin.oom.DirectMemoryTest2.main(DirectMemoryTest2.java:20)
由DirectMemory导致的内存溢出,明显特征是:在Heap Dump文件中不会看见明显的异常。