jvm的stop the world时间过长优化

2024-05-10 21:18

本文主要是介绍jvm的stop the world时间过长优化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

现象:小米有一个比较大的公共离线HBase集群,用户很多,每天有大量的MapReduce或Spark离线分析任务在进行访问,同时有很多其他在线集群Replication过来的数据写入,集群因为读写压力较大,且离线分析任务对延迟不敏感,所以其G1GC的MaxGCPauseMillis设置是500ms。

但是随着时间的推移,我们发现了一个新的现象,线程的STW时间可以到3秒以上,但是实际GC的STW时间却只有几百毫秒!

打印GC日志

  • -XX:+PrintGC

  • -XX:+PrintGCDetails

  • -XX:+PrintGCApplicationStoppedTime

  • -XX:+PrintHeapAtGC

  • -XX:+PrintGCDateStamps

  • -XX:+PrintAdaptiveSizePolicy

  • -XX:+PrintTenuringDistribution

具体的日志log示例如下:[Times: user=1.51 sys=0.67, real=0.14 secs]

2019-06-25T12:12:43.376+0800: 3448319.277: Total time for which application threads were stopped: 2.2645818 seconds, Stopping threads took: xxx seconds

-XX:+PrintGCApplicationStoppedTime会打出类似上面的日志,这个stopped时间是JVM里所有STW的时间,不止GC。我们遇到的现象就是stopped时间远大于上面的GC real耗时,比如GC只耗时0.14秒,但是线程却stopped了2秒多。这个时候大概率就是GC时线程进入Safepoint耗时过长,所以需要2个新的GC参数来打印出Safepoint的情况。

打印Safepoint相关日志

  • -XX:+PrintSafepointStatistics

  • -XX:PrintSafepointStatisticsCount=1

需要注意的是Safepoint的情况是打印到标准输出里的,具体的日志log示例如下:

vmop    [threads: total initially_running wait_to_block] 65968.203: ForceAsyncSafepoint [931   1   2]

这部分日志是时间戳,VM Operation的类型,以及线程概况,比如上面就显示有2个线程在等待进入Safepoint:

[time: spin block sync cleanup vmop] page_trap_count[2255  0  2255 11  0]  1

接下来的这部分日志是到达Safepoint时的各个阶段以及执行操作所花的时间:

spin:因为JVM在决定进入全局Safepoint的时候,有的线程在Safepoint上,而有的线程不在Safepoint上,这个阶段是等待未在Safepoint上的用户线程进入Safepoint。

block:即使进入Safepoint,用户线程这时候仍然是running状态,保证用户不在继续执行,需要将用户线程阻塞

sync:spin+block的耗时

所以上面的日志就是说,有2个线程进入Safepoint特别慢,其他线程等待这2个线程进入Safepoint等待了2255ms。打印进入Safepoint慢的线程

  • -XX:+SafepointTimeout

  • -XX:SafepointTimeoutDelay=2000

这两个参数的意思是当有线程进入Safepoint超过2000毫秒时,会认为进入Safepoint超时了,这时会打出来哪些线程没有进入Safepoint,具体日志如下:# SafepointSynchronize::begin: Timeout detected:

# SafepointSynchronize::begin: Timed out while spinning to reach a safepoint.# SafepointSynchronize::begin: Threads which did not reach the safepoint:# "RpcServer.listener,port=24600" #32 daemon prio=5 os_prio=0 tid=0x00007f4c14b22840 nid=0xa621 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE# SafepointSynchronize::begin: (End of list)

从上面的日志可以看出来是RpcServer.listener进入Safepoint耗时过长,那么该如何解决这个问题呢?这就需要了解一点GC和Safepoint的背景知识了。

GC及Safepoint

GC

GC(Garabage Collection):垃圾回收,是Java这种自动内存管理语言中的关键技术。GC要解决的三个关键问题是:哪些内存可以回收?什么时候回收?以及如何回收?对于哪些内存可以回收,常见的有引用计数算法和可达性分析算法来判断对象是否存活。可达性分析算法的基本思路是从GC Roots出发向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则称该对象不可达,也就是说可以回收了。常见的GC Roots对象有:

  • 虚拟机栈中引用的对象

  • 方法区中静态属性引用的对象

  • 方法区中常量引用的对象

  • Native方法栈中引用的对象

Safepoint

Java虚拟机HotSpot的实现中,使用一组称为OopMap的数据结构来存放对象引用,从而可以快速且准确的完成GC Root扫描。但程序执行的过程中,引用关系随时都可能发生变化,而HotSpot虚拟机只会在特殊的指令位置才会生成OopMap来记录引用关系,这些位置便被称为Safepoint。换句话说,就是在Safepoint这个点上,虚拟机对于调用栈、寄存器等一些重要的数据区域里什么地方包含了什么引用是十分清楚的,这个时候是可以很快完成GC Roots的扫描和可达性分析的。HotSpot会在所有方法的临返回之前,以及所有Uncounted loop的循环回跳之前放置Safepoint。当需要GC时候,虚拟机会首先设置一个标志,然后等待所有线程进入Safepoint,但是不同线程进入Safepoint的时间点不一样,先进入Safepoint的线程需要等待其他线程全部进入Safepoint,所以Safepoint是会导致STW的。

Counted loop

JVM认为比较短的循环,所以不会放置Safepoint,比如用int作为index的循环。与其对应的是Uncounted loop。

Uncounted loop

JVM会在每次循环回跳之前放置Safepoint,比如用long作为index的循环。所以一个常见的问题是当某个线程跑进了Counted loop时,JVM启动了GC,然后需要等待这个线程把循环跑完才能进入Safepoint,如果这个循环是个大循环,且循环内执行的比较慢,而且不存在其他函数调用产生其他Safepoint,这时就需要等待该线程跑完循环才能从其他位置进入Safepoint,这就导致了其他线程可能会长时间的STW。

解 决 问 题

UseCountLoopSafepoints-XX:+UseCountedLoopSafepoints这个参数会强制在Counted loop循环回跳之前插入Safepoint,也就是说即使循环比较短,JVM也会帮忙插入Safepoint了,用于防止大循环执行时间过长导致进入Safepoint卡住的问题。但是这个参数在JDK8上是有Bug的,可能会导致JVM Crash,而且是到JDK9才修复的,具体参考JDK-8161147。鉴于我们使用的是OpenJDK8,所以只能放弃该方案。

修改循环index为long型

上面的Safepoint Timeout日志已经明确指出了,进Safepoint慢的线程是RpcServer里的listener线程,所以在仔细读了一遍代码之后,发现其调用到的函数cleanupConnections里有个大循环,具体代码如下:

/** cleanup connections from connectionList. Choose a random range     * to scan and also have a limit on the number of the connections     * that will be cleanedup per run. The criteria for cleanup is the time     * for which the connection was idle. If 'force' is true then all     * connections will be looked at for the cleanup.     * @param force all connections will be looked at for cleanup     */private void cleanupConnections(boolean force) {  if (force || numConnections > thresholdIdleConnections) {    long currentTime = System.currentTimeMillis();    if (!force && (currentTime - lastCleanupRunTime) < cleanupInterval) {      return;    }    int start = 0;    int end = numConnections - 1;    if (!force) {      start = rand.nextInt() % numConnections;      end = rand.nextInt() % numConnections;      int temp;      if (end < start) {        temp = start;        start = end;        end = temp;      }    }    int i = start;    int numNuked =0;    while (i <= end) {      Connection c;      synchronized (connectionList) {        try {          c = connectionList.get(i);        } catch (Exception e) {return;}      }        if (c.timedOut(currentTime)) {        if (LOG.isDebugEnabled())          LOG.debug(getName() + ": disconnecting client " + c.getHostAddress());        closeConnection(c);        numNuked++;        end--;        //noinspection UnusedAssignment        c = null;        if (!force && numNuked == maxConnectionsToNuke) break;      }      else i++;    }    lastCleanupRunTime = System.currentTimeMillis();  }}

如注释所说,它会从connectionList中随机选择一个区间,然后遍历这个区间内的connection,并清理掉其中已经timeout的connection。但是,connectionList有可能很大,因为出问题的集群是个离线集群,会有多个MR/Spark Job来进行访问,而每个Job又会同时起多个Mapper/Reducer/Executer来进行访问,其每一个都是一个HBase Client,而Client为了性能考虑,默认连同一个RegionServer的connection数使用了配置4,这就导致connectionList里面可能存在大量的从client连接过来的connection。更为关键的是,这里循环的index是int类型,所以这是一个Counted loop,JVM不会在每次循环回跳的时候插入Safepoint。当GC发生时,如果RpcServer的listener线程刚好执行到该函数里的循环内部时,则必须等待循环跑完才能进入Safepoint,而此时其他线程也必须一起等着,所以从现象上看就是长时间的STW。    

Debug的过程很曲折,但问题解决起来其实很简单,就是把这里的循环index从int类型改为long型即可,这样JVM会在每次循环回跳前插入Safepoint,即使GC时候执行到了循环内部,也只需执行到单次循环体结束便可以进入Safepoint,无需等待整个循环跑完。

 

至此,问题得到解决。

最后,本文重点不是介绍Safepoint原理,主要是对一次线上真实Case的的踩坑记录,希望文中的JVM参数配置和Debug过程可以对大家有所帮助,如有错误,欢迎指正。

 

参考文档:

  https://bugs.openjdk.java.net/browse/JDK-8161147

  http://calvin1978.blogcn.com/articles/safepoint.html

  https://xhao.io/2018/02/safepoint-1/

  https://www.zhihu.com/question/29268019

 《深入理解Java虚拟机》周志明著

这篇关于jvm的stop the world时间过长优化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/977581

相关文章

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001