JVM调优实战问题详解

3.调优分析与实战

  1. 大内存硬件上的程序部署策略:
    目前单体应用在较大内存的硬件主要的部署方式有2种:1.通过一个单独的虚拟机实例来管理大量的堆内存;2.同时使用若干个虚拟机,建立逻辑集群来利用硬件资源。
    如果使用单个Java虚拟机实例来管理大内存,需要考虑下面的问题:
    1.回收大块堆内存而导致的长时间停顿,自从G1收集器的出现,增量回收之后,但要到ZGC和Shenandoah收集器成熟后才得到相对彻底的解决。
    2.大内存必须有64位虚拟机的支持,但由于压缩指针、处理器缓存行容量等因素,64位性能普遍低于32位虚拟机
    3.必须保证应用程序足够稳定,因为这种大型单体应用要是发生了堆内存溢出,几乎无法产生堆转储快照,就算了成功了也难以分析(因为快照文件太大),只能应用JMC这种能够在生产环境中进行的运维工具。
    4.相同的程序在64位消耗的内存一般比32位虚拟机要大,这是因为指针膨胀,数据类型对齐补白导致的,可以开启(默认开启)压缩指针来缓解。
    鉴于上诉问题,可以选择使用若干个虚拟机建立逻辑集群来利用硬件资源,做法是在一台机器上启动多个应有服务器进程,为每个服务器进程分配不同端口,然后在前端建立一个负载均衡器,以反向代理的方式来分配访问请求。目的仅仅是为了尽可能利用硬件资源,所以使用无Session复制的亲和式集群(也就是均衡器按一定的规则算法将一个固定的用户请求永远分配到一个固定的集群节点进行处理即可)PS:可能会遇到1.节点竞争全局的资源,最典型的就是磁盘竞争,各个节点如果同时访问某个磁盘文件的话,很容易导致I/O异常。2.很难最高效率地利用某些连接池,一般每个节点都有自己独立的连接池,导致一些已经满了,另一些仍有较多空余。3.如果使用32位虚拟机作为集群节点的话,会受到最高4GB(2的32次幂的限制)。4.大量使用本地缓存(HashMap)的应用,在集群中会造成较大内存浪费,因为每个逻辑节点上都有一份缓存,可以考虑改为集中式缓存。
    总结:1.特征:将32位系统升级到64位系统,内存也增大,反而出现了停顿问题,尝试将堆内存重新缩小,可以避免长时间停顿。说明这是虚拟机的问题,分配的内存过大,导致完成一次垃圾收集会长时间停顿。2.解决方式:调整为建立5个32位JDK的逻辑集群,每个进程按2GB计算,占用10GB。另外建立一个Apache服务作为前端均衡代理,考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,处理器资源敏感度低,因此改为CMS收集器进行垃圾回收。

  2. 集群间同步导致的内存溢出
    1.描述:构建了一个6个节点的亲和式集群,节点没有进行Session同步,但是有一些需求要实现部分数据在各个节点间共享,读写频繁,竞争很激烈,性能影响较大,后面是有JBossCache构建了一个全局缓存。启用后,正常使用了一段时间,最近不定期出现多次的内存溢出问题。
    2.原因:由于信息有传输失败需要重发的可能性,在确认所有注册在GMS(Group Membership Service)的节点都收到正确的信息前,发送的信息必须在内存中保留。在服务端中有一个负责安全校验的全局过滤器,每当接收到请求时,均会更新一个最后操作时间,并同步到所有的节点去。在服务使用过程中,往往一个页面会产生很多次请求,因此这个过滤器导致集群各个节点之间网络交互非常频繁,当网络情况不能满足传输要求时。重发数据在内存中不断堆积,很快就产生了内存溢出。
    3.解决方式:这一类被集群共享的数据要使用类似JBossCache这种非集中式的集群缓存来同步的话,可以允许读操作频繁,因为数据在本地内存由一份副本,但不应当有过于频繁的写操作,会带来很大的网络同步的开销。

  3. 堆外内存导致的溢出错误
    1.描述:基于B/S的电子考试系统,测试期间发现服务端不定时抛出内存溢出异常。尝试把堆内存调到最大,基本没效果,加入-XX:+HeapDumpOnOutOfMemoryError参数,也没有任何反应。只好挂着jstat观察,发现垃圾并不频繁,内存很稳定,但就是照样不停抛出OOM。
    2.原因:Direct Memory耗用的内存不算入堆内存内,直接内存只能等待Full GC清除。
    3.解决方式:先捕获到异常,再在Catch块里面通过System.gc命令来触发收集或者通过-XX:MaxDirectMemorySize增大直接内存的大小。

  4. 外部命令导致系统缓慢
    1.描述:系统在做大并发压力测试时,发现请求响应时间比较慢,通过工具发现处理使用率很高,但占用大多数并不是该应用本身。
    2.原因:通过脚本发现消耗处理器资源最多的是"fork"系统调用,fork系统调用是Linux用来产生新进程的,在虚拟机中,代码只会创建新的线程,不应当有进程的产生。答案:每个用户请求的处理都需要执行一个外部Shell脚本来获得系统的一些信息。执行这个Shell的脚本是通过Runtime.getRuntime().exec()来调用的。这个操作非常消耗资源,即使外部命令本身能很快执行完毕,但频繁调用时创建进程的开销也很大。(虚拟机首先复制一个和当前虚拟机一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程)
    3.解决办法:去掉这个Shell脚本执行的语句,改用Java的API去获取这些信息。

  5. 服务器虚拟机进程崩溃
    1.描述:系统正常运行一段时间后,发现在运行期间频繁出现集群节点的虚拟机进程自动关闭的现象。从系统日志注意到,每个节点的虚拟机进程在崩溃之前,都发生过大量相同的异常。
    2.原因:这是一个远端断开连接的异常,系统最近与一个OA门户做了集成。在待办事项变化时,要通过Web服务通知OA门户系统,把待办事项的变化同步到OA门户之中。由于MIS系统的用户多,待办事项变化很快,为了不被OA系统速度拖累,使用异步的方式调用Web服务,但由于两边服务速度完全不对等,时间越长就累积越多Web服务没有调用完成,导致在等待的线程和Socket连接越来越多,最终导致崩溃。
    3.解决办法:通知OA门户方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列后,系统恢复正常。

  6. 不恰当数据结构导致内存占用过大
    1.描述:一个使用ParNew加CMS的收集器组合的后台RPC服务器,平时对外服务的Minor GC完全可以接受,但业务上需要每10分钟加载一个约80MB的数据文件到内存进行数据分析,这些数据会在内存中形成超过100万个HashMap<Long,Long>Entry,在这段时间里面Minor GC就会造成超过500ms的停顿。
    2.原因:平时Minor GC时间很短,原因是新生代的绝大部分对象都是可清除的,垃圾收集后Eden和Survivor基本上处于完全空闲的状态。但是在分析数据文件期间,Eden空间很快被填满引发垃圾收集,ParNew使用的是复制算法,这个算法的高效是建立在对象朝生夕灭的,如果存活对象过多,把这些对象复制到Survivor
    并维持这些对象引用的正确性就成为一个沉重的负担,因此导致垃圾收集的暂停时间明显变长。
    3.解决办法:1.如果不修改程序,仅从GC调优的角度去解决这个问题,可以考虑把Survivor空间去掉,让新生代存活的对象在第一次Minor GC后立即进入老年代,等到Major GC的时候再去清理它们(Major GC目前只有CMS收集器有)。2.根本原因是HashMap<Long,Long>来储存数据文件空间效率太低了。只有Key和Value所存放的2个长整型数据是有效数据,为了增加2个长整型数字,实际耗费的内存增加了好几倍。

  7. 由Windows虚拟内存导致的长时间停顿
    1.描述:一个带心跳检测功能的GUI桌面程序,程序上线后发现心跳检测有误报的可能,查询日志发现误报的原因是程序会偶尔出现1分钟的时间完全无日志输出,处于停顿状态。
    2.原因:由于是桌面程序,所需的内存并不大,所以开始并没有想到是垃圾收集导致的程序停顿,但是从日志文件中确认了停顿确实是由垃圾收集导致的。大部分收集时间都控制在100ms以内,但偶尔就出现一次接近1分钟的长时间收集过程。 除了日志外,还观察到这个GUI程序内存变化的一个特点,当它最小化的时候,资源管理中显示的占用内存大幅度减小,但是虚拟内存则没有变化,因此怀疑程序在最小化时它的工作内存被自动交换到磁盘的页面文件之中了,这样发生垃圾收集时就可能因为恢复页面文件的操作导致不正常的垃圾收集停顿。
    3.解决办法:在Java的GUI程序中要避免这种现象,可以加入参数keepWorkingSetOnMinimize=true来解决。例如VisualVM,启动配置文件就有这个参数,保证程序在恢复最小化时能够立即响应。

  8. 由安全点导致长时间停顿
    1.描述:有一个较大的承担公共计算任务的离线HBase集群,运行在JDK8上,使用G1收集器,由于集群读写压力较大,而离线分析任务对延迟不会特别敏感,所以将-XX:MaxGCPauseMillis参数设置到了500ms。不过运行一段时间后发现垃圾收集的停顿经常达到3秒以上,而实际收集器进行回收的动作就只占其中的几百ms。
    2.原因:日志显示这次垃圾收集一共花费了0.14s,但其中用户线程却足足停顿了2.26ms。所以先加入参数-XX:+PrintSafepointStatistics和-XX:PrintSafepointStatisticsCount=1去查看安全点日志。
    日志显示当前虚拟机的操作是等待所以用户线程进入到安全点,但是有2个线特别慢,导致发生了很长时间的自旋等待。
    3.解决办法:第一步是把这2个特别慢的线程给找出来,添加-XX:+SafepointTimeout和-XX:SafepointTimeoutDelay=2000,让虚拟机在等到线程进入安全点的时间超过2000ms就认定为超时,就会输出导致问题的线程名称。 安全点是以"是否具有让程序长时间执行的特征"为原则进行选定的,所以方法调用、循环跳转、异常跳转这些位置都可能会设置有安全点。但是HotSpot虚拟机为了避免安全点过多带来沉重的负担,对循环还有一项优化措施:认为循环次数较少,执行时间应该也不会太长,所以使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环。不会被放置安全点。通常这个优化措施是可行的,但循环执行的时间不单单由次数决定,如果循环体单次执行特别慢,即使是可数循环也可能会耗费很多的时间。 HotSpot提供了-XX:+UseCountedLoopSafepoints参数去强制在可数循环中也放置安全点。
    最终查明导致这个问题是HBase中一个连接超时清理的函数。把循环索引的数据类型从int改为long即可。

4.Eclipse运行速度调优

  1. 编译时间是指虚拟机的即时编译器编译热点代码的耗时,HotSpot虚拟机内置了2个即时编译器,如果一段方法被调用次数到达一定程度,就会被判定为热代码交给即时编译器即时编译为本地代码,提高运行速度。所以只要Java程序代码编写没问题,随着运行时间增长,代码被编译得越来越彻底,运行速度应当是越运行越快的。PS:Java的运行期编译的一大缺点就是它进行编译需要消耗机器的计算资源,影响程序的运行时间。
  2. 新生代垃圾收集频繁发生,很明显是由于虚拟机分配给新生代的空间太小导致。完全有必要使用-Xmn参数手动调整新生代的大小。
  3. Eclipse启动时Full GC大多数是由于老年代容量以及永久代扩展而导致的,为了避免这些扩展所带来的性能浪费,可以把-Xms和-XX:PermSize参数值设置为-Xmx和-XX:MaxPermSize参数值一样,这样就可以强制虚拟机在启动的时候就把老年代和永久代的容量固定下来,避免运行时自动扩展。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值