JVM GC 调优命令看这一篇就够了
2023-11-11 23:27·IT果果日记
jstat
可用于统计内存分配速率、GC次数,GC耗时
jstat常用命令格式
jstat -gc <pid> <统计间隔时间> <统计次数>
例如:jstat -gc 6 1000 10 ,统计pid=6的进程,每秒统计1次,统计10次。参数含义:
- S0C:年轻代中第一个Survivor区的容量,单位为KB。
- S1C:年轻代中第二个Survivor区的容量,单位为KB。
- S0U:年轻代中第一个Survivor区已使用大小,单位为KB。
- S1U:年轻代中第二个Survivor区已使用大小,单位为KB。
- EC:年轻代中Eden区的容量,单位为KB。
- EU:年轻代中Eden区已使用大小,单位为KB。
- OC:老年代的容量,单位为KB。
- OU:老年代已使用大小,单位为KB。
- MC:元空间的容量,单位为KB。
- MU:元空间已使用大小,单位为KB。
- CCSC:压缩类的容量,单位为KB。
- CCSU:压缩类已使用大小,单位为KB。
- YGC:Young GC的次数。
- YGCT:Young GC所用的时间。
- FGC:Full GC的次数。
- FGCT:Full GC的所用的时间。
- GCT:GC的所用的总时间。
jmap
可用于了解系统运行时的对象分布,查看内存
jmap常用命令格式
# 按照类占用内存大小降序排列,查看对象占用内存情况
jmap -histo:live <pid>
例如,输入命令 jmap -histo:live 6,如图。参数含义如下:
- [C is a char[]
- [S is a short[]
- [I is a int[]
- [B is a byte[]
- [[I is a int[][]
上面的输出中[C对象占用Heap这么多,往往跟String有关,String其内部使用final char[]数组来保存数据的。constMethodKlass/ methodKlass/ constantPoolKlass/ constantPoolCacheKlass/ instanceKlassKlass/ methodDataKlass与Classloader相关,常驻于Perm区。
生成堆内存转储快照
生成堆内存转储快照命令格式如下:
# 生成堆内存转储快照,在当前目录下导出dump.hrpof的二进制文件,
# 可以用eclipse的MAT图形化工具分析
jmap -dump:live,format=b,file=dump.hprof <pid>
例如,输入命令 jmap -dump:live,format=b,file=dump.hprof 6 ,就会生成一个dump.hprof文件
如何在OutOfMemoryError时,自动生成hprof文件
在JVM启动时,添加如下参数:
- -XX:+HeapDumpOnOutOfMemoryError:当JVM发生OutOfMemoryError错误时,自动生成hprof文件。
- -XX:HeapDumpPath=:指定hprof文件的输出路径。
- -XX:HeapDumpInterval=:指定hprof文件生成的时间间隔。
- -XX:StartFlightRecording:启用Java飞行记录器(JFR),它可以记录应用程序的性能数据,并生成hprof文件。
- -XX:FlightRecorderOptions:指定Java飞行记录器的配置选项。
如何打开dump文件?
想要打开dump.hprof文件可以利用jdk自带的工具 jvisualvm
点菜单“文件”,选择“装入”
装入时选择文件类型为“堆 Dump”类型
如何分析dump文件?
选择类,然后对堆内存大小进行排序,双击可以查看具体类型的实例内存占用情况
双击具体的实例可以查看该实例的内容和占用的大小,可以选择全部展示或者把该内容保存为一个txt文件
jstack
jstack是JVM自带的Java堆栈跟踪工具,它用于打印出给定的java进程ID、core file、远程调试服务的Java堆栈信息。
- jstack命令用于生成虚拟机当前时刻的线程快照。
- 线程快照是虚拟机中每个线程在执行时的方法堆栈的记录集合。生成线程快照的主要目的是帮助定位导致线程长时间停顿的原因,例如线程间的死锁、死循环、长时间等待外部资源等问题。
- 当线程出现停顿时,使用jstack可以帮助我们查看各个线程的调用堆栈,了解没有响应的线程在后台正在做什么事情或等待什么资源。
- 当Java程序崩溃并生成core文件时,jstack工具可以帮助我们获取core文件的java stack和native stack的信息,这样我们就能轻松地了解Java程序崩溃的原因以及问题发生在程序的哪个地方。
- 另外,jstack工具还可以附属到正在运行的Java程序上,获取当前运行的Java程序的java stack和native stack的信息。如果当前运行的Java程序处于挂起状态,jstack非常有用。
jstack命令格式
jstack命令用于打印指定Java进程、核心文件或远程调试服务器的Java线程的Java堆栈跟踪信息。 jstack命令可以生成JVM当前时刻的线程快照。
jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-IP
- executable:产生core dump的java可执行程序
- core: 将被打印信息的core dump文件
- remote-hostname-or-IP: 远程debug服务的主机名或ip
- server-id: 唯一id,假如一台主机上多个远程debug服务
线程状态
Java语言定义了6种线程状态:
- New:创建后尚未启动的线程处于这种状态,不会出现在Dump中。
- RUNNABLE:包括Running和Ready。线程开启start()方法,会进入该状态,在虚拟机内执行的。
- Waiting:无限的等待另一个线程的特定操作。
- Timed Waiting:有时限的等待另一个线程的特定操作。
- 阻塞(Blocked):在程序等待进入同步区域的时候,线程将进入这种状态,在等待监视器锁。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
Dump文件的线程状态一般其实就以下3种:
- RUNNABLE,线程处于执行中
- BLOCKED,线程被阻塞
- WAITING,线程正在等待
Monitor 监视锁
每个对象都与一个monitor 相关联。当且仅当拥有所有者时(被拥有),monitor才会被锁定。执行到monitorenter指令的线程,会尝试去获得对应的monitor,如下:
- 每个对象维护着一个记录着被锁次数的计数器, 对象未被锁定时,该计数器为0。线程进入monitor(执行monitorenter指令)时,会把计数器设置为1.
- 当同一个线程再次获得该对象的锁的时候,计数器再次自增.
- 当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。
monitor的拥有者线程才能执行 monitorexit指令。线程执行monitorexit指令,就会让monitor的计数器减一。如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了。
Dump 文件分析关注重点
- runnable,线程处于执行中
- deadlock,死锁(重点关注)
- blocked,线程被阻塞 (重点关注)
- Parked,停止
- locked,对象加锁
- waiting,线程正在等待
- waiting to lock 等待上锁
- Object.wait(),对象等待中
- waiting for monitor entry 等待获取监视器(重点关注)
- Waiting on condition,等待资源(重点关注),最常见的情况是线程在等待网络的读写
实战一 - jstack 分析死锁问题
什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法进行下去。
如何用如何用jstack排查死锁问题?
先来看一段会产生死锁的Java程序,源码如下:
package com.examples.test.thread.deadlock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeathLockTest {
private static Lock lock1 = new ReentrantLock();
private static Lock lock2 = new ReentrantLock();
public static void deathLock() {
Thread t1 = new Thread() {
@Override
public void run() {
try {
lock1.lock();
System.out.println(Thread.currentThread().getName() + " get the lock1");
Thread.sleep(1000);
lock2.lock();
System.out.println(Thread.currentThread().getName() + " get the lock2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
lock2.lock();
System.out.println(Thread.currentThread().getName() + " get the lock2");
Thread.sleep(1000);
lock1.lock();
System.out.println(Thread.currentThread().getName() + " get the lock1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//设置线程名字,方便分析堆栈信息
t1.setName("mythread-果果1号");
t2.setName("mythread-果果2号");
t1.start();
t2.start();
}
public static void main(String[] args) {
deathLock();
}
}
运行结果如图:
显然,线程果果1号和线程果果2号都是只执行到一半,就陷入了阻塞等待状态。
jstack排查Java死锁步骤
- 在终端中输入jsp查看当前运行的java程序
- 使用 jstack -l pid 查看线程堆栈信息
- 分析堆栈信息
在终端中输入jsp查看当前运行的java程序
使用 jstack -l pid 查看线程堆栈信息
分析堆栈信息。由上图,可以清晰看到死锁信息:
- mythread-果果1号 等待这个锁 “0x00000000d5b67c10”,这个锁是由于mythread-果果2号线程持有。
- mythread-果果2号 等待这个锁 “0x00000000d5b67be0”, 这个锁是由mythread-果果1号线程持有。
实战二 - jstack 分析CPU过高问题
来个导致CPU过高的demo程序,一个死循环
package com.examples.test.thread.jstack;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 有个导致CPU过高程序的demo,死循环
*/
public class JstackCpuCase {
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
Task task1 = new Task();
Task task2 = new Task();
executorService.execute(task1);
executorService.execute(task2);
}
public static Object lock = new Object();
static class Task implements Runnable {
public void run() {
synchronized (lock) {
long sum = 0L;
while (true) {
sum += 1;
}
}
}
}
}
jstack 分析CPU过高步骤
- top
- top -Hp pid
- jstack pid
- jstack -l [PID] >/tmp/log.txt
- 分析堆栈信息
1、top命令。
在服务器上,我们可以通过top命令查看各个进程的cpu使用情况,它默认是按cpu使用率由高到低排序的。
由上图中,我们可以找出pid为21340的java进程,它占用了最高的cpu资源,凶手就是它。
2、top -Hp pid 命令。
通过 top -Hp 21340 可以查看该进程下,各个线程的cpu使用情况,如下:
可以发现pid为21350的线程,CPU资源占用最高,小本本把它记下来,接下来拿jstack给它拍片子
3、jstack pid 命令。
通过top命令定位到cpu占用率较高的线程之后,接着使用jstack pid命令来查看当前java进程的堆栈状态,
jstack 21350后,内容如下:
4、jstack -l [PID] >/tmp/log.txt
其实,前3个步骤,堆栈信息已经出来啦。但是一般在生成环境,我们可以把这些堆栈信息打到一个文件里,再回头仔细分析。
5、分析堆栈信息
我们把占用cpu资源较高的线程pid(本例子是21350),将该pid转成16进制的值。
在thread dump中,每个线程都有一个nid,我们找到对应的nid(5366),发现一直在跑(24行)
这个时候,可以去检查代码是否有问题。 当然,也建议隔段时间再执行一次stack命令,再一份获取thread dump,毕竟两次拍片结果(jstack)对比更准确。
jinfo
用来查看正在运行的 Java 应用程序的扩展参数,包括Java System属性和JVM命令行参数,
命令格式
jinfo [options]
- jinfo : 打印jvm版本、所有Java System Properties,以及所有VM flags(非常实用)
- jinfo -flag :打印对应的启动参数的值
- jinfo -flag [+/-] :启用或禁用对应的启动参数
- jinfo -flag = :设置对应的启动参数的值
其他GC工具
- 监控告警系统:Zabbix、Prometheus、Open-Falcon
- jdk自动实时内存监控工具:VisualVM
- 堆外内存监控: Java VisualVM安装Buffer Pools 插件、google perf工具、Java NMT(Native Memory Tracking)工具
- GC日志分析:GCViewer、gceasy
- GC参数检查和优化:http://xxfox.perfma.com/
GC优化案例
数据分析平台系统频繁Full GC
数据分析平台主要监控用户在APP中的行为并进行定时分析统计,同时支持报表导出功能,采用CMS GC算法进行内存管理。然而,数据分析师在使用过程中发现系统页面打开经常出现卡顿。通过jstat命令的监测,发现每次进行Young GC后,约有10%的存活对象进入到老年代。
造成这一现象的原因是Survivor区的空间设置过小。因此,在每次Young GC后,Survivor区域无法容纳所有的存活对象,导致它们提前进入老年代。为了解决这个问题,我们决定调整Survivor区的大小,使其能够容纳Young GC后的存活对象。
通过调整,我们使得对象在Survivor区经历多次Young GC后,达到一定的年龄阈值,才会被转移到老年代。这样,每次Young GC后进入老年代的存活对象数量大幅减少,仅有几百Kb。这一改变显著降低了Full GC的频率,使系统运行更加稳定。
业务对接网关OOM
系统在运行几小时后出现了OOM(Out Of Memory)错误,并在重启后几小时再次出现。通过分析,在eclipse MAT工具分析中我们发现网关主要消费了Kafka数据,进行数据处理和计算,然后转发到另一个Kafka队列。然而,代码中存在一个问题是异步打印业务Kafka topic数据,由于数据量较大,大量对象在内存中积压等待打印,最终导致了OOM。
账号权限管理系统频繁长时间Full GC
系统提供了各种账号鉴权服务,但在使用过程中,我们发现系统经常无法正常使用。通过Zabbix的监控平台监控发现,系统频繁发生长时间Full GC,但老年代的堆内存并没有占满。经过调查,我们发现业务代码中调用了System.gc()。