Java调优案例分析与实战

本章内容将着重考虑如何在应用部署层面去解决问题,其中有不少案例中的问题的确可以在设计和开 发阶段就先行避免,但这并不是本篇文章要讨论的话题。我们主要探讨如何在不改变已有软硬件版本和规格的前提下, 调整部署和配置策略去解决或者缓解问题。

本篇文章结合着【深入理解Java虚拟机】一书当中整理了本篇博客。

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

1.1、问题背景

一个15万PV/日(pv代表浏览量)左右的在线文档类 型网站,使用的是32位操作系统,给HotSpot虚拟机 只分配了1.5GB的堆内存,当时用户确实感觉到使用网站比较缓慢。第一反应是加大内存。

1.2、升级硬件

更换了硬件系统,服务器的硬件为四路志强处理器、16GB物理内存,操作系统为64位 CentOS 5.4,Resin作为Web服务器。整个服务器暂时没有部署别的应用,软件版本选用的是64位的JDK 5,使用-Xmx和-Xms参数将Java堆大小固定在12GB。使用一段时间后发现服务器的运行效果十分不 理想,网站经常不定期出现长时间失去响应

分析原因:监控服务器运行状况后发现网站失去响应是由垃圾收集停顿所导致的,回收12GB的Java堆,一次Full GC的停顿时间就高达14秒。由于程序设计的原因,访问文档时会把文档从磁盘提取到内存中导致内 存中出现很多由文档序列化产生的大对象这些大对象大多在分配时就直接进入了老年代没有在 Minor GC中被清理掉。这种情况下即使有12GB的堆,内存也很快会被消耗殆尽,由此导致每隔几分 钟出现十几秒的停顿

加大内存后产生的问题:本来想的是通过升级硬件来改善访问速度,却反而出现了停顿问题,尝试 过将Java堆分配的内存重新缩小到1.5GB或者2GB,这样的确可以避免长时间停顿,但是在硬件上的投 资就显得非常浪费。

1.3、应用部署

每一款Java虚拟机中的每一款垃圾收集器都有自己的应用目标与最适合的应用场景,如果在特定 场景中选择了不恰当的配置和部署方式,自然会事倍功半。目前单体应用在较大内存的硬件上主要的 部署方式有两种:

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

此案例中的管理员采用了第一种部署方式。对于用户交互性强、对停顿时间敏感、内存又较大的 系统,并不是一定要使用Shenandoah、ZGC这些明确以控制延迟为目标的垃圾收集器才能解决问题 (当然不可否认,如果情况允许的话,这是最值得考虑的方案)。

使用Parallel Scavenge/Old收集器(Java默认的收集器),并 且给Java虚拟机分配较大的堆内存也是有很多运行得很成功的案例的,但前提是必须把应用的Full GC 频率控制得足够低至少要低到不会在用户使用过程中发生,譬如十几个小时乃至一整天都不出现一 次Full GC,这样可以通过在深夜执行定时任务的方式触发Full GC甚至是自动重启应用服务器来保持内 存可用空间在一个稳定的水平

如何控制Full GC控制Full GC频率的关键是老年代的相对稳定,这主要取决于应用中绝大多数对象能否符合“朝生 夕灭”的原则,即大多数对象的生存时间不应当太长,尤其是不能有成批量的、长生存时间的大对象产 生,这样才能保障老年代空间的稳定。

在许多网站和B/S形式的应用里,多数对象的生存周期都应该是请求级或者页面级的,会话级和全 局级的长生命对象相对较少。只要代码写得合理,实现在超大堆中正常使用没有Full GC应当并不困 难,这样的话,使用超大堆内存时,应用响应速度才可能会有所保证。

单体应用需要注意的:如果读者计划使用 单个Java虚拟机实例来管理大内存,还需要考虑下面可能面临的问题:

  1. 回收大块堆内存而导致的长时间停顿,自从G1收集器的出现,增量回收得到比较好的应用, 这个问题有所缓解,但要到ZGC和Shenandoah收集器成熟之后才得到相对彻底地解决。

  2. 必须保证应用程序足够稳定,因为这种大型单体应用要是发生了堆内存溢出,几乎无法产生堆转 储快照(要产生十几GB乃至更大的快照文件),哪怕成功生成了快照也难以进行分析;如果确实出了 问题要进行诊断,可能就必须应用JMC这种能够在生产环境中进行的运维工具。

  3. 相同的程序在64位虚拟机中消耗的内存一般比32位虚拟机要大,这是由于指针膨胀,以及数据类 型对齐补白等因素导致的,可以开启(默认即开启)压缩指针功能来缓解。

什么是增量回收?

通常的GC算法很繁重,一旦GC开始执行,我们原本该进行的程序就被迫停止。也就是说,繁重的GC原本是辅助程序变成了主程序,而本该执行的主程序变成了辅程序。这就是臭名昭著的停止型GC,英文名称很酷叫:Stop the world GC。针对繁重的GC算法,人们提出增量式垃圾回收算法。增量:incremental,意味着慢慢变化,也就是说增量式垃圾回收算法是慢慢回收,一点一点与主程序交替执行而不是直接霸占主程序的运行时间。

集群部署:一台物理机器上启动多个应用服务器进程,为每个服 务器进程分配不同端口,然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。

考虑到我们在一台物理机器上建立逻辑集群的目的仅仅是尽可能利用硬件资源,并不是要按职 责、按领域做应用拆分,也不需要考虑状态保留、热转移之类的高可用性需求,不需要保证每个虚拟 机进程有绝对准确的均衡负载,因此使用无Session复制的亲合式集群是一个相当合适的选择。仅仅需 要保障集群具备亲合性,也就是均衡器按一定的规则算法(譬如根据Session ID分配)将一个固定的用 户请求永远分配到一个固定的集群节点进行处理即可,这样程序开发阶段就几乎不必为集群环境做任 何特别的考虑

集群部署缺点

  1. 节点竞争全局的资源,最典型的就是磁盘竞争,各个节点如果同时访问某个磁盘文件的话(尤其 是并发写操作容易出现问题),很容易导致I/O异常。
  2. 很难最高效率地利用某些资源池,譬如连接池,一般都是在各个节点建立自己独立的连接池,这 样有可能导致一些节点的连接池已经满了,而另外一些节点仍有较多空余。尽管可以使用集中式的 JNDI来解决,但这个方案有一定复杂性并且可能带来额外的性能代价。
  3. 如果使用32位Java虚拟机作为集群节点的话,各个节点仍然不可避免地受到32位的内存限制,在 32位Windows平台中每个进程只能使用2GB的内存,考虑到堆以外的内存开销,堆最多一般只能开到 1.5GB。在某些Linux或UNIX系统(如Solaris)中,可以提升到3GB乃至接近4GB的内存,但32位中仍 然受最高4GB(2的32次幂)内存的限制。
  4. 大量使用本地缓存(如大量使用HashMap作为K/V缓存)的应用,在逻辑集群中会造成较大的内 存浪费,因为每个逻辑节点上都有一份缓存,这时候可以考虑把本地缓存改为集中式缓存

1.4、最终解决方案

  1. 集群部署:没有选择升级JDK版本,而 是调整为建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了 10GB内存。另外建立一个Apache服务作为前端均衡代理作为访问门户。

  2. 切换垃圾收集器:考虑到用户对响应速度比较关 心,并且文档服务的主要压力集中在磁盘和内存访问,处理器资源敏感度较低,因此改为CMS收集器 进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比起硬件升级前有较大提升。

两种收集器追求的目标不同 :

  1. CMS收集器是以获取最短回收停顿时间为目标的收集器
  2. Parallel Scavenge收集器(jdk默认的收集器)目标是达到一个可控制的吞吐 量

二、集群间同步导致的内存溢出

2.1、问题背景

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

什么是MIS系统?

所谓MIS(管理信息系统–Management Information System)系统,主要指的是进行日常事务操作的系统。这种系统主要用于管理需要的记录,并对记录数据进行相关处理,将处理的信息及时反映给管理者的一套网络管理系统。

什么是亲合式集群?

给session创造粘性,意思是让用户每次都访问的同一个应用服务器,这样就要在前端服务器apache中记录下,用户首次访问的是哪个tomcat,将用户后面发送的请求都发送到这个tomcat上去,这种方式的集群称为亲合式集群。

什么是WebLogic ?

通俗的讲weblogci是一种web容器,如果你知道apache,tomcat,iis你就应该知道,就是把我们开发的java应用程序运行起来,并提供服务的一种程序。

什么是JBossCache?

JBoss Cache 是一个分布式的企业应用缓存,它的目的是为企业应用分布式和集群提供解决方案,通过缓存需要频繁访问的 Java 对象,提高应用的可用性并大幅度提升应用的整体性能。JBoss Cache是一个单独的产品,你可以单独使用或将其部署注册于中间件平台当作服务来使用。JBoss Cache也是一个Java类库,你也可以扩展将其整合到你的应用中使用。

2.2、排查原因

在内存溢出异常不出现的时候,服务内存回收状况一直正常,每次内存回收后都能恢复到一个稳 定的可用空间。开始怀疑是程序某些不常用的代码路径中存在内存泄漏,但管理员反映最近程序并未 更新、升级过,也没有进行什么特别操作。只好让服务带着
-XX:+HeapDumpOnOutOfMemoryError 参数运行了一段时间。在最近一次溢出之后,管理员发回了堆转储快照,发现里面存在着大量的 org.jgroups.protocols.pbcast.NAKACK对象。

2.3、分析原因

JBossCache是基于自家的JGroups进行集群间的数据通信,JGroups使用协议栈的方式来实现收发 数据包的各种所需特性自由组合,数据包接收和发送时要经过每层协议栈的up()和down()方法,其中 的NAKACK栈用于保障各个包的有效顺序以及重发。

由于信息有传输失败需要重发的可能性,在确认所有注册在GMS(Group Membership Service)的 节点都收到正确的信息前,发送的信息必须在内存中保留。而此MIS的服务端中有一个负责安全校验 的全局过滤器每当接收到请求时,均会更新一次最后操作时间,并且将这个时间同步到所有的节点 中去,使得一个用户在一段时间内不能在多台机器上重复登录。在服务使用过程中,往往一个页面会 产生数次乃至数十次的请求,因此这个过滤器导致集群各个节点之间网络交互非常频繁。当网络情况不能满足传输要求时,重发数据在内存中不断堆积,很快就产生了内存溢出

2.4、最终结果

这个案例中的问题,既有JBossCache的缺陷,也有MIS系统实现方式上的缺陷。JBoss-Cache官方 的邮件讨论组中讨论过很多次类似的内存溢出异常问题,据说后续版本也有了改进。

而更重要的缺陷 是,这一类被集群共享的数据要使用类似JBossCache这种非集中式的集群缓存来同步的话,可以允许 读操作频繁,因为数据在本地内存有一份副本,读取的动作不会耗费多少资源,但不应当有过于频繁 的写操作,会带来很大的网络同步的开销。

三、堆外内存导致的溢出错误

3.1、问题背景

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

测试期间发现服务端不定时抛出内存溢出异常,服务不一定每次都出现异常,但假如正式考试时 崩溃一次,那估计整场电子考试都会乱套。网站管理员尝试过把堆内存调到最大,32位系统最多到 1.6GB基本无法再加大了,而且开大了基本没效果,抛出内存溢出异常好像还更加频繁。加入-XX:+HeapDumpOnOutOfMemoryError参数,居然也没有任何反应,抛出内存溢出异常时什么文件都没有 产生。无奈之下只好挂着jstat紧盯屏幕,发现垃圾收集并不频繁,Eden区、Survivor区、老年代以及方 法区的内存全部都很稳定,压力并不大,但就是照样不停抛出内存溢出异常。最后,在内存溢出后从 系统日志中找到异常堆栈如下图所示。
在这里插入图片描述

3.2、排查原因

我们 知道操作系统对每个进程能管理的内存是有限制的,这台服务器使用的32位Windows平台的限制是 2GB,其中划了1.6GB给Java堆,而Direct Memory(直接内存)耗用的内存并不算入这1.6GB的堆之内,因此它最大 也只能在剩余的0.4GB空间中再分出一部分而已。在此应用中导致溢出的关键是垃圾收集进行时,虚 拟机虽然会对直接内存进行回收,但是直接内存却不能像新生代、老年代那样,发现空间不足了就主 动通知收集器进行垃圾回收,它只能等待老年代满后Full GC出现后,“顺便”帮它清理掉内存的废弃对 象否则就不得不一直等到抛出内存溢出异常时,先捕获到异常,再在Catch块里面通过System.gc()命 令来触发垃圾收集。但如果Java虚拟机再打开了-XX:+DisableExplicitGC开关,禁止了人工触发垃圾 收集的话,那就只能眼睁睁看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了。而本案 例中使用的CometD 1.1.1框架,正好有大量的NIO操作需要使用到直接内存

从实践经验的角度出发,在处理小内存或者32位的应用问题时,除了Java堆和方法区之外,我们 注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制:

  1. 直接内存:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出
    OutOfMemoryError或 者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. 虚拟机和垃圾收集器:虚拟机、垃圾收集器的工作也是要消耗一定数量的内存的。

四、外部命令导致系统缓慢

4.1、问题背景

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

4.2、排查原因

通过Solaris 10的dtrace脚本可以查看当前情况下哪些系统调用花费了最多的处理器资源,dtrace运 行后发现最消耗处理器资源的竟然是“fork”系统调用。众所周知,“fork”系统调用是Linux用来产生新进 程的,在Java虚拟机中,用户编写的Java代码通常最多只会创建新的线程,不应当有进程的产生,这又 是个相当不正常的现象。

通过联系该系统的开发人员,最终找到了答案:每个用户请求的处理都需要执行一个外部Shell脚 本来获得系统的一些信息。执行这个Shell脚本是通过Java的Runtime.getRuntime().exec()方法来调用的。 这种调用方式可以达到执行Shell脚本的目的,但是它在Java虚拟机中是非常消耗资源的操作,即使外 部命令本身能很快执行完毕,频繁调用时创建进程的开销也会非常可观。Java虚拟机执行这个命令的 过程是首先复制一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最 后再退出这个进程。如果频繁执行这个操作,系统的消耗必然会很大,而且不仅是处理器消耗,内存 负担也很重。

4.3、最终结果

去掉这个Shell脚本执行的语句,改为使用Java的API去获取这些信息后,系统很快恢 复了正常。

五、服务器虚拟机进程崩溃

5.1、问题背景

一个基于B/S的MIS系统,硬件为两台双路处理器、8GB内存的HP系统,服务器是WebLogic 9.2(与第二个案例中那套是同一个系统)。

正常运行一段时间后,最近发现在运行期间频繁出现集群 节点的虚拟机进程自动关闭的现象,留下了一个hs_err_pid###.log文件后,虚拟机进程就消失了,两台 物理机器里的每个节点都出现过进程崩溃的现象。从系统日志中注意到,每个节点的虚拟机进程在崩 溃之前,都发生过大量相同的异常,如下所示。
在这里插入图片描述

5.2、排查原因

这是一个远端断开连接的异常,通过系统管理员了解到系统最近与一个OA门户做了集成,在MIS 系统工作流的待办事项变化时,要通过Web服务通知OA门户系统,把待办事项的变化同步到OA门户 之中。通过SoapUI测试了一下同步待办事项的几个Web服务,发现调用后竟然需要长达3分钟才能返 回,并且返回结果都是超时导致的连接中断。

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

5.3、最终结果

通知OA门户 方修复无法使用的集成接口,并将异步调用改为生产者/消费者模式的消息队列实现后,系统恢复正 常。

六、服务器虚拟机进程崩溃

6.1、问题背景

一个后台RPC服务器,使用64位Java虚拟机,内存配置为-Xms4g-Xmx8g-Xmn1g,使用ParNew加 CMS的收集器组合。平时对外服务的Minor GC时间约在30毫秒以内,完全可以接受。但业务上需要每 10分钟加载一个约800MB的数据文件到内存进行数据分析,这些数据会在内存中形成超过100万个 HashMap<Long,Long>Entry,在这段时间里面Minor GC就会造成超过500毫秒的停顿,对于这种长度 的停顿时间就接受不了了。

6.2、排查原因

具体情况如下面的收集器日志所示。
在这里插入图片描述

观察这个案例的日志,平时Minor GC时间很短,原因是新生代的绝大部分对象都是可清除的,在 Minor GC之后Eden和Survivor基本上处于完全空闲的状态。但是在分析数据文件期间,800MB的Eden 空间很快被填满引发垃圾收集,但Minor GC之后,新生代中绝大部分对象依然是存活的。我们知道 ParNew收集器使用的是复制算法,这个算法的高效是建立在大部分对象都“朝生夕灭”的特性上的,如 果存活对象过多,把这些对象复制到Survivor并维持这些对象引用的正确性就成为一个沉重的负担,因 此导致垃圾收集的暂停时间明显变长。

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

6.3、最终结果

我们具体分析一下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)+HashMap Ref(8byte)=88byte,空间效率为有效数据除以全部内存空间,即16字节/88字节=18%,这确实太低了。

采用其他数据结构存储数据,提高内存利用率。

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

7.1、问题背景

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

7.2、排查原因

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

在这里插入图片描述

从收集器日志中找到长时间停顿的具体日志信息(再添加了-XX:+PrintReferenceGC参数),找 到的日志片段如下所示。从日志中看到,真正执行垃圾收集动作的时间不是很长,但从准备开始收 集,到真正开始收集之间所消耗的时间却占了绝大部分。

在这里插入图片描述

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

7.3、最终结果

在Java的GUI程序中要避免这种现象,可以加入参数“
-Dsun.awt.keepWorkingSetOnMinimize=true”来解决。这个参数在许多AWT的程序上都有应用,例如 JDK(曾经)自带的VisualVM,启动配置文件中就有这个参数,保证程序在恢复最小化时能够立即响 应。在这个案例中加入该参数,问题马上得到解决。

八、由安全点导致长时间停顿

8.1、问题背景

有一个比较大的承担公共计算任务的离线HBase集群,运行在JDK 8上,使用G1收集器。每天都 有大量的MapReduce或Spark离线分析任务对其进行访问,同时有很多其他在线集群Replication过来的 数据写入,因为集群读写压力较大,而离线分析任务对延迟又不会特别敏感,所以将-XX:MaxGCPauseMillis(表示每次GC最大的停顿毫秒数)参数设置到了500毫秒。不过运行一段时间后发现垃圾收集的停顿经常达到3秒以 上,而且实际垃圾收集器进行回收的动作就只占其中的几百毫秒

8.2、排查原因

现象如以下日志所示。
在这里插入图片描述
考虑到不是所有读者都了解计算机体系和操作系统原理,笔者先解释一下user、sys、real这三个时 间的概念:

  1. user:进程执行用户态代码所耗费的处理器时间。
  2. sys:进程执行核心态代码所耗费的处理器时间。
  3. real:执行动作从开始到结束耗费的时钟时间。

请注意,前面两个是处理器时间,而最后一个是时钟时间,它们的区别是处理器时间代表的是线 程占用处理器一个核心的耗时计数而时钟时间就是现实世界中的时间计数。如果是单核单线程的场 景下,这两者可以认为是等价的,但如果是多核环境下,同一个时钟时间内有多少处理器核心正在工 作,就会有多少倍的处理器时间被消耗和记录下来。

在垃圾收集调优时,我们主要依据real时间为目标来优化程序,因为最终用户只关心发出请求到得 到响应所花费的时间,也就是响应速度,而不太关心程序到底使用了多少个线程或者处理器来完成任 务

日志显示这次垃圾收集一共花费了0.14秒,但其中用户线程却足足停顿了有2.26秒,两者差距已经 远远超出了正常的TTSP(Time To Safepoint)耗时的范畴。所以先加入参数-XX:+PrintSafepointStatistics-XX:PrintSafepointStatisticsCount=1去查看安全点日志,具体如下所示:

在这里插入图片描述
日志显示当前虚拟机的操作(VM Operation,VMOP)是等待所有用户线程进入到安全点,但是 有两个线程特别慢,导致发生了很长时间的自旋等待。日志中的2255毫秒自旋(Spin)时间就是指由 于部分线程已经走到了安全点,但还有一些特别慢的线程并没有到,所以垃圾收集线程无法开始工 作,只能空转(自旋)等待

解决问题的第一步是把这两个特别慢的线程给找出来,这个倒不困难,添加
-XX:+SafepointTimeout-XX:SafepointTimeoutDelay=2000两个参数,让虚拟机在等到线程进入安全点的 时间超过2000毫秒时就认定为超时,这样就会输出导致问题的线程名称,得到的日志如下所示:
在这里插入图片描述
从错误日志中顺利得到了导致问题的线程名称为“RpcServer.listener,port=24600”。

8.3、最终结果

当垃圾收集发生时, 如果RpcServer的Listener线程刚好执行到该函数里的可数循环时,则必须等待循环全部跑完才能进入安 全点,此时其他线程也必须一起等着,所以从现象上看就是长时间的停顿。找到了问题,解决起来就 非常简单了,把循环索引的数据类型从int改为long即可,但如果不具备安全点和垃圾收集的知识,这 种问题是很难处理的。

猜你喜欢

转载自blog.csdn.net/weixin_43888891/article/details/124539840