4

jvm调优思路及调优案例 - 寻找的路上

 1 year ago
source link: https://www.cnblogs.com/process-h/p/16886918.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

jvm调优思路及调优案例

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

项目运行内存分析

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

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

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

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

1844129-20221113203628316-1219717320.png

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. 动态对象年龄判断(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

1844129-20221113203701002-9888352.png

示例程序启动后,我们调用测试类的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变化:

1844129-20221113203734704-1849139104.png

发现不仅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参数是否生效,优化后的内存模型为:

1844129-20221113203803685-532229073.png

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

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

1844129-20221113203814561-314084991.png

我们思考下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
1844129-20221113203830666-1894901247.png

查到了有大量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<User> users = queryUsers();

        for (User user: users) {
            //TODO 业务处理
            System.out.println("user:" + user.toString());
        }
        return "end";
    }

    /**
     * 模拟批量查询用户场景
     * @return
     */
    private ArrayList<User> queryUsers() {
        ArrayList<User> 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的各个调优知识点,相信有心的读者可以学到一些知识点。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK