前面的一个章节都是在手关于检测信息的工具,那么本章节中就要运用这些工具来解决我们日常中遇到的问题,这样工具才显得有意义。
一、案例分析
1.高性能硬件上的程序部署策略
描述:一个15万PV/天左右的在线文档类型网站硬件系统升级为4个CPU、16GB物理内存,操作系统为64位的CentOS 5.4,Resin作为服务器,为了尽量利用硬件资源,选用64位的JDK1,5,通过设置-Xmx和-Xms参数将Java堆固定在12GB,结果运行一段时间后,网站经常出现不定期的长时间失去响应的情况
分析:监控服务器发现失去响应是有GC停顿导致的,虚拟机运行在Server模式下,默认使用吞吐量优先的收集器,回收12GB的堆,一次fullGC的停顿时间就高达14秒,在加上文档从磁盘中提取到内存中,导致内存中出现很对由于文档序列产生的大数据,这些大对象很多进入老年代,内存很快被消耗殆尽
解决:目前在高性能硬件上部署程序,目前主要有两种方式:通过64位的jdk类使用大内存和使用若干个32位虚拟机建立逻辑群来利用硬件资源,前者的部署对于Full GC的控制比较严格,而且面前64位的应用还不是非常的成熟,所以考虑最多的部署方式还是后一种,考虑在一台物理机器上启动多个应用服务进程,每个服务进程分配不同端口,然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。目前的情况如果不考虑其他的,仅仅是为了最大的利用硬件资源,可以使用无Session复制的亲合式集群。
2.集群间同步导致的内存溢出
描述:一个基于B/S的MIS系统,硬件为两台2个CPU、8GB内存的HP小型机,服务器为Weblogic9.2,每台机器启动三个weblogic实例,构成一个6个节点的亲合式集群。节点之间没有Session共享,但是有需求需要实现部分数据在各个节点间共享,开始的时候放在数据库中保存,但是由于读写频繁,后面使用JBossCache构建了一个全局缓存。启动之后,服务器正常使用了一段较长的时间,但是最近却不定期的出现了多次的内存溢出问题。
分析:初步断定可能是JBossCache的原因,他是基于自家的JGroups进行集群间的数据通信,由于信息在传输失败的时候有重发的可能性,在确认所有注册在CMS是节点都收到正确的信息前,发送的信息必须在内存中保存,而在此MIS的服务端中有一个负责安全校验的全局Filter,当没接受到请求时,均会更新最后一次操作时间,并且将这个时间同步到所有的节点上去,使得一个用户在一段时间内不能在多台机器上登录,在服务使用的过程中,往往一个页面会产生数次乃至数十次的请求,因此这个过滤器导致集群各个节点之间网络交互非常频繁,当网络情况不能满足传输要求时,重发数据在内存中不断堆积,很快就出现了内存溢出。
解决:主要是JBossCache的缺陷,当然也有MIS系统实现上的缺陷,所有最好的解决办法就是替换成别的
3.堆外内存导致的溢出错误
描述:基于B/S的电子考试系统,采用逆向AJAX技术,选用CometD 1.1.1作为服务端推送框架,服务器是Jetty 7.1.4,硬件为一台普通PC机,Core i5 CPU,4GB内存,运行32位Windows操作系统,测试期间发现服务端不定时抛出内存溢出异常,万一正式考试的时候出现这样的问题,将会带来很严重的后果。
分析:可能是堆没有开到最大,可是32位系统堆最大分配1.6GB,可是监控显示堆并没有任何压力还是出现了内存溢出,那么会不会是堆之外的内存,这时候我们想到了前面学习的直接内存,这个部分的内存不能在达到限制的时候通知垃圾回收器回收,只有等到老年代满了之后执行Full GC的时候才能顺便将其回收,而且本案例中的CometD 1.1.1框架,正好有大量的NIO操作需要用到直接内存(Direct memory),所有基本上就能确定是这个部分的内存溢出了
解决:Direct Memory通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出异常。线程堆栈通过-Xss参数调整,Socket缓存区分配不合理也会抛出异常,如果代码中使用JNI调用本地库,那本地使用的内存也不再堆中,虚拟机和GC的代码执行也需要消耗一定的内存。
4.外部命令导致系统缓慢
描述:运行一台4个CPU的Solars 10操作系统上,中间为GlassFish服务器,系统在做大并发压力测试的时候,发现请求响应时间比较慢。
分析:通过操作系统的mpatat工具发现CPU使用率很高,但是并不是用户应用的CPU高,而是“fork”系统调用,这个系统调用是Linux用来产生新进程的,后来发现每个用户请求的处理都是需要执行一个外部shell脚本来获得一些信息,这个脚本是通过java的Runtime.getRuntime().exec()方法来调用的,这种调用在虚拟机中是非常消耗资源的,所以频繁调用对CPU的压力很大。
解决:将这些shell脚本改成用java的API去获取这些信息
5.服务器JVM进程崩溃
描述:一个基于B/S的MIS系统,硬件为两个CPU、8GB内存的HP系统,服务器是Weblogic9.2,正常运行一段时间后,最近发现在运行期间频繁出现集群节点的虚拟机进程自动关闭的现象,留下一个hs_err_pid###.log文件后,进程就消失了,两台物理机器里的每个节点都出现过进程崩溃的现象。
分析:了解到最近与一个OA门户做了集成,通过web服务通知OA门户系统,把代办事项的变化同步到OA门户之中,当时是使用异步的方式调用web服务,但是由于两边的服务速度完全不对等,时间越长就累积了越多的web服务没有调用完成,导致在等待的线程和Socket连接越来越多,最终在超过虚拟机的承受能力后使得虚拟机进程崩溃。
解决:通知OA方修复无法使用的集成接口,并将异步调用改成生产者/消费者模式的消息队列实现后,系统恢复正常。
6.不恰当的数据结构导致内存占用过大
描述:一个后台RPC服务器,使用64位虚拟机,内存配置-Xms4g -Xmx8g -Xmn1g,使用ParNew+CMS收集器组合平时对外的Minor GC时间约在30毫秒以内,完全可以接受,但是现在的业务是每10分钟需要加载一个约80MB的数据文件到内存进行数据分析,这些数据在内存中会形成超过100万个HashMap<Long,Long>Entry,在这段时间里面Minor GC就会造成500毫秒的停顿,对于这个停顿接受不了。
分析:平时Minor GC之后的Eden和survivor基本处于空闲状态,但是在分析数据期间,Minor GC之后新生代中的绝大部分对象还是存活的,我们知道ParNew收集器是基于复制算法,这个算法的高效是建立在大部分对象都是朝生夕灭的特性上的,现在大部分对象都能存活,把这些对象复制到Survivor并维持对象引用的正确就成为一个沉重的负担,因此导致GC暂停时间明显变长。
解决:考虑将Survivor空间去掉,让新生代中存活的对象在第一次Minor GC之后就进入老年代,等到Full GC的时候再去收集他,但是治标不治本,现在考虑修改程序,文件中的数据在内存中的就够数据是HashMap<Long,Long>,这种结构的空间效率只有16B/88B=18%,太低,可以换成空间效率高一点的结构数据。
7.由Windows虚拟机内存导致的长时间停顿
描述:有一个带心跳检测功能的GUI桌面程序,每15秒回发送一次心跳检测信号,如果对方30秒内没有信息返回,就认为和对方程序的连接断开,程序上线后发现心跳检测有误报的嫌疑,查询日志发现误报的原因是程序偶尔出现间隔约一分钟左右的时间完全无日志输出,处于停顿状态。
分析:在GC收集的过程中偶尔会有一次1分钟的,查看得出其实真正GC的时间并不是很长,时间都是花在开始GC到真正GC之间,还有当他最小化时,怀疑工作内存被自动交换到磁盘的页面文件中,这样发生GC时就有可能因为恢复页面文件的操作导致不正常的GC
解决:加入参数-Dsun.awt.keepWorkingSetOnMinimize=true
二、Eclipse运行速度调优
1.设置最大堆大小并开启JMX管理(需要在VisualVM中收集原始数据)
2.升级JDK版本
3.编译时间和类加载时间优化
4.调整内存设置控制垃圾回收频率
5.选择收集减低延迟