记一次jvm性能调优

背景

项目的海外版本,已经有两年没有更新了,目前需要更新到最新版本,申请资源时,已经没有当前使用的4C8G服务器资源了,只有2C4G的服务器了,当前每台服务器的qps有1500左右,看cpu也只使用了5%的样子,虽然新版本增加了不少功能,就算资源缩一半,看起来也是可行的。

实际情况

单独拉了一个分组,新版本先上了一台,观察了一天,日志没发现啥问题,便继续挂上其他机器,跑了一会,收到性能报警,max有超过1s的情况,看老版本的max,毛刺也很多,但没超过1s的情况。

性能较好的是老版本,新版本明显比老版本差很多,不止max,tp999、tp99等各个指标都很差。

排查-代码差异

首先看了下新老版本的代码,看下是否是新版代码有消耗性能的逻辑,两边代码对比了下,变化基本不大,增加的逻辑,也没有性能耗点。

排查-线程运行情况

因为代码用到了线程池,看看是否因为线程太多,过度竞争引起,通过jstack下载线程dump,发现有很多Waiting on condition状态的线程。

其中queryThread-开头的便是线程池创建的线程,有很多处于等待状态,看看处于这种状态的线程的解释。

系统线程状态为Waiting on condition

系统线程处于此种状态说明它在等待另一个条件的发生来唤醒自己,或者自己调用了sleep()方法。此时JVM线程的状态通常是java.lang.Thread.State: WAITING (parking)(等待唤醒条件)或java.lang.Thread.State: TIMED_WAITING (parking或sleeping)(等待定时唤醒条件)。

如果大量线程处于此种状态,说明这些线程又去获取第三方资源了,比如第三方的网络资源或读取数据库的操作,长时间无法获得响应,导致大量线程进入等待状态。因此,这说明系统处于一个网络瓶颈或读取数据库操作时间太长。

再看下更详细的栈信息:

"queryThread-29" #196 prio=5 os_prio=0 tid=0x00007fcf78018000 nid=0x7fe waiting on condition [0x00007fcdabdfc000]
   java.lang.Thread.State: TIMED_WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000006c2b57720> (a java.util.concurrent.SynchronousQueue$TransferStack)
	at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
	at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:460)
	at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
	at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:941)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1066)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
	- None

JVM线程的状态是 java.lang.Thread.State: TIMED_WAITING (parking),说明线程处于定时等待的状态,parking指线程处于挂起中。

waiting on condition需要结合堆栈中的 parking to wait for <0x00000006c2b57720> (a java.util.concurrent.SynchronousQueue$TransferStack) 一起来分析。首先,本线程肯定是在等待某个条件的发生来把自己唤醒。其次,SynchronousQueue并不是一个队列,只是线程之间移交信息的机制,当我们把一个元素放入到 SynchronousQueue 中的时候必须有另一个线程正在等待接受移交的任务,因此这就是本线程在等待的条件。

因为代码会通过线程去redis获取数据,那么可能等待的就是这个了,但是对比老版线程dump,并没有这种情况,也就是说从redis获取数据是很快的,不应该出现这么多等待的情况,难道是因为低配机器线程池创建的线程太多了?

调整-线程池参数

本来想参考下,老版机器的线程池参数,然而,老版就没有设置线程池参数,那么意味着使用的是默认的线程池的参数,通过jmap下载堆dump,可以看到老版线程池的运行时配置。

 可以看到这个配置也是不合理的,如果处理的慢,会导致大量请求进入队列,但事实却没有发生这种情况,因为每个请求处理的很快,可以看到largestPoolSize=8,也就是同时工作的线程最大也就8个,所以这个没有参考的意义,只有一个一个的调整参数,来试试了。

当前的参数:threadpool: { size: 4 , max: 32 , queue: 50 , keep: 60 }

最终效果参数:threadpool: { size: 4 , max: 16 , queue: 0 , keep: 60 }

最终调整为这个参数后,毛刺少了不少,超时的情况也变少了,但感觉还是有问题,和老版比较,还是有差距。

再次排查-编译线程

再次查看线程dump,又有了新的发现。

 发现有很多CompilerThread线程处于等待状态,这些线程是干嘛的,先看下:

什么是JIT编译?

在谈到 java的编译机制的时候,其实应该按时期,分为两个部分。一个是 javac指令将java源码变为 java字节码的静态编译过程。 另一个是 java字节码编译为本地机器码的过程,最初都是解释执行,当发现某个方法或代码块运行特别频繁时,就会将这些热点代码编译成本地机器码,并以各种手段尽可能地进行代码优化,以提高热点代码的执行效率,因为这个过程是在程序运行时期完成的所以称之为即时编译(JIT:Just In Time)。

解释器与编译器

目前主流的Java虚拟机,内部都同时包含解释器和编译器,两者各有优势:当程序需要迅速启动和执行的时候,使用解释器可以省去编译的时间,立即运行。当程序启动后,随着时间的推移,编译器可以把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率。如果程序运行环境内存资源比较紧张,可以使用解释执行,节约内存(如部分嵌入式系统和大部分的JavaCard应用就只有解释器存在),反之可以使用编译执行来提升效率。程序的执行,可以在解释和编译之间切换,两者是相辅相成的配合工作。

JIT编译类型:C1编译器、C2编译器、分层编译器

HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler(C1编译器)和Server Compiler(C2编译器)。在分层编译的工作模式出现以前,HotSpot虚拟机是采用解释器搭配其中的一个编译器工作,使用那个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,也可以使用“-client”或“-server”参数指定虚拟机运行在那个模式下。

即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度越高的代码,所花费的时间会越长;要想编译优化程度更高的代码,解释器可能还需要替编译器收集性能监控信息,这对解释阶段的速度也有影响。为了程序启动相应速度和运行效率之间达到最佳平衡,加入了分层编译的功能。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,包括:

  • 第 0 层:程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
  • 第 1 层:使用C1编译器,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;
  • 第 2 层:仍然使用C1编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
  • 第 3 层:仍然使用C1编译器执行,开启全部的性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
  • 第 4 层:使用C2编译器,将字节码编译为本地代码,会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

实施分层编译后,解释器、C1和C2编译器就会同时工作,热点代码都可能会被多次编译,用C1可以获取更高的编译速度,用C2可以获取更好的编译质量,在解释执行的时候也无需承担收集性能监控信息的任务,在C2采用高复杂度的优化算法时,C1可先采用简单优化来为它争取更多的编译时间。

在默认条件下,无论是方法调用产生的标准编译请求,还是栈上替换编译请求,虚拟机在编译器还未完成编译之前,都仍然按照解释的方式继续执行,而编译动作则在后台的编译线程中进行。所以这个CompilerThread线程就是C1、C2编辑器线程,在给我们的程序做优化(方法内联、逃逸分析、公共子表达式消除、数组边界检查消除等),这个线程数默认是服务器的核数,但是取的是物理机的核数,因为现在一般都是docker,所以使用默认的就不合适了。

再次排查-GC

基于上面的编译线程,想到GC的并行线程数也是默认取物理机的核数,在线程dump里面并没有发现GC的线程,这应该和我获取线程dump的时机有关,正好GC结束了。

调整-JVM参数

jvm增加这两个参数:

-XX:ParallelGCThreads=2 -XX:CICompilerCount=2

更详细的合理jvm参数设置,参考这里:

记一次GC优化_matt8的专栏-CSDN博客现象young gc时间达到了150-500ms之间,每个服务器的时间不一样,都在这个区间,监控的tp99和tp999有明显的毛刺环境docker环境,服务器规格:2c4g,容器:tomcat8,jdk:8,回收器:cms排查过程1、首先想到查看gc日志,发现有如下日志:[GC (Allocation Failure) [ParNew: 561827K->2873K(629120K), 0.4622482 secs] 1149586K->590676K(2027264Khttps://blog.csdn.net/matt8/article/details/106724026?spm=1001.2014.3001.5501

总结

共涉及两个改造点:

线程池参数:threadpool: { size: 4 , max: 16 , queue: 0 , keep: 60 }

jvm参数:-XX:ParallelGCThreads=2 -XX:CICompilerCount=2

注意,这里是基于2C4G服务器的配置,其他配置需要调整。

效果

11.240.112.41:2C4G,新版服务器

11.240.113.146:4C8G,老版服务器

可以看出,新版服务器虽然配置低,但是性能更好,更稳定,提升还是很明显的。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值