2021-08-19

问题描述:

压测过程中,发现tomcat服务器不灵活,刷新(控制台)页面长时间转圈,无响应。需要找出CUP占用较高原因,进行分析并且优化

解决过程:

1. top命令检查CPU占用较高进程

top

发现pid为17084的java进程占用服务器cpu很高
请添加图片描述

2. 命令 ps -mp 【替换为进程ID PID】 -o THREAD,tid,time,查看该进程占用高的线程;

ps -mp 17084 -o THREAD,tid,time | sort -rn

发现tid为23311的线程占用CPU较高且吃香时间较长,图2效果更明显,是调优前效果,作为对比图用

图1在这里插入图片描述
图2
请添加图片描述

3. 命令printf “%x\n” pid,把tid转换为16进制;

printf “%x\n” 23311

输出结果为5b0f
在这里插入图片描述

4. 命令jstack pid| grep tid(十六进制) -A 10 -B 30,查看进程pid为17084且tid为5b0f的线程的栈信息 前后30行;

jstack 17084 | grep 5b0f -A 10 -B 30

在这里插入图片描述
从报错信息中可以看到为,栈异常导致频繁GC,想要继续追踪其他参数,进一步确认问题

图2为效果更明显图
请添加图片描述

5.命令jmap -histo,查看进程里占用的对象情况,我这里截取占用内存最多的前30条日志;

jmap -histo 17084 | sort -n -r -k 2 | head -30

由图基本上问题已经定位到了,走查代码发现:主要应该是频繁分发那里的代码逻辑,创建了太多得对象,导致大量引用一直存活,且越来越多(GCRoot可达),从6中图2可以看到,青年代和老年代占满,频繁full jc STW,压测请求堆积压垮服务器。
在这里插入图片描述

也可以通过

6. 垃圾回收问题,使用jstat -gcuti pid命令,查看进程Java内存分布及回收情况来验证,间隔1秒打印一次,共5次;

jstat -gcutil统计gc信息显示列名具体描述

jstat -gcutil 17084 1s 5

从图中相同代码,不同时间段产生的YGC和FGC的次数可以看出GC频繁,且FGC频繁(pid为33745的进程效果比较明显,作为对比图使用)
在这里插入图片描述
S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比

S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比

E:年轻代中Eden(伊甸园)已使用的占当前容量百分比

O:old代已使用的占当前容量百分比

M:metadate已使用的占当前容量百分比

YGC:从应用程序启动到采样时年轻代中gc次数

YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)

FGC:从应用程序启动到采样时old代(全gc)gc次数

FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)

GCT:从应用程序启动到采样时gc用的总时间(s)

  • 6.1

    jstat -gcutil信息来看,可以看到GC是ParallelScavenge(-XX:+UseParallelGC,在server class machine上这个是默认GC;以及有可能full GC用的是PSCompact)

  • 6.2
    ParallelScavenge有个默认打开的功能叫做adaptive size policy,在应用分配内存压力很大的时候有可能会把两个survivor space都收缩到最小(几乎为0),于是young gen基本上就整个都是eden,经历young GC能存活下来的对象就会直接晋升到old gen去,等到full GC的时候再看能不能回收。从 jstat -gcutil 的空间占用数据似乎是反映了这种情况

  • 6.3
    而ParallelScavenge在触发GC的时候,要么会触发单独的young GC,要么会触发young GC + full GC(这里是full GC前会关联触发一次young GC,这个行为由 -XX:+ScavengeBeforeFullGC 参数控制)。在触发full GC的时候,它有可能一口气循环接连触发多次full GC——如果跑了一次full GC还没回收到足够内存,那么会再“用力”一点再触发一次full GC,不行就再“用力”一点…(“用力”例如说ParallelScavenge的默认策略一开始是不会强迫清理SoftReference的,但需要再“用力”一点的时候就会清理SoftReference。)

  • 进程号pid为33745 的 jstat -gcutil 的GC次数数据似乎是反映了这种情况

  • 6.4 jstat -gc pid【进程号】也可以查看gc的信息,查看gc的次数,及时间

jstat -gc 17084

在这里插入图片描述
S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
EC:年轻代中Eden(伊甸园)的容量 (字节)
EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
OC:Old代的容量 (字节)
OU:Old代目前已使用空间 (字节)
YGC:从应用程序启动到采样时年轻代中gc次数
YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
FGC:从应用程序启动到采样时old代(全gc)gc次数
FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
GCT:从应用程序启动到采样时gc用的总时间(s)

7. 垃圾回收问题,通过jmap -heap pid命令,打印栈情况来验证信息

jmap -heap 37745

从图中可以明显看到Old Generation使用率为93.17234102692849%
在这里插入图片描述
图2为效果显著图
请添加图片描述
Heap Configuration: #堆配置情况

  • MinHeapFreeRatio = 40 #堆最小使用比例

  • MaxHeapFreeRatio = 70 #堆最大使用比例

  • MaxHeapSize = 8589934592 (8192.0MB) #堆最大空间

  • NewSize = 1363144 (1.2999954223632812MB) #新生代初始化大小

  • MaxNewSize = 5152702464 (4914.0MB) #新生代可使用最大容量大小

  • OldSize = 5452592 (5.1999969482421875MB) #老生代大小

  • NewRatio = 2 #新生代比例

  • SurvivorRatio = 8 #新生代与suvivor的占比

  • MetaspaceSize = 21807104 (20.796875MB) #元数据空间初始大小

  • CompressedClassSpaceSize = 1073741824 (1024.0MB) #类指针压缩空间大小, 默认为1G

  • MaxMetaspaceSize = 17592186044415 MB #元数据空间的最大值, 超过此值就会触发 GC溢出( JVM会动态地改变此值)

  • G1HeapRegionSize = 2097152 (2.0MB) #区块的大小

Heap Usage: G1 Heap:

  • regions = 4096 # G1区块初始化大小
  • capacity = 8589934592 (8192.0MB) #G1区块最大可使用大小
  • used = 1557972768 (1485.7986145019531MB) #G1区块已使用内存
  • free = 7031961824 (6706.201385498047MB) #G1区块空闲内存
  • 18.137190118432045% used #G1区块使用比例G1
    Young Generation: 新生代
    Eden Space: #Eden区空间
  • regions = 670
  • capacity = 2699034624 (2574.0MB)
  • used = 1405091840 (1340.0MB)
  • free = 1293942784 (1234.0MB)
  • 52.05905205905206% used
    Survivor Space: #Survivor区
  • regions = 3
  • capacity = 6291456 (6.0MB)
  • used = 6291456 (6.0MB)
  • free = 0 (0.0MB)
  • 100.0% used
    G1 Old Generation: #老生代
  • regions = 72
  • capacity = 1589641216 (1516.0MB)
  • used = 146589472 (139.79861450195312MB)
  • free = 1443051744 (1376.2013854980469MB)
  • 9.221544492213267% used

8.垃圾回收问题,通过jstack命令,输出dump文件来验证

jstack -l 17804 > jstack.log

图1
请添加图片描述
图2
请添加图片描述

图3
请添加图片描述
图4
请添加图片描述

  • 问题分析

    通过图1,图2,图3可以看出:
    搜索java.lang.Thread.State: RUNNABLE ,共520次,说明所有线程都处于运行状态
    搜索java.net.SocketInputStream.socketRead0(Native Method) ,共500次,说明tomcat线程都阻塞在socket的read方法。
    搜索at com.telecomyt.mdm.sms.api.AuthApi.getMember(AuthApi.java:98),共500次,此为业务代码
    根据日志查看业务代码:

String response = HttpUtil.postJson( authApi.ucenterProperties.getUrl() + authApi.ucenterProperties.getMemberInfoUri(),GsonUtil.toJson(memberDto),null)

使用的java.net的http请求,并且没有配置超时时间,默认超时时间为-1,导致服务调用超时却未关闭,大量占用线程,随着压测数据增加造成大量引用无法回收。
参考文档1
参考文档2

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值