调优案例分析与实战

调优案例分析与实战

案例分析

高性能硬件上的程序部署策略

在一个15万PV/天左右的在线文档类型网站,硬件条件为4个CPU、16GB物理内存,操作系统为64位CentOS 5.4,Resin作为Web服务器。整个服务器资源都可供给这个访问量不算太大的网站使用。管理员为尽量利用硬件资源选用64位JDK 1.5,并通过-Xmx和-Xms参数将Java堆固定在12GB。使用一段时间后,发现使用效果不理想,网站经常不定期出现长时间失去响应

通过监控服务器运行状态发现网站失去响应是由GC停顿导致,虚拟机运行在Server模式下,默认使用吞吐量优先收集器,回收12GB的堆,一次Full GC停顿时间高达14秒。访问文档需要把文档从磁盘提取到内存中,导致内存中出现很多由文档序列化产生的大对象,这些对象很多进入了老年代,没有在Minor GC中清理掉。在这种情况下即使12G的堆,内存也快速被消耗掉,导致每隔十几分钟出现十几秒停顿

程序部署上的主要问题是过大的堆内存进行回收时带来的长时间停顿。在高性能硬件上部署程序,主要有两种方式

  • 通过64位JDK来使用大内存

  • 使用若干个32位虚拟机建立逻辑集群来利用硬件资源

对于用户交互性强、对停顿时间敏感的系统,可以给Java虚拟机分配超大堆的前提是应用程序的Full GC频率足够低,至少不能影响用户使用,如十几个小时乃至一天才出现一次Full GC,这样可以通过在深夜执行定时任务触发Full GC

控制Full GC频率的关键是应用中绝大多数对象能符合"朝生夕灭"的原则,大多数对象的生存时间不应太长,不能有成批量的、长生存时间的大对象产生,才能保证老年代空间的稳定。在大多数网站形式应用中,主要对象的生命周期都应该是请求级或页面级,会话级和全局级的长生命对象相对很少。正常情况下,应当能实现在超大堆中正常使用而没有Full GC,使用超大堆内存,网站响应速度会比较有保证,如果需要使用64位JDK管理大内存,还需要考虑下面这些问题

  • 内存回收导致的长时间停顿

  • 程序要足够稳定,因为这种应用要产生堆溢出几乎无法产生堆转储快照,因为需要产生十几GB乃至更大的Dump文件,即使产生快照也几乎无法分析

  • 相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,及数据类型对齐补白导致

对于第二种通过使用若干个32位虚拟机建立逻辑集群来利用硬件资源,其具体做法是在一台物理机器上启动多个应用服务进程,每个服务进程分配不同端口,然后前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。在一台物理机器上建立逻辑集群目的仅仅是为了尽可能利用硬件资源,并不需要关心状态保留、热转移之类的高可用性需求,也无需保证每个虚拟机进程有绝对准确的负载均衡,因此使用无Session复制的亲和式集群是一个相当不错的选择。我们仅仅需要保障集群具备亲合性,均衡器按一定的规则算法将一个固定的用户请求分配到固定的一个集群节点进行处理。使用逻辑集群可能会遇到下面问题

  • 尽量避免节点竞争全局资源,如磁盘竞争,各个节点如果同时访问某个磁盘文件,很容易导致IO异常

  • 很难高效地利用某些资源池,如连接池

  • 各个节点仍然不可避免地受到32位内存限制,在32位Windows平台中每个进程只能使用2GB内存,考虑堆外内存开销,堆最多只能使用1.5GB,在某些Linux或UNIX系统中,可以提升到3GB或接近4GB内存,但32位中仍然受最高4GB内存开销限制

  • 大量使用本地缓存的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点上都有一份缓存,可以考虑把本地缓存改为集中式缓存

将部署方案调整为5个32位JDK的逻辑集群,每个进程按2GB计算,其中固定堆为1.5GB。占用10GB内存。再建立一个Apache服务作为前端均衡代理访问门户。由于用户对响应速度比较关心,文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度低,因此使用CMS收集器进行垃圾收集

集群同步导致的内存溢出

在一个基于B/S的MIS系统中,硬件为两台2CPU、8GB内存的HP小型机,服务器是WebLogic 9.2,每台机器启动3个WebLogic实例,构成6节点亲合集群,节点之间不进行Session同步,有一些需求要实现部分数据在各个节点间共享。开始这些数据存放在数据库中,由于读写频繁竞争激烈,性能影响较大,改用JBossCache构建全局缓存,全局缓存开启后,服务正常工作一段时间后,出现不定期的内存溢出异常

在内存溢出异常不出现时,服务内存回收状况一直正常,每次内存回收后都能恢复到一个稳定的可用空间,开始怀疑是一些不常用的代码路径中存在内存泄漏,但最近并未升级、更新,以及特别的操作。 通过让服务带着-XX:+HeapDumpOnOutOfMemoryError参数运行一段数据后,在最近一次溢出后,发现存在大量org.jgroups.protocols.pbcast.NAKACK对象。

JBossCache是基于JGroups进行集群间的数据通信,JGroup使用协议栈方式实现收发数据包的各种所需特性自由组合,数据包接收和发送时要经过每层协议栈的up()和down()方法,其中NAKACK栈用于保障各个包的有效顺序、及重发。信息有传输失败需要重发可能性,在确认所有注册的GMS的节点都收到正确的信息前,发送的消息必须在内存中保留。在MIS的服务端中有一个负责安全校验的全局Filter,每当接收到请求时,均会更新一次最后操作时间,并将这个时间同步到所有节点,使得一个用户在一段时间内不能在多台机器上登录。在服务使用过程中,往往一个页面会产生数次乃至数十次的请求,这个过滤器将导致集群各个节点之间网络交互非常频繁。当网络情况不能满足传输要求时,重发数据在内存中不断堆积,很快会产生溢出

堆外内存导致的溢出错误

一个基于B/S的电子考试系统,为实现客户端实时从服务器接收考试数据,系统使用逆向AJAX技术,硬件为一台普通PC机,Core i5 CPU,4GB内存,32位Windows操作系统。但测试期间发现服务端不定时抛出内存溢出异常,尝试把堆开到最大,但32位系统最多到1.6GB基本无法加大,但开大后并没有效果,反而感觉更加频繁。加入-XX:+HeapDumpOnOutOfMemoryError,没有任何反应,抛出内存溢出时并没有产生文件。最后通过挂着jstat,发现GC并不频繁,Eden区、Survivor区、老年代以及永久代内存都显示压力并不大,但即使这样也依旧不停抛出内存溢出异常

操作系统对每个进程能管理的内存是有限的,在32位Windows平台限制是2GB,其中规划1.6GB给Java堆,而Direct Memory内存并不算入1.6GB堆内,只能是0.4GB堆外空间中的一部分。通过观察内存溢出后系统日志异常堆栈,发现导致溢出的关键是:垃圾收集进行时,虚拟机虽会对Direct Memory进行回收,但Direct Memory却不能像新生代、老年代那样,发现空间不足就通知收集器进行垃圾回收,只能等待老年代满了后Full GC,顺便清理掉内存的废弃对象。否则只能等到抛出内存溢出异常时,先catch掉,再调用System.gc(),但如果虚拟机打开-XX:+DisableExplicitGC开关,那就只能抛出内存溢出异常,即使现在堆中还有大量空闲内存

除Java堆和永久代之外,需要注意下面这些区域也会占用较多内存,所有内存总和受到操作系统进程最大内存限制

  • Direct Memory:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError: Direct buffer memory

  • 线程堆栈:可通过-Xss调整大小,内存不足抛出StackOverflowError或OutOfMemoryError:unable to cerate new native thread

  • Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占用37KB和25KB内存,连接多时这块内存占用也比较客观。如果无法分配则可能抛出IOException:Too many open files异常

  • JNI代码:如果代码中使用JNI调用本地库,本地库使用的内存也不再堆中

  • 虚拟机和GC:虚拟机、GC的代码执行也要消耗一定内存

外部命令导致系统缓慢

一个数字校园应用系统,运行在一台4个CPU的Solaris 10操作系统上。系统做大并发压力测试时,发现请求响应时间比较慢,通过操作系统mpstat工具发现CPU使用率很高,并且系统占用绝大多数的CPU资源的程序不是应用系统本身

通过Solaris 10的Dtrace脚本发现最消耗CPU资源的是fork系统调用。fork系统调用是Linux用来产生新进程的,而在Java虚拟机中,用户编写的Java代码最多只有线程的概念,不应该有进程的产生

通过分析发现每个用户请求处理都需要执行一个外部shell脚本获取系统的一些信息,执行shell脚本是通过Java的Runtime.getRuntime().exec()方法来调用,这种方式在Java虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建的进程开销也非常可观。在Java虚拟机中执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,在用这个新的进程去执行外部命令,最后再退出进程。如果频繁执行这个操作,系统的消耗会很大,不仅是CPU,内存负担也会很重。更推荐使用Java的API去获取这些系统信息

服务器JVM程序崩溃

一个基于B/S的MIS系统,硬件为两台2个CPU、8GB内存的HP系统,服务器是WebLogic 9.2。正常运行一段时间后,发现运行期间频繁出现集群节点的虚拟机进程自动关闭现象,产生一个hs_err_pid###.log文件后,进程就消失,两台物理机里的每个节点都出现过进程崩溃的现象。在日志中分析发现,在每个节点的虚拟机进程崩溃前不久,都发生过远端断开连接异常,通过系统管理员得知最近与一个OA门户做了集成,MIS系统工作流的待办事项变化时,通过Web服务器通知OA门户系统,将代办事项的变化同步到OA门户中。通过SoapUI测试发现同步代办事项的Web服务,调用后需要3分钟才能返回,切都是响应连接中断

由于MIS系统用户多,待办事项变化快,为不被OA系统速度拖累,使用异步方式调用Web服务,但由于两边服务速度的完全不对等,时间越长累积越多Web服务没有调用完成,导致等待的线程和Socket连接越来多,最终超过虚拟机的承受能力是的虚拟机进程崩溃。解决方式:采用消息队列方式实现

不恰当数据结构导致内存占用过大

一个后台RPC服务器,使用64位虚拟机,内存配置为-Xms4g -Xmx8g -Xmn1g,使用ParNew+CMS的收集器组合,平时对外服务的Minor GC时间约在30毫秒内,满足性能需求。但是在业务上需要每10分钟加载一个约80MB的数据文件到内存进行数据分析,这些数据会在内存中形成超过100完个HashMap<Long, Long> Entry,在这段时间里Minor GC会造成超过500毫秒的停顿,这个停顿时间是客户无法忍受的。该项目平时Minor GC时间很短,新生代的绝大部分对象都是可清除的,在Minor GC之后Eden和Survivor基本上处于完全空闲的状态。在分析数据文件期间,800MB的Eden空间很快被填满引发GC,但Minor GC之后,新生代中绝大部分对象依然是存活的。ParNew收集器使用的是复制算法,这个算法的高效是建立在大部分对象"朝生夕灭"的特性上,如果存活对象过多,把这些对象复制到Survivor并维持这些对象引用的正确就成为一个沉重的负担,导致GC暂停时间明显变长

仅从GC调优的角度去解决问题,可以考虑将Survivor空间去掉,即加入参数-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0或者-XX:+AlwaysTenure,让新生代中存活的对象在第一次Minor GC后立即进入老年代,等到Major GC的时候在清理。但这种方式治标不治本,根本的原因还是使用HashMap<Long, Long>结构来存储数据文件空间效率太低

在HashMap<Long, Long>结构中,只有key和Value所存放的两个长整型数据是有效的数据,共16B(2*8B)。这两个长整型数据包装成java.lang.Long对象之后,就分别具有8B的MarkWord、8B的Klass指针,在加上8B存储数据的long值。在这两个Long对象组成Map.Entry之后,又多了16B的对象头,然后一个8B的next字段和4B的int类型的hash字段,为了对齐还需要加4B的空白填充,最后还有HashMap中对这个Entry的8B的引用,这样增加两个长整型数字,实际消耗的内存为(Long(24B) * 2) + Entry(32) + HashMap Ref(8B) = 88B,空间利用率为16B/88B=18%

由Windows虚拟内存导致的长时间停顿

在一个带心跳检测功能的GUI桌面程序,每15秒会发送一次心跳检测信号,如果对方30秒以内都没有信号返回,就认为对方程序的连接已经断开。程序上线后发现心跳检测有误报的概率,通过日志发现误报是因为程序偶尔出现间隔约一分钟左右的时间完全无日志输出,处于停顿状态

因为是桌面程序,所需内存并不大(-Xmx256m),所以开始并没有想到是GC导致的程序停顿,但加入参数-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -Xloggc: gclog.log后,从GC日志文件中确认停顿确实是GC导致的,大部分GC时间都控制在100毫秒以内,但偶尔会出现一次接近1分钟的GC

从GC日志中找到长时间停顿的具体日志信息,添加-XX:+PrintReferenceGC参数,从日志中看到,真正执行GC动作的时间不是很长,但从准备开始GC,到真正开始GC之间所消耗的时间却占了绝大部分

除GC日志之外,还观察到这个GUI程序内存变化的一个特点,当它最小化的时候,资源管理中显示的占用内存大幅度减少,但虚拟内存则没有变化,怀疑程序在最小化时工作内存被自动交换到磁盘的页面文件中了,这样发生GC时就有可能因为恢复页面文件的操作而导致不正常的GC停顿

在Java的GUI程序中要避免这种现象,可以加入参数"-Dsun.awt.keepWorkingSetOnMinimize=true"来解决。这个参数在许多AWT的程序上都有应用,例如JDK自带的Visual VM,用于保证程序在恢复最小化时能够立即响应

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值