结论先行:常见的OOM
(一)栈内存溢出:java.lang.StackOverflowError
(二)堆内存溢出:java.lang.OutOfMemoryError: Java heap space
(三)直接内存溢出:java.lang.OutOfMemoryError: Direct buffer memory
(四)“垃圾回收上头”:java.lang.OutOfMemoryError: GC overhead limit exceeded
(五)元空间溢出:java.lang.OutOfMemoryError: Metaspace
(六)线程创建过多:java.lang.OutOfMemoryError: Unable to create new native thread
一. StackOverflowError
1.1 写个bug
/**
* 栈空间溢出,属于错误!!!!
*/
public class StackOverflowErrorDemo {
public static void main(String[] args) {
StackOverflowError();
}
public static void StackOverflowError(){
StackOverflowError();
}
}
1.2 bug现象
1.3 记住,这属于错误分支,不是异常!
1.4 现象分析
♦ 无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法而无法退出的情况;
♦ 执行了大量方法,导致线程栈空间耗尽;
♦ 方法内声明了海量的局部变量;
1.5 解决方案
♦ 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug;
♦ 排查是否存在类之间的循环依赖(当两个对象相互引用,在调用toString方法时也会产生这个异常);
♦ 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制;
二. Java heap space
2.1 写个bug
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
// byte[] bytes = new byte[30*2014*2014];
String str = "javaheapsapce";
while (true){
// 故意生成很多新对象
// 带上参数: -Xms10m -Xmx10m
str += str + new Random().nextInt(100000)+ new Random().nextInt(500000);
str.intern();
}
}
}
2.2 bug现象
2.3 现象分析
♦ 请求创建一个超大对象,通常是一个大数组;
♦ 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值;
♦ 过度使用终结器(Finalizer),该对象没有立即被 GC;
♦ 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收;
2.4 解决方案
针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:
♦ 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制;
♦ 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级;
♦ 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接;
三. Direct buffer memory
3.1 写个bug
import java.nio.ByteBuffer;
/**
* 导致原因:
* 写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲(Buffer)的I/O 方式,它可以使用Native函数库直接分配堆外内存,
* 然后通过一个存储再Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能再一些场景中显著提高性能,因为避免了再java堆和native堆中来回复制数据。
*
* ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
* ByteBuffer.allocteDirect(capability)第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
*
* 但是如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC, DirectByteBuffer对象他们就不会被回收,这时候堆内存充足,但本地内存可能已经使用
* 完了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
*/
public class DirectBufferDemo {
// -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public static void main(String[] args) throws Exception {
System.out.println("配置demaxDirectMemory"+(sun.misc.VM.maxDirectMemory()/(double)1024/1024)+"MB");
Thread.sleep(3000);
ByteBuffer bb = ByteBuffer.allocateDirect(6*1024*1024);
}
}
3.2 bug现象
3.3 现象分析
♦ Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory 错误。
3.4 解决方案
♦ Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查;
♦ 检查是否直接或间接使用了 NIO,如 netty,jetty 等;
♦ 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值;
♦ 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效;
♦ 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean()方法来主动释放被 Direct ByteBuffer 持有的内存空间;
四. GC overhead limit exceeded
4.1 写个bug
import java.util.ArrayList;
import java.util.List;
/**
* GC回收时间过长时会抛出outOfMemroyError。过长的定义是,超过98%的时间用来做CC并且回收了不到2%的堆内存连续多校GC都只回收了到2%的极端情况下才会地出。假如不抛出GC overhead Limit错误会发生什么情况呢?
* 那就是G清理的这么点内存很快会再次填满,迫使GC再次执行.这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果
*/
public class GCOverheadDemo {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true){
list.add(String.valueOf(++i).intern());
}
}catch (Throwable e){
System.out.println("*** i="+i);
e.printStackTrace();
throw e;
}
}
}
4.2 bug现象
不知道为啥,我实现出来的一直显示不出来 GC overhead limit exceeded
4.3 现象分析
♦ JVM 内置了垃圾回收机制GC,所以作为 Javaer 的我们不需要手工编写代码来进行内存分配和释放,但是当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误(俗称:垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。
4.4 解决方案
♦ 添加 JVM 参数-XX:-UseGCOverheadLimit 不推荐这么干,没有真正解决问题,只是将异常推迟;
♦ 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码;
♦ dump内存分析,检查是否存在内存泄露,如果没有,加大内存;
五. Metaspace
5.1 写个bug
/**
* JVM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class MetaspaceOOMDemo {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaspaceOOMDemo.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
//动态代理创建对象
return methodProxy.invokeSuper(o, objects);
});
enhancer.create();
}
}
}
5.2 bug现象
Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace
5.3 现象分析
♦ JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。
5.4 解决方案
方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景中,就应该特别关注这些类的回收情况。这类场景除了上边的 GCLib 字节码增强和动态语言外,常见的还有,大量 JSP 或动态产生 JSP 文件的应用(远古时代的传统软件行业可能会有)、基于 OSGi 的应用(即使同一个类文件,被不同的加载器加载也会视为不同的类)等。
♦ -XX:MaxMetaspaceSize 设置元空间最大值,默认是 -1,表示不限制(还是要受本地内存大小限制的)
♦ -XX:MetaspaceSize 指定元空间的初始空间大小,以字节为单位,达到该值就会触发 GC 进行类型卸载,同时收集器会对该值进行调整
♦ -XX:MinMetaspaceFreeRatio 在 GC 之后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集频率,类似的还有 MaxMetaspaceFreeRatio
六. Unable to create new native thread
6.1 写个bug
public class UnableCreateNewThreadDemo {
public static void main(String[] args) {
for (int i = 0; ; i++) {
System.out.println("******** i = "+i);
new Thread(()->{
try {
Thread.sleep(Integer.MAX_VALUE);
}catch (Exception e){
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
6.2 bug现象
6.3 现象分析
JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread,常见的原因包括以下几类:
♦ 线程数超过操作系统最大线程数限制(和平台有关)
♦ 线程数超过 kernel.pid_max(只能重启)
6.4 解决方案
♦ 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程;
♦ 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制;