一. 常见的OOM
1. 简述OOM错误种类:
1. java.lang.StackOverflowError //这个虽然不是OOM,但是也是内存溢出错误
2. java.lang.OutOfMemoryError: Java heap space
3. java.lang.OutOfMemoryError: GC overhead limit exceeded
4. java.lang.OutOfMemoryError: Direct buffer memory
5. java.lang.OutOfMemoryError: unable to create new native thread
6. java.lang.OutOfMemoryError: Metaspace
二. 细说OOM
1. java.lang.StackOverflowError
栈空间溢出 ,递归调用,栈被撑爆了,正常大小的栈是542K~1024K
代码演示:
// 错误产生原因:深度调用方法,导致出不来,栈爆了
public static void main(String[] args) {
stackOverflowError();
}
private static void stackOverflowError() {
stackOverflowError();
}
运行结果:
2. java.lang.OutOfMemoryError: Java heap space
堆内存溢出
private static void Demo02() {
//配置 VM options: -Xms1m -Xmx1m
//一句代码也可以
byte[] bytes = new byte[5 * 1024 * 1024];
}
private static void Demo01() {
String str = "JmStart";
while (true) {
//配置 VM options: -Xms1m -Xmx1m
str += new Random().nextInt(10000000);
}
}
public static void main(String[] args) {
//两种方式都可以报出异常
//Demo01();
//Demo02();
}
Demo01运行结果:
Demo02运行结果:
3. java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收时间过长
过长的定义是超过98%的时间用来做GC,并且回收了不倒2%的堆内存,连续多次GC,都回收了不到2%的极端情况下才会抛出。
如果不抛出,那就是GC清理的一点内存很快会被再次填满,迫使GC再次执行,这样就恶性循环。
CPU使用率一直是100%,而GC却没有任何成果。
代码演示:
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
//配置 VM options : -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
while (true) {
//保证添加到集合中的对象不重复
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("===========i: " + i);
e.printStackTrace();
throw e;
}
}
运行结果:
4. java.lang.OutOfMemoryError: Direct buffer memory
直接内存挂了,写NIO程序经常使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel)与缓存区(Buffer)的I/O方式,
它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,
这样能在一些场景中显著提高性能,因为避免了在java堆和native堆中来回复制数据
ByteBuffer.allocate(capability) 第一种方式是分配JVM堆内存,属于GC管辖,由于需要拷贝所以速度较慢
ByteBuffer.alloctedDirect(capability)分配os本地内存,不属于GC管辖,不需要拷贝,速度较快
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,
但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序直接崩溃了。
public static void main(String[] args) {
// 配置 VM options: -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
// 没有配置 VM 之前,MaxDirectMemory大小为1796.0M,约是我的电脑内存的四分之一
System.out.println("配置的MaxDirectMemory: " + (sun.misc.VM.maxDirectMemory() / (double)1024 / 1024) + "M");
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
运行结果:
5. java.lang.OutOfMemoryError: unable to create new native thread
应用创建了太多线程,一个应用进程创建了多个线程,超过系统承载极限。
你的服务器并不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024,超过这个数量,就会报错。
解决办法:
降低应用程序创建线程的数量,分析应用给是否针对需要这么多线程,如果不是,减到最低修改linux服务器配置。
在虚拟机Linux系统中演示观看更直观。
代码演示:
public static void main(String[] args) {
for (int i = 1; ; i++) {
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); }
}, ""+i).start();
}
}
运行结果:
6. java.lang.OutOfMemoryError: Metaspace
元空间主要存放了虚拟机加载的类的信息、常量池、静态变量、即时编译后的代码。
代码演示:
static class OOMTest{
}
public static void main(String[] args) {
int i = 0; // 记录多次报出异常
try {
while (true) {
i++; // 累加
// 配置 VM options : -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m
// 使用下面的 Enhancer,需要导 cglib.jar 和 asm.jar包
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.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, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("===============第多少次报出异常: " + i);
e.printStackTrace();
}
}
运行结果: