0%

JVM 调优案例分析(整理总结)

JVM 调优案例分析(整理总结)

所有案例均来自《深入理解Java虚拟机》

大内存硬件上程序部署策略

5.2.1案例:

案例中的网站是在线文档类型的,15万PV/日左右,原本的服务器使用HotSpot虚拟机,分配了1.5GB的堆内存。因为访问速度缓慢,网站服务器最近做了升级,服务器的硬件为四代志强处理器、16GB物理内存,操作系统为64位CentOS 5.4,Resin作为Web服务器,升级服务器后通过-Xmx和-Xms给Java堆内存固定在12GB。

在升级服务器后发现网站使用起来并不让人满意,经常会出现长时间的失去响应,使用体验还不如升级之前。监控服务器运行状况后,发现网站失去响应是由垃圾收集停顿所导致的,12GB的堆内存,进行一次FULL GC需要14秒。因为业务需要,

在查看文档时需要从磁盘读取文件到内存,内存会很快被这些文件的序列化大对象装满,并且这些对象因为体积较大,会直接分配在老年代,Minor GC无法清理这些大对象,这就导致序列化大对象不断的叠加,最后占满12GB触发Full GC,stop-the-world网站失去响应。

后续把Java堆内存缩小到1.5GB或者2GB可以避免长时间停顿,但是这样做对网站的使用体验并没有提升,而且会浪费硬件资源。

不同的Java虚拟机有不同的特性,这些特性可能适合不同的应用场景。在较大内存的硬件上主要有两种部署方式:

  1. 通过单独的Java虚拟机程序来管理大量的堆内存
  2. 同时使用若干个Java虚拟机,建立集群利用硬件资源

每种方式都会遇到不同的问题,整体看来较为方便且简单的方式是建立多个应用程序,使用nginx等服务器建立负载均衡。

最后的部署方案是调整为建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB内存。另外建立一个Apache服务作为前端均衡代理作为访问门户。考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,处理器资源敏感度较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比起硬件升级前有较大提升。

总结

  1. 要避免频繁出现Full GC,最重要的就是让堆中老年代的对象相对稳定,如果堆中老年代的对象“满了-删,满了-删”,每删一次应用程序都会停止响应
  2. 要避免时间较长的FullGC,如果堆内存较大FullGC时间就会长,应用程序停止响应的时间长,用户体验非常不好
  3. 单体应用配合大内存硬件时,主要问题是避免FullGC。只要代码写的合理,在正常使用时不出现FullGC还是可以的,但是这样对代码质量要求较高。

堆外内存导致的溢出错误

5.2.3案例:

这是一个基于B/S的电子考试系统,为了实现客户端能实时地从服务器端接收考试数据,系统使用了逆向AJAX技术(也称为Comet或者Server Side Push),选用CometD 1.1.1作为服务端推送框架,服务器是Jetty 7.1.4,硬件为一台很普通PC机,Core i5 CPU,4GB内存,运行32位Windows操作系统。(放在现在可能就会用websocket)

测试期间发现服务端经常提示内存溢出,加入-XX:+HeapDumpOnOutOfMemoryError参数没有文件生成,通过jstat查看服务发现Eden区、Survivor区、老年代以及方法区的内存全部都很稳定,压力并不大,但就是照样不停抛出内存溢出异常。考虑到项目使用到了Comet,因为Comet中有大量的NIO操作,这些操作会使用到直接内存,有可能是堆外内存溢出。

我们都知道操作系统给每个进程分配的内存是有限的,这个项目最多能使用2GB内存,1.6GB给Java堆,Direct Memory使用的内存在剩余的0.4GB中。直接内存只会在FullGC时被回收,这就导致了垃圾收集器在老年代不满时不进行FullGC,但此时直接内存已经空间不足,即使这时候堆内存中还有空间,最终随着直接内存是空间被用完提示内存溢出。

总结

  1. 直接内存溢出经常容易被忽略,在使用NIO或者其他涉及直接内存的工具时需要额外注意。
  2. 下面的区域还会占用较多内存
    1. 直接内存:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOf-MemoryError或者OutOfMemoryError:Direct buffer memory。
    2. 线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)或者OutOfMemoryError(如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)。
    3. Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,可能会抛出IOException:Too many open files异常。
    4. JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中,而是占用Java虚拟机的本地方法栈和本地内存的。
    5. 虚拟机和垃圾收集器:虚拟机、垃圾收集器的工作也是要消耗一定数量的内存的。

外部命令导致系统缓慢

5.2.4案例:

一个数字校园应用系统,运行在一台四路处理器的Solaris 10操作系统上,中间件为GlassFish服务器。系统在做大并发压力测试的时候,发现请求响应时间比较慢,通过操作系统的mpstat工具发现处理器使用率很高,但是系统中占用绝大多数处理器资源的程序并不是该应用本身。这是个不正常的现象,通常情况下用户应用的处理器占用率应该占主要地位,才能说明系统是在正常工作。

通过Solaris 10的dtrace脚本可以查看当前情况下哪些系统调用花费了最多的处理器资源,dtrace运
行后发现最消耗处理器资源的竟然是“fork”系统调用。“fork”是用来创建新进程的,正常的Java程序最多创建新的线程,无法创建进程。

最后了解到系统中有一个业务是:每个用户的请求都需要执行一个为外部Shell脚本。执行这个脚本是通过Java的Runtime.getRuntime().exec()方法来调用,JVM执行这个方法会复制(fork)一个和当前虚拟机一样环境的进程,再用这个新的进程去执行外部命令,最后退出这个进程。

总结

在并发测试的时候应用程序因为使用Runtime.getRuntime().exec(),创建了太多新进程,导致系统资源被抢占,原来的程序运行受到影响。

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

5.2.6案例:

一个后台RPC服务器,使用64位Java虚拟机,内存配置为-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空间很快被填满引发垃圾收集,但Minor GC之后,新生代中绝大部分对象依然是存活的。我们知道ParNew收集器使用的是复制算法,这个算法的高效是建立在大部分对象都“朝生夕灭”的特性上的,如果存活对象过多,把这些对象复制到Survivor并维持这些对象引用的正确性就成为一个沉重的负担,因此导致垃圾收集的暂停时间明显变长。

如果不修改程序,仅从GC调优的角度去解决这个问题,可以考虑直接将Survivor空间去掉(加入参数-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0或者-XX:+Always-Tenure),让新生代中存活的对象在第一次Minor GC后立即进入老年代,等到Major GC的时候再去清理它们。这种措施可以治标,但也有很大副作用;治本的方案必须要修改程序,因为这里产生问题的根本原因是用HashMap<Long,Long>结构来存储数据文件空间效率太低了。

我们具体分析一下HashMap空间效率,在HashMap<Long,Long>结构中,只有Key和Value所存放的两个长整型数据是有效数据,共16字节(2×8字节)。这两个长整型数据包装成java.lang.Long对象之后,就分别具有8字节的Mark Word、8字节的Klass指针,再加8字节存储数据的long值。然后这2个Long对象组成Map.Entry之后,又多了16字节的对象头,然后一个8字节的next字段和4字节的int型的hash字段,为了对齐,还必须添加4字节的空白填充,最后还有HashMap中对这个Entry的8字节的引用,这样增加两个长整型数字,实际耗费的内存为(Long(24byte)×2)+Entry(32byte)+HashMapRef(8byte)=88byte,空间效率为有效数据除以全部内存空间,即16字节/88字节=18%,这确实太低了。

总结

这一段没有任何删减,文中的分析过程真的让我很佩服,通过GC日志和ParNew收集算法分析当前程序的停顿原因,又细致的分析了HashMap的数据结构,最终找到需要修改的地方。

如果觉得我的文章对您有用,赏我一包辣条吧!您的支持将鼓励我继续创作!也可以加我微信一起交流学习,折腾点有意思事情。