频繁GC
https://heapdump.cn/article/1870333
JVM性能问题排查
典型问题
- CPU使用过高 —— CPU问题
- JVM线程阻塞数过高 —— CPU问题
- GC次数过高 —— 内存问题
- 单次GC占时过高 —— 内存问题
- JVM内存使用率过高 —— 内存问题
- 慢查询数量过高 —— 慢请求
CPU占用过高
参考:
日常程序中常见的耗CPU的操作:
- 频繁GC,访问量高时,有可能造成频繁的GC、甚至FGC。当调用量大时,内存分配过快,就会造成GC线程不停的执行,导致CPU飙高
- 序列化与反序列化,程序执行xml解析的时,调用量增大的情况下,导致了CPU被打满
- 加密、解密
- 正则表达式校验,曾经线上发生一次血案,正则校验将CPU打满。大概原因是:Java 正则表达式使用的引擎实现是 NFA 自动机,这种引擎在进行字符匹配会发生回溯(backtracking)
- 线程上下文切换、当启动了很多线程,而这些线程都处于不断的阻塞状态(锁等待、IO等待等)和执行状态的变化过程中。当锁竞争激烈时,很容易出现这种情况
- 某些线程在做无阻塞的运算,简单的例子while(true)中不停的做运算,没有任何阻塞。写程序时,如果需要做很久的计算,可以适当将程序sleep下
CPU占用过高一般是线程问题,可以找到CPU占用最高的线程,然后排查线程部分的代码是否有问题。
那么如何找到对应的线程呢?这里提供两种方式:jvm 指令和arthas。
JVM指令方式
三步走:
- 找到CPU占用最高的进程
- 找到CPU占用最高的线程
- 找到该线程对应的代码
找到CPU占用最高的线程
如果是容器部署的,基本上一个容器一个Java进程,直接用 jps 命令找到Java进程的PID即可;
如果不是容器部署的,一个服务器上有多个Java进程,则需要使用top命令来找到CPU占用最高的进程,获取PID。
找到CPU占用最高的线程
通过 top -Hp pid 命令列出对应进程下的所有线程信息,获取到CPU占用最高的线程的PID
$ top -Hp 48200
这里拿到的线程pid是十进制的,需要转换成16进制。
$ print "%x\n" 8925
找到该线程对应的代码
使用 jstack pid | grep tid -A30 找到对用的线程信息
$ jstack 48200 | grep 0x22dd -A30
然后在线程信息中找到服务的代码,针对性的看一下就可以了。
线程状态
一个Thread对象可以有多个状态,在java.lang.Thread.State中,总共定义六种状态:
- NEW:线程刚刚被创建,也就是已经new过了,但是还没有调用start()方法,jstack命令不会列出处于此状态的线程信息
- RUNNABLE:就绪状态,线程是可运行的,但并不是在运行中
- BLOCKED:线程处于阻塞状态,正在等待一个monitor lock。通常情况下,是因为本线程与其他线程公用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法,而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。
- WAITING:等待状态
- TIME_WAITING:线程等待指定的时间
- TERMINATED:线程已终止
在 dump 文件中,线程状态和Thread.State中定义的枚举略有不同:
- Deadlock,死锁,两个线程针对临界资源相互等待
- Runnable,执行中
- Waiting on condition,等待资源,或等待某个条件的发生
- Waiting on monitor entry,等待获取监视器,线程在等待进入一个临界区
- Object.wait() 或 TIMED_WAITING,对象等待中
- Suspended,暂停
- Blocked,阻塞
- Parked,停止
其中,Deadlock、Waiting on condition、Waiting on monitor entry和Blocked状态的线程需要重点关注。
如果发现有大量的线程都在处在 Wait on condition,从线程 stack看,正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读 写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。可以结合其他网络分析工具定位问题。
如果堆栈信息明确是应用代码,则证明该线程正在等待资源。一般是大量读取某资源,且该资源采用了资源锁的情况下,线程进入等待状态,等待资源的读取。
Blocked,线程阻塞,是指当前线程执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态,可以理解为等待资源超时的线程。
如果线程处于Blocked状态,但是原因不清楚。可以使用jstack -m pid得到线程的mixed信息。
Arthas
使用arths就比较简单了,启动arthas后进入到指定的应用,然后使用 thread -n 找到CPU占用最高的几个线程即可。
$ thread -n20

线程数过高
可能的问题点
- 瞬时流量过大导致
- 定时任务导致
- 特殊的线程池:方法中创建了线程池,但是使用后没有及时关闭。
思路
排查线程数过高的思路和CPU占用过高的方式类似,也是使用 jstack 命令。
可以根据线程名称看看是不是哪个线程池过多,线程池名称 pool-xx-thread-xx 。
DefaultThreadFactory() {
@SuppressWarnings("removal")
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
如果不存在线程很多的线程池,则使用jstack转存dump信息,可以在 fastthread.io 进行在线分析。
GC次数过高
gc动作主要发生在年轻代(YGC)、老年代(FullGC)和元空间。
FullGC次数过多,主要考虑两点:
- 代码中一次获取了大量的对象,导致内存溢出,此时可以通过eclipse的mat工具查看内存中有哪些对象比较多;
- 内存占用不高,但是Full GC次数还是比较多,此时可能是显示的
System.gc()调用导致GC次数过多,这可以通过添加-XX:+DisableExplicitGC来禁用JVM对显示GC的响应。
可减少对象的创建,增大新生代以及调整幸存区
单次GC时间过长
参考:
动作
使用 jmap 转存dump文件
$ jmap -dump:format=b,file=heap pid
特别注意,转存dump文件时,整个应用会停滞,所以需要先摘掉该节点的流量再执行指令。
jstat 打印GC的情况,可以看到单位时间内的年轻代和老年代的GC次数和消耗时间。
$ jstat -gcutil pid 1000 10
CPU问题
arthas thread指令
jvm jstack指令
内存问题
- 查询数据库导致
- 嵌套循环导致
- 远程调用慢
- 方法慢导致的局部变量内存膨胀
- 关键点要及时dump
慢请求
- trace跟踪 —— arthas
常用工具
JVM监控
arthas: dashboard thread trace watch
JDK:jstack、jmap、jstat
GC log
-
-XX:+PrintGCDateStamps:打印 GC 发生的时间戳。
-
-XX:+PrintTenuringDistribution:打印 GC 发生时的代龄信息。
-
-XX:+PrintGCApplicationStoppedTime:打印 GC 停顿时长
-
-XX:+PrintGCApplicationConcurrentTime:打印 GC 间隔的服务运行时长
-
-XX:+PrintGCDetails:打印 GC 详情,包括 GC 前/内存等。
-
-Xloggc:../gclogs/gc.log.date:指定 GC log 的路径
GC log可配合 https://gceasy.io/ 在线分析工具使用。
文章标签
冬眠
博主专注于技术、阅读与思考。在这里记录学习、思考与生活。
第 1 篇,共 3 篇
