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<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的各个调优知识点,相信有心的读者可以学到一些知识点。

最新文章

  1. OC中的extern,static,const
  2. AJAX应用优势
  3. C#事务的使用
  4. Linux 的启动流程
  5. poj 3728 The merchant 倍增lca求dp
  6. HDU 5024 Wang Xifeng&#39;s Little Plot(枚举)
  7. SETLOCAL
  8. hdoj 1257 DP||贪心
  9. [51nod1310]Chandrima and XOR
  10. 如何在mysql客户端即mysql提示符下执行操作系统命令
  11. Linux内核原理与分析-第一周作业
  12. js四则运算
  13. python_06 函数、全局变量与局部变量、函数递归
  14. TI am335x am437x PRU
  15. nginx:在linux上进行nginx的安装
  16. 26.mysql日志
  17. javascript柯里化
  18. ionic路由(页面切换)
  19. 大白话系列之C#委托与事件讲解(序言)
  20. [心平气和读经典]The TCP/IP Guide(002)

热门文章

  1. Mybatis-Plus使用@TableField实现自动填充日期
  2. 第九十一篇:Vue 具名插槽作用域
  3. 存储更弹性,详解 Fluid “ECI 环境数据访问” 新功能
  4. go 中解析JSON的三种姿势
  5. DataTable转Json格式
  6. 《Java基础——方法的调用》
  7. ImGUI 1.87 绘制D3D外部菜单
  8. Jupyter Notebook单元格加宽的方法3种
  9. 洛谷P1638 逛画展 (尺取法)
  10. 洛谷P1719 最大加权矩形 (DP/二维前缀和)