5. 老年代垃圾回收统计
命令:
jstat -gcold 进程ID
参数含义:
- MC:方法区大小
- MU:方法区已使用大小
- CCSC:压缩指针类空间大小
- CCSU:压缩类空间已使用大小
- OC:老年代大小
- OU:老年代已使用大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间,新生代+老年代
6. 老年代内存统计
命令:
jstat -gcoldcapacity 进程ID
参数含义:
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:老年代大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
7. 元数据空间统计
命令
jstat -gcmetacapacity 进程ID
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小指针压缩类空间大小
- CCSMX:最大指针压缩类空间大小
- CCSC:当前指针压缩类空间大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
8.整体运行情况
命令:
jstat -gcutil 进程ID
- S0:Survivor 1区当前使用比例
- S1:Survivor 2区当前使用比例
- E:Eden区使用比例
- O:老年代使用比例
- M:元数据区使用比例
- CCS:指针压缩使用比例
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
通过查询上面的参数来分析整个堆空间。
二、Arthas线上分析工具的使用
Arthas的功能非常强大,现附上官方文档:https://arthas.aliyun.com/doc/
其实想要了解Arthas,看官方文档就可以了,功能全而详细。那为什么还要整理一下呢?我们这里整理的是一些常用功能,以及在紧急情况下可以给我们帮大忙的功能。
Arthas分为几个部分来研究,先来看看我们的研究思路哈
1.安装及启动---这一块简单看,对于程序员来说,so easy
2.dashboard仪表盘功能---类似于JDK的jstat命令,
3.thread命令查询进行信息---类似于jmap命令
4.反编译线上代码----这个功能很牛,改完发版了,怎么没生效,反编译看看。
5.查询某一个函数的返回值
6.查询jvm信息,并修改----当发生内存溢出是,可以手动设置打印堆日志到文件
7.profiler火焰图
下面就来看看Arthas的常用功能的用法吧
1、Arthas的安装及启动
其实说到这快,不得不提的是,之前我一直因为arthas是一个软件,要启动,界面操作。当时我就想,要是这样,在线上安装一个单独的应用,公司肯定不同意啊~~~,研究完才发现,原来Arthas就是一个jar包。运行起来就是用java -jar 就可以。
1) 安装
可以直接在Linux上通过命令下载:
wget https://alibaba.github.io/arthas/arthas-boot.jar
也可以在浏览器直接访问https://alibaba.github.io/arthas/arthas-boot.jar,等待下载成功后,上传到Linux服务器上。
2) 启动
执行命令就可以启动了
java -jar arthas-boot.jar
启动成功可以看到如下界面:
然后找到你想监控的进程,输入前面对应的编号,就可以开启进行监控模式了。比如我要看4
看到这个就表示,进入应用监听成功
2、dashboard仪表盘--查询整体项目运行情况
执行命令
dashboard
这里面一共有三块
1)线程信息
我们可以看到当前进程下所有的线程信息。其中第13,14号线程当前处于BLOCKED阻塞状态,阻塞时间也可以看到。通过这个一目了然,当前有两个线程是有问题的,处于阻塞状态GC线程有6个。
2)内存信息
内存信息包含三个部分:堆空间信息、非堆空间信息和GC垃圾收集信息
堆空间信息
- g1_eden_space: Eden区空间使用情况
- g1_survivor_space: Survivor区空间使用情况
- g1_old_gen: Old老年代空间使用情况
非堆空间信息
- codeheap_'non-nmethods': 非方法代码堆大小
- metaspace: 元数据空间使用情况
- codeheap_'profiled_nmethods':
- compressed_class_space: 压缩类空间使用情况
GC垃圾收集信息
- gc.g1_young_generation.count:新生代gc的数量
- gc.g1_young_generation.time(ms)新生代gc的耗时
- gc.g1_old_generation.count: 老年代gc的数量
- gc.g1_old_generation.time(ms):老年代gc的耗时
3) 运行时信息
- os.name:当前使用的操作系统 Mac OS X
- os.version :操作系统的版本号 10.16
- java.version:java版本号 11.0.2
- java.home:java根目录 /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home
- systemload.average:系统cpu负载平均值4.43
load average值的含义
> 单核处理器
假设我们的系统是单CPU单内核的,把它比喻成是一条单向马路,把CPU任务比作汽车。当车不多的时候,load <1;当车占满整个 马路的时候 load=1;当马路都站满了,而且马路外还堆满了汽车的时候,load>1
> 多核处理器
我们经常会发现服务器Load > 1但是运行仍然不错,那是因为服务器是多核处理器(Multi-core)。
假设我们服务器CPU是2核,那么将意味我们拥有2条马路,我们的Load = 2时,所有马路都跑满车辆。
Load = 2时马路都跑满了
- processors : 处理器个数 8
- timestamp/uptime:采集的时间戳Fri Jan 07 11:36:12 CST 2022/2349s
通过仪表盘,我们能从整体了解当前线程的运行健康状况
3.thread命令查询CPU使用率最高的线程及问题原因
通过dashboard我们可以看到当前进程下运行的所有的线程。那么如果想要具体查看某一个线程的运行情况,可以使用thread命令
1. 统计cpu使用率最高的n个线程
先来看看常用的参数。
参数说明
参数名称 | 参数说明 |
id | 线程id |
[n:] | 指定最忙的前N个线程并打印堆栈 |
[b] | 找出当前阻塞其他线程的线程 |
[i <value> ] |
指定cpu使用率统计的采样间隔,单位为毫秒,默认值为200 |
[--all] | 显示所有匹配的线程 |
我们的目标是想要找出CPU使用率最高的n个线程。那么需要先明确,如何计算出CPU使用率,然后才能找到最高的。计算规则如下:
首先、第一次采样,获取所有线程的CPU时间(调用的是java.lang.management.ThreadMXBean#getThreadCpuTime()及sun.management.HotspotThreadMBean.getInternalThreadCpuTimes()接口) 然后、睡眠等待一个间隔时间(默认为200ms,可以通过-i指定间隔时间) 再次、第二次采样,获取所有线程的CPU时间,对比两次采样数据,计算出每个线程的增量CPU时间 线程CPU使用率 = 线程增量CPU时间 / 采样间隔时间 * 100% 注意: 这个统计也会产生一定的开销(JDK这个接口本身开销比较大),因此会看到as的线程占用一定的百分比,为了降低统计自身的开销带来的影响,可以把采样间隔拉长一些,比如5000毫秒。
统计1秒内cpu使用率最高的n个线程:
thread -n 3 -i 1000
从线程的详情可以分析出,目前第一个线程的使用率是最高的,cpu占用了达到99.38%。第二行告诉我们,是Arthas.java这个类的第38行导致的。
由此,我们可以一眼看出问题,然后定位问题代码的位置,接下来就是人工排查问题了。
2、查询出当前被阻塞的线程
命令:
thread -b
可以看到内容提示,线程Thread-1被线程Thread-0阻塞。对应的代码行数是DeadLockTest.java类的第31行。根据这个提示去查找代码问题。
3、指定采样的时间间隔
命令
thread -i 1000
这个的含义是个1s统计一次采样
4.反编译线上代码----这个功能很牛,改完发版了,怎么没生效,反编译看看。
说道Arthas,不得不提的一个功能就是线上反编译代码的功能。经常会发生的一种状况是,线上有问题,定位问题后立刻改代码,可是发版后发现没生效,不可能啊~~~刚刚提交成功了呀。于是重新发版,只能靠运气,不知道为啥没生效。
反编译线上代码可以让我们一目了然知道代码带动部分是否生效。反编译代码使用Arthas的jad命令
jad 命令将JVM中实际运行的class的byte code反编译成java代码 JAVA 复制 全屏
用法:
jad com.lxl.jvm.DeadLockTest
运行结果:
运行结果分析:这里包含3个部分
- ClassLoader:类加载器就是加载当前类的是哪一个类加载器
- Location: 类在本地保存的位置
- 源码:类反编译字节码后的源码
如果不想想是类加载信息和本地位置,只想要查看类源码信息,可以增加--source-only参数
jad --source-only 类全名
6. ognl 动态执行线上的代码
能够调用线上的代码,是不是很神奇了。感觉哪段代码执行有问题,但是又没有日志,就可以使用这个方法动态调用目标方法了。
我们下面的案例都是基于这段代码执行,User类:
public class User { private int id; private String name; public User() { } public User(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
DeadLockTest类:
public class DeadLockTest { private static Object lock1 = new Object(); private static Object lock2 = new Object(); private static List<String> names = new ArrayList<>(); private List<String> citys = new ArrayList<>(); public static String add() { names.add("zhangsan"); names.add("lisi"); names.add("wangwu"); names.add("zhaoliu"); return "123456"; } public List<String> getCitys() { DeadLockTest deadLockTest = new DeadLockTest(); deadLockTest.citys.add("北京"); return deadLockTest.citys; } public static List<User> addUsers(Integer id, String name) { List<User> users = new ArrayList<>(); User user = new User(id, name); users.add(user); return users; } public static void main(String[] args) { new Thread(() -> { synchronized (lock1) { try { System.out.println("thread1 begin"); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock2) { System.out.println("thread1 end"); } } }).start(); new Thread(() -> { synchronized (lock2) { try { System.out.println("thread2 begin"); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock1) { System.out.println("thread2 end"); } } }).start(); } }
1)获取静态函数
> 返回值是字符串
ognl '@全路径类名@静态方法名("参数")'
示例1:在DeadLockTest类中有一个add静态方法,我们来看看通过ognl怎么执行这个静态方法。执行命令
ognl '@com.lxl.jvm.DeadLockTest@add()' 其中,第一个@后面跟的是类的全名称;第二个@跟的是属性或者方法名,如果属性是一个对象,想要获取属性里面的属性或者方法,直接打.属性名/方法名 即可。
运行效果:
我们看到了这个对象的返回值是123456
> 返回值是对象
ognl '@全路径类名@静态方法名("参数")' -x 2
这里我们可以尝试一下替换-x 2 为 -x 1 ;-x 3;
* 案例1:返回对象的地址。不加 -x 或者是-x 1
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' 或 ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 1
返回值
* 案例2:返回对象中具体参数的值。加 -x 2
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 2
返回值
* 案例3:返回对象中有其他对象
- 命令:
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 2
执行结果:
-x 2 获取的是对象的值,List返回的是数组信息,数组长度。
- 命令:
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 3
执行结果:
-x 3 打印出对象的值,对象中List列表中的值。
* 案例4:方法A的返回值当做方法B的入参
ognl '#value1=@com.lxl.jvm.DeadLockTest@getCitys(), #value2=@com.lxl.jvm.DeadLockTest@generatorUser(1,"lisi",#value1), {#value1,#value2}' -x 2
> 方法入参是简单类型的列表
ognl '@com.lxl.jvm.DeadLockTest@returnCitys({"beijing","shanghai","guangdong"})'
> 方法入参是一个map对象
ognl '#value1=new com.lxl.jvm.User(1,"zhangsan"), #value1.setCitys({"bj", "sh"}), #value2=#{"mum":"zhangnvshi","dad":"wangxiansheng"}, #value1.setFamily(#value2), #value1' -x 2
2)获取静态字段
ognl '@全路径类名@静态属性名'
示例:在DeadLockTest类中有一个names静态属性,下面来看看如何获取这个静态属性。执行命令:
ognl '@com.lxl.jvm.DeadLockTest@names' 其中,第一个@后面跟的是类的全名称;第二个@跟的是属性或者方法名,如果属性是一个对象,想要获取属性里面的属性或者方法,直接打.属性名/方法名 即可。
运行效果:
第一次执行获取属性命令,返回的属性是一个空集合;然后执行add方法,往names集合中添加了属性;再次请求names集合,发现有4个属性返回。
3) 获取实例对象
ognl '#value1=new com.lxl.jvm.User(1,"zhangsan"),#value1.setName("aaa"), #value1.setCitys({"bj", "sh"}), {#value1}' -x 2
获取实例对象,使用new关键字,执行结果:
7. 线上代码修改
生产环境有时会遇到非常紧急的问题,或突然发现一个bug,这时候不方便重新发版,或者发版未生效,可以使用Arthas临时修改线上代码。通过Arthas修改的步骤如下:
1. 从读取.class文件 2. 编译成.java文件 3. 修改.java文件 4. 将修改后的.java文件编译成新的.class文件 5. 将新的.class文件通过classloader加载进JVM内
第一步:读取.class文件
sc -d *DeadLockTest*
使用sc命令查看JVM已加载的类信息。关于sc命令,查看官方文档:
https://arthas.aliyun.com/doc/sc.html
- -d : 表示打印类的详细信息
最后一个参数classLoaderHash,表示在jvm中类加载的hash值,我们要获得的就是这个值。
第二步:使用jad命令将.class文件反编译为.java文件才行
jad -c 7c53a9eb --source-only com.lxl.jvm.DeadLockTest > /Users/lxl/Downloads/DeadLockTest.java
- jad命令是反编译指定已加载类的源码
- -c : 类所属 ClassLoader 的 hashcode
- --source-only:默认情况下,反编译结果里会带有
ClassLoader
信息,通过--source-only
选项,可以只打印源代码。 - com.lxl.jvm.DeadLockTest:目标类的全路径
- /Users/lxl/Downloads/DeadLockTest.java:反编译文件的保存路径
/* * Decompiled with CFR. * * Could not load the following classes: * com.lxl.jvm.User */ package com.lxl.jvm; import com.lxl.jvm.User; import java.util.ArrayList; import java.util.List; public class DeadLockTest { private static Object lock1 = new Object(); private static Object lock2 = new Object(); private static List<String> names = new ArrayList<String>(); private List<String> citys = new ArrayList<String>(); public static List<String> getCitys() { DeadLockTest deadLockTest = new DeadLockTest(); /*25*/ deadLockTest.citys.add("北京"); /*27*/ return deadLockTest.citys; } ...... public static void main(String[] args) { ...... } }
这里截取了部分代码。
第三步:修改java文件
public static List<String> getCitys() { System.out.println("-----这里增加了一句日志打印-----"); DeadLockTest deadLockTest = new DeadLockTest(); /*25*/ deadLockTest.citys.add("北京"); /*27*/ return deadLockTest.citys; }
第四步:使用mc命令将.java文件编译成.class文件
mc -c 512ddf17 -d /Users/luoxiaoli/Downloads /Users/luoxiaoli/Downloads/DeadLockTest.java
- mc: 编译.java文件生.class文件, 详细使用方法参考官方文档
https://arthas.aliyun.com/doc/mc.html
- -c:指定classloader的hash值
- -d:指定输出目录
- 最后一个参数是java文件路径
这是反编译后的class字节码文件
第五步:使用redefine命令,将.class文件重新加载进JVM
redefine -c /Users/***/Downloads/com/lxl/jvm/DeadLockTest.class
最后看到redefine success,表示重新加载.class文件进JVM成功了。
注意事项
redefine命令使用之后,再使用jad命令会使字节码重置,恢复为未修改之前的样子。官方关于redefine命令的说明
第六步:检验效果
这里检测效果,调用接口,执行日志即可。
8、实时修改生产环境的日志级别
这个功能也很好用,通常,我们在日志中打印的日志级别一般是infor、warn、error级别的,debug日志一般看不到。那么出问题的时候,一些日志,在写代码的时候会被记录在debug日志中,而此时日志级别又很高。那么迫切需要调整日志级别。
这个功能很好用啊,我们可以将平时不经常打印出来的日志设置为debug级别。设置线上日志打印级别为info。当线上有问题的时候,可以将日志级别动态调整为debug。异常排查完,在修改回info。这对访问量特别大日志内容很多的项目比较有效,可以有效节省日志输出带来的开销。
第一步:使用logger命令查看日志级别
- 当前应用的日志级别是info
- 类加载的hash值是18b4aac2
我们定义一个接口,其源代码内容如下:
@PostMapping(value = "test") public String test() { log.debug("这是一条 debug 级别的日志"); log.info("这是一条 info 级别的日志"); log.error("这是一条 error 级别的日志"); log.warn("这是一条 warn 级别的日志"); return "完成"; }
可以调用接口,查看日志输出代码。
我们看到,日志输出的是info及以下的级别。
第二步:修改logger日志的级别
logger -c 18b4aac2 --name ROOT --level debug
修改完日志级别以后,输出日志为debug级别。
8. 查询jvm信息,并修改----当发生内存溢出时,可以手动设置打印堆日志到文件
通常查询jvm参数,使用的是Java自带的工具[jinfo 进程号]。arthas中通过vmoption获取jvm参数:
假设,我们要设置JVM出现OutOfMemoryError的时候,自动dump堆快照
vmoption HeapDumpOnOutOfMemoryError true
这时,如果发生堆内存溢出,会打印日志到文件
9. 监控函数耗时
trace 待监控方法的全类名 待监控的方法名
trace com.lxl.jvm.DeadLockTest generatorUser
- 通过圈起来的部分可以看到,接口的入口函数time总耗时371ms
- 其中getDataFromDb函数耗时200ms
- getDataFromRedis函数耗时100ms
- getDataFromOuter函数耗时50ms
- process函数耗时20ms
很明显,最慢的函数已经找到了,接下里就要去对代码进行进一步分析,然后再进行优化