JVM性能调优
高性能硬件上部署程序
策略
通过64位JDK来使用大内存
控制Full GC时间
对于交互性强、对停顿时间敏感的系统,可以给Java虚拟机超大内存的前提是有把握把应用程序的GC频率控制的足够低,譬如十几个小时乃至一天才出现一次Full GC,这样可以在深夜定时任务执行Full GC甚至重启服务器来保持内存空间在一个稳定的水平。
控制Full GC频率
控制Full GC频率关键是应用中绝大多数对象能否符合“朝生夕死”的原则,即大多数对象的生存时间不应太长,尤其是不能有成批量的、长生存时间的大对象产生,这样才能保障老年代空间的稳定。
控制对象的生命周期
在大多数网站形式的应用里,主要对象的生存周期应该是请求级或是页面级的,会话级和全局级的长生命对象相对很少。
面临问题
- 内存回收导致的长时间停顿
- 64位JDK的性能测试结果普遍低于32位JDK
- 需要保证程序足够稳定,如果产生几十G的堆转储快照也无法分析
- 相同程序在64位JDK内存消耗一般比32位JDK要大,这是由于指针膨胀以及数据对齐补白等因素造成的
使用若干个32位虚拟机建立逻辑集群来利用硬件资源
工作方式
在一台物理机上启动若干个服务器进程,启动不同的端口,在使用负载均衡服务器(根据SessionID分配)使用反向代理来分配请求。
面临问题
- 尽量避免节点竞争全局的资源,最典型的就是磁盘竞争,如果每个节点同时访问一个磁盘文件(尤其是并发写操作)时,容易导致I/O异常
- 很难高效的利用某些资源池
- 每个节点都受32位的内存限制(windows: 2GB, linux: 4GB)
- 大量使用本地缓存,可以共用一份集中式缓存
集群间同步导致的内存溢出
解决方案
被集群共享的数据可以允许频繁的读操作,但是要避免频繁的写操作。
堆外内存导致的内存溢出
问题表现
服务器不定时抛出OOM异常,服务器不一定每次都会出异常,开大内存也没有用,并且异常会更频繁,新生代和老年代内存也很稳定。查看系统日志:java.nio.DirectByteBuffer报出内存溢出,即直接内存(Direct Memory)内存溢出。Direct Memory不能像新生代和老年代一样,内存不足时通知垃圾收集器进行垃圾回收,他只能等老年代满了后Full GC,然后顺便清理内存的废弃对象。
容易造成该现象的内存区域
- Direct Memory
- NIO有大量的操作
- 线程堆栈
- 无法分配新的堆栈时抛出OOM
- 无法建立新的线程时抛出StackOverFlowError
- Socket 缓冲区
- Receive缓冲区(约37KB),如果无法分配可能会抛出IOException
- Send缓冲区(约25KB),如果无法分配可能会抛出IOException
- JNI代码
- 如果代码使用JNI调用本地方法,那本地库使用的内存也不在堆中
- 虚拟机和GC
- 虚拟机和GC执行的代码也需要消耗一定的内存
外部命令导致系统缓慢
问题表现
做大压力测试时发现请求响应很慢,查出CPU使用率很高,并且占用绝大多数的CPU资源不是应用系统本身,查看最消耗CPU资源的是“fork”系统调用。
导致原因
每个用户的请求都要执行一个shell脚本去获得系统信息,调用脚本是通过java的Runtime.getRuntime.exec()
方法来调用的,它在Java虚拟机中是非常消耗资源的操作,即使外部命令很快就能执行完毕,频繁调用时创建进程的开销也非常客观。
虚拟机执行步骤
- 克隆一个和当前虚拟机拥有一样环境变量的进程
- 用这个进程去执行外部命令
- 退出进程
改进方案
直接使用Java的API去掉用
服务器JVM进程崩溃
问题表现
在服务器正常运行一段时间后,发现运行期间频繁出现集群节点的虚拟机进程自动关闭的现象,留下了一个**.log
文件后,进程就消失了。每台服务器上的节点都出现过进程崩溃的现象。研究发现,每个崩溃的虚拟机节点在崩溃前不久都抛出了相同的SocketException: Connection reset
。
导致原因
这是一个远端断开连接的异常,由于工作流要通过Web服务通知OA门户系统代办事项有变化,但是调用后要长达3分钟后才能返回,并且返回都是连接失败,为了不被OA拖累,使用异步的方式调用Web服务,但是由于两边服务速度的完全不对等,导致等待的线程和Socket连接越来越多,最终导致虚拟机进程崩溃。
改进方案
通知OA门户方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列模式。
不恰当的数据结构导致占用内存过大
问题表现
RPC服务器,使用的ParNew + CMS收集器组合,平时对外服务的Minor约在30ms以内。但业务每十分钟加载一个约80MB的数据文件到内存中进行数据分析,这些数据会在内存中形成100W个HashMap
导致原因
在平时Minor GC后,Eden区和Survivor区都很空闲,但是在数据分析时,800M的Eden很快就被填满然后触发Minor GC,但GC后还有大量的对象存活下来,由于ParNew使用的是复制算法,所以复制到Survivor区并维持这些对象的引用的正确就成了一个沉重的负担,因此GC暂停时间变长。
其实根本原因是HashMap
改进方案
- 仅从GC调优角度
- 可以将Survivor区去掉,让新生代第一次Minor GC就进入老年代
- 更换数据结构
参考资料
周志明. 深入理解JVM虚拟机