JVM调优实战

JVM的一些查询指令

jps:查看进程及pid
jmap:查看内存信息,实例个数以及占用内存大小(线上内存飙高可以使用此命令查询)
jmap -histo pid:根据查询出的pid查看历史生成的实例,我们也可以在此命令后加上 > ./文件名 来把当前日志到处到当前目录下,例如/jmap -histo pid > ./log.txt。
num:序号 instances:实例数量 bytes:占用空间大小 class name:类名称
请添加图片描述
jmap -histo:live pid:查看当前存活的实例,执行过程中可能会触发一次full gc
jmap -dump:format=b,file=test.hprof pid:导出堆内存溢出日志,也可以在JVM参数中设置内存溢出时自动导出dump文件,之后可以用jvisualvm工具导入该dump文件进行分析
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./ 文件路径
jstack
jstack pid 可以查找死锁,jvisualvm工具自动检测死锁就是这个命令实现的。
请添加图片描述
jstack还可以找出占用cpu最高的线程堆栈信息:
先使用top命令查处占用率高的pid,然后用top -p pid显示此进程的内存信息,按H,获取每个线程的内存情况,找到内存和cpu占用最高的线程tid,用命令jstack pid|grep -A 10 16进制tid,就可以找到导致cpu飙高的代码具体位置。
jinfo :
jinfo -flags查看jvm的启动参数包括jvm默认参数
jinfo - sysprops 查看java系统参数
jstat:可以查看堆内存各部分的使用量,和加载类的数量。
jstat -gc pid :查看内存使用及GC压力整体情况(最常用)如jstat -gc pid 2000 10, 2000是每两秒执行一次此命令 10是执行0次
请添加图片描述

调优思路

通过jstat查询的参数我们可以估算出以下数据:
年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10,通过观察EU(eden区的使用率)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
Young GC的触发频率和每次耗时
有了年轻代对象增长速率我们就能推根据Eden区的大小推算出Young GC的触发频率,通过YGCT/YGC 可以算出平均耗时。
每次Young GC后有多少对象存活和进入老年代
大概知道了Young GC的频率,假设是每5分钟一次,那么可以把命令命令改成 jstat -gc pid 300000 10 ,重新观察每次结果Eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
Full GC的触发频率和每次耗时
有了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

优化思路可以根据以下顺序
1、大对象直接进入老年代:根据代码判断是否有大对象,如果有可以通过调大年轻代来优化,一般出现较少。
2、长期存活的对象进入老年代:没有了大对象,就分析长期存活对象,这种对象一般最开始就放进老年代并且不容易变成垃圾对象,如果每次Young Gc后新增到老年代的对象内存很大,并且很稳定,应该就不是长期存活对象(比如老年代1G,每次Young Gc都会新增700M对象到老年代)
3、对象动态年龄判断:如果也不是长期存活对象,并且每次Young Gc新增到老年代的对象还很多,就可能是触发了对象动态年龄判断机制,我们可以把年轻代适当调大点,老年代可以小点,尽量让每次Young GC后的存活对象小于Survivor区域的50%。
4、老年代空间分配担保机制:还有就是看是否触发了老年代空间分配担保机制,像jdk1.8就默认开启了这个机制,可以根据老年代增长速率,还有老年代的剩余空间,把老年代使用率参数设置小一点,让老年代剩余空间大一点。
总体来说,jVM调优主要是减少Full Gc次数,尽量别让对象进入老年代,减少Full GC的频率,Minor Gc消耗时间基本上极短,优化完Full Gc后在考虑优化Young GC。

Full gc比Minor gc多的主要原因:
1、元空间不够导致的多余full gc
2、代码中调用System.gc()或者Runtime.getRuntime().gc()造成多余的full gc,这种一般线上禁用,加上参数-XX:+DisableExplicitGC可以使以上调用失效。
3、触发了老年代空间分配担保机制,因为每次触发都会先Full Gc再Minor Gc,并且如果Minor Gc后老年代空间又满了,还会再触发一次Full Gc。

调优案例:
机器配置:2核4G
JVM内存大小:2G
系统运行时间:7天
期间发生的Full GC次数和耗时:500多次,200多秒
期间发生的Young GC次数和耗时:1万多次,500多秒
JVM参数设置如下:

-Xms1536M 
-Xmx1536M 
-Xmn512M 
-Xss256K 
-XX:SurvivorRatio=6  
-XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M 
-XX:+UseParNewGC  
-XX:+UseConcMarkSweepGC  
-XX:CMSInitiatingOccupancyFraction=75 
-XX:+UseCMSInitiatingOccupancyOnly 

模拟代码如下:

@Test
public void test() throws Exception {
	for (int i = 0; i < 10000; i++) {
		String result = restTemplate.getForObject("http://localhost:8080/user/process", String.class);
		Thread.sleep(1000);
	}
}
public class IndexController {

    @RequestMapping("/user/process")
    public String processUserData() throws InterruptedException {
        ArrayList<User> users = queryUsers();

        for (User user: users) {
            System.out.println("user:" + user.toString());
        }

        return "end";
    }

    /**
     * 模拟批量查询用户场景
     * @return
     */
    private ArrayList<User> queryUsers() {
        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 8000; i++) {
            users.add(new User(i,"xinyu"));
        }
        return users;
    }
}
public class User {
	
	private int id;
	private String name;

	byte[] a = new byte[1024*100];

	public User(){}

	public User(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}

}

根据以上数据,大致算下来每天会发生70多次Full GC,平均每小时3次,每次Full GC在400毫秒左右,用 jstat gc -pid 命令可以得出下图内存模型:
在这里插入图片描述
如果代码中没有大对象
1、从图中看平均20分钟就有700多M对象进入老年代导致FullGc,老年代内存只有1G,每次都有700M对象进入而且不会OOM,说明这些对象都会在Full Gc之后被清理掉,这些对象是本应放在Eden区,不是长期存活对象,因为长期存活对象会在程序启动最开始就进入老年代,就算有变化也不会太大。
2、分析完这些对象不是长期存活对象,所以说在Minor Gc阶段这些对象就应该被清理掉,从图中看Eden区每分钟都会被占满,Minor Gc的话应该也不会出现什么问题,但是如果系统并发较高情况下,一个线程可能几秒才能执行完,在没有执行完时就触发了Minor Gc,这些对象会被判断为不是垃圾对象,直接放进Survivor区,此时Survivor中可能还有其他存活对象,如果这些对象总和超过了Survivor的50%,就会触发动态年龄判断机制,此时也会导致大量对象进入老年代,可以先通过增加年轻代内存来优化,参数设置如下:

-Xms1536M 
-Xmx1536M
-Xmn1024M
-Xss256K
-XX:SurvivorRatio=6  
-XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M 
-XX:+UseParNewGC  
-XX:+UseConcMarkSweepGC  
-XX:CMSInitiatingOccupancyFraction=92 //因为老年代缩小,为了保证老年代空间大小,需要加大老年代使用率比例
-XX:+UseCMSInitiatingOccupancyOnly 

得到新的内存模型:
在这里插入图片描述
此时执行jstat发现Full Gc次数比Minor gc的次数还多了,优化失败。
3、调大年轻代后还没有解决,这时发现元空间没有Full Gc,代码中也没有调用System.gc(),就有可能是触发了老年代空间担保机制,这可能是大量对象创建导致的,这时我们可以通过jmap -hisato pid 查看历史生成的实例:(这里使用visualvm工具查询)
在这里插入图片描述
发现一个Byte[]数组占用内存最多,并且User对象有19万个字节和8000多个实例,这是不正常的,首先排查代码中的User对象,发现其中有一行byte[] a = new byte[1024*100];一个数组就是100kb。并且每次调用queryUsers方法都会有8000个User实例创建,只是数组就有800多M,这样肯定会导致内存占用高,这样就找到了需要优化的代码,我们把代码queryUsers中循环次数改为800,这样每次只有80M对象生成,就可以保证在Minor Gc阶段就可以清理掉,运行之后,问题解决,优化成功。
如果这种new对象的操作比较多,代码位置难以定位的时候,还可以通过线程占用时间来定位问题,下图就可以看出也是queryUsers方法及其引用占用线程时间长
在这里插入图片描述

阿里巴巴Arthas

阿里巴巴2018年开源的Arthas也是JVM调优工具,支持 JDK6+, 采用命令行交互模式,可以用很简短的命令查询出很多运行参数,功能丰富,用于定位生产环境问题很方便。

常用使用场景
1、查询cup占用情况:dashboard命令可以查看整个进程的运行情况,线程、内存、GC、运行环境信息:
在这里插入图片描述
2、查看线程详细情况:thread
在这里插入图片描述
thread加上线程ID 可以查看线程堆栈,可以用于线程占用cup高时可以定位对应代码
在这里插入图片描述thread -b 可以查看线程死锁
在这里插入图片描述
3、反编译线上代码:jad加类的全名,可以反编译,可以查看线上代码是否是正确的版本请添加图片描述4、查看及修改线上运行代码的变量值:ognl
查看某个类中变量的值:ognl @全类名@hashSet
往变量中添加值:ognl ‘@全类名@hashSet.add(“123”)’

更多命令可以用help命令查看,或查看文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值