• jvm调优思路及调优案例


    jvm调优思路及调优案例

    ​ 我们说jvm调优,其实就是不断测试调整jvm的运行参数,尽可能让对象都在新生代(Eden)里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。从而减少STW(stop the world)的时间。

    调优思路

    项目运行内存分析

    ​ 我们运行应用程序时,一般会设置一些jvm参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代大小,大对象的阈值,大龄对象进入老年代的阈值等。

    ​ 而设置这些jvm参数,有2种方式:

    1. 通过物理内存分析设置,比如机器有8G内存,假设操作系统分配2-3G,元空间分配256M,堆分配4-5G。
    2. 通过1设置之后,再通过分析具体的gc日志来调优。

    ​ 我们知道jvm有自己的运行时数据区(内存模型),其中堆大小,以及堆中的年轻代、老年代的大小比例至关重要,主要就是调整堆中的内存比例,运行时数据区(内存模型)图,如下图:

    具体思路

    1、分析年轻代对象增长的速率

    ​ 可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。

    2、Young GC的触发频率和每次耗时

    ​ 知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。

    3、每次Young GC后有多少对象存活和进入老年代

    ​ 这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。

    4、Full GC的触发频率和每次耗时

    ​ 知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

    总结:尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

    注意:对象进入老年代的几种方式:

    1. 大对象
    2. 对象到达一定年龄阈值
    3. 动态对象年龄判断(Young GC后的存活对象小于Survivor区域的50%)

    调优案例

    案例准备

    ​ 这里准备了一个示例程序(demo链接),运行以后,我们采用上篇文章介绍到的jstat工具查看各个内存gc的情况。

    初始JVM参数:

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

    根据这些参数,我们知道大体的内存模型是这样的:最快经过6s之后才会发生一次Young GC

    调优分析

    示例程序启动后,我们调用测试类的test()方法:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes={Application.class})// 指定启动类
    public class ApplicationTests {
    
    	@Bean
    	public RestTemplate restTemplate() {
    		return new RestTemplate();
    	}
    
    	@Autowired
    	private RestTemplate restTemplate;
    
    	@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);
    		}
    	}
    }
    

    然后观察整个过程前后,虚拟机的内存gc变化:

    发现不仅Young GC次数增多了,Full GC的次数也随着增多,说明对象不仅增长得快,连进入老年代的时间挺快的。

    我们回想一下对象进入老年代的几种方式:

    1. 大对象(代码排除没有大对象)
    2. 对象到达一定年龄阈值(通过Young GC观察没有达到15次)
    3. 动态对象年龄判断(Young GC后的存活对象小于Survivor区域的50%)

    所以应该是动态对象年龄判断机制导致Full GC次数变多了。我们可以尝试着优化下JVM参数,把年轻代适当调大点。

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

    可以通过jinfo查看JVM参数是否生效,优化后的内存模型为:

    优化后我们再重新跑一下程序,新的gc变化:

    ​ 优化完发现没什么变化,反而是Full GC次数还变多了。

    我们思考下full gc 比minor gc还多的原因有哪些?

    1、元空间不够导致的多余full gc

    2、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过-XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果

    3、老年代空间分配担保机制

    可以简单排除掉前2个原因,第三个老年代空间担保机制也可以通过观察minor gc 与full gc的次数比例进行排除,那接下来就可能真的就是程序产生了很多占内存的对象。我们可以通过jmapjvisualvm来跟踪到占内存的对象。

    jmap -histo 27808
    

    查到了有大量User对象生成,这个可能是问题所在,但不确定,还必须找到对应的代码确认,如何找到对应的代码有如下几种方式:

    1、代码里全文搜索生成User对象的地方(适合只有少数几处地方的情况)

    2、如果生成User对象的地方太多,无法定位具体代码,我们可以同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高,参考上一篇
    https://www.cnblogs.com/process-h/p/16879018.html

    最终定位到的代码如下:

    @RestController
    public class IndexController {
    
        @RequestMapping("/user/process")
        public String processUserData() throws InterruptedException {
            ArrayList users = queryUsers();
    
            for (User user: users) {
                //TODO 业务处理
                System.out.println("user:" + user.toString());
            }
            return "end";
        }
    
        /**
         * 模拟批量查询用户场景
         * @return
         */
        private ArrayList queryUsers() {
            ArrayList users = new ArrayList<>();
            for (int i = 0; i < 5000; i++) {
                users.add(new User(i,"zhuge"));
            }
            return users;
        }
    }
    
    public class User {
    	
    	private int id;
    	private String name;
    
    	// 1024B * 100 = 100KB
    	byte[] a = new byte[1024*100];
    
    	public User(){}
    
    	public User(int id, String name) {
    		super();
    		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;}
    
    }
    

    ​ 发现User类中定义了一个byte[] a 成员变量,占了100KB,在queryUsers()中,一次性在内存中添加了500M的对象数据,明显不合适,需要根据上述中的运行时内存数据区域阈值进行优化,尽量消除这种朝生夕死的对象导致的full GC.

    总结:到这里,调优案例就结束了,整个过程考虑了jvm的各个调优知识点,相信有心的读者可以学到一些知识点。

  • 相关阅读:
    【Flink】任务调度原理、自定义数据源、基本转换算子的使用之map
    Java(五)(Object类,克隆,Objects类,包装类,StringBuilder,StringJoiner,BigDecimal)
    Junit单元测试
    JDBC中execute、executeQuery和executeUpdate的区别
    学习笔记|串口通信的基础知识|同步/异步|RS232|常见的串口软件的参数|STC32G单片机视频开发教程(冲哥)|第二十集:串口通信基础
    手撕红黑树 | 变色+旋转你真的明白了吗?【超用心超详细图文解释 | 一篇学会Red_Black_Tree】
    详谈判断点在多边形内的七种方法
    基于springboot+vue的大学社团管理系统(前后端分离)
    Day12--渲染二级和三级分类列表
    Prometheus+Grafana普罗米修斯搭建+监控MySQL
  • 原文地址:https://www.cnblogs.com/process-h/p/16886918.html