调优案例分析三【堆外内存导致的溢出错误】
2021-07-13 本文已影响0人
云芈山人
案例出于《深入理解Java虚拟机》第二版
场景
一个学校的小型项目:基于B/S的电子考试系统,为了实现客户端能实时地从服务器接收考试数据,系统使用了逆向AJAX技术(也称Comet或者Server Side Push),选用CometD1.1.1作为服务端推送框架,服务器是Jetty7.1.4,硬件为一台普通PC机,Core i5 CPU,4GB 内存,运行32位Windows操作系统。
问题
- 测试期间发现服务端不定时抛出内存溢出异常,网站管理员尝试过把堆开到最大,而32位系统最多到1.6GB就基本无法再加大了,且开大基本没效果,抛出内存溢出异常好像更频繁。
- 加入-XX:+HeapDumpOnOutOfMemoryError,也没反应,抛出内存溢出异常时什么文件都没有发生。
- 通过jstat发现GC并不频繁,Eden区、Survivor区、老年代以及永久代内存全部都无问题,但是仍然不停抛出内存溢出异常。
分析
- 系统日志中找到异常堆栈
[org.eclipse.jetty.util.log] handle failed java.lang.OutOfMemoryError:null - 操作系统对每个进程能管理的内存是有限制的,这台服务器使用的32位Windows平台的限制是2GB,其中划了1.6GB给Java堆,而Direct Memory内存并不算入1.6GB的堆之内,因此它最大也只能在剩余的0.4GB空间中分出一部分。
- 此应用中导致内存溢出的关键:垃圾收集进行时,虚拟机虽会对Direct Memory 进行回收,但Direct Memory却不能像新生代、老年代那样,发现空间不足就通知收集器进行垃圾回收,它只能等老年代满了后Full GC,然后“顺便” 帮它清理内存的废弃对象。否则它只能一直等到抛出内存溢出异常时,先catch,再在catch块里面:System.gc()!
- 若虚拟机无动作(如打开了-XX:+DisableExplicitGC开关),则即使堆中仍有许多空间内存,却不得不抛出内存溢出异常。
- 而本案例中CometD1.1.1框架,正好有大量的NIO操作需要使用到Direct Memory 内存。
经验
从实践经验的角度出发,除Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。
- Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或者OutOfMemoryError:Direct buffer memory。
- 线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或者OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。
- Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。
- JNI代码:如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。
- 虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。