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

jvm调优思路及调优案例的更多相关文章

  1. JVM调优思路

    一.jvm内存调优 (Gc  和 Full gc) hotspot -Xms40m  最小堆内存 -Xmx512m 最大值内存 -verboose:gc -XX:PrintGCDetails -XX: ...

  2. visualvm工具远程对linux服务器上的JVM虚拟机进行监控与调优

    文/朱季谦 最近在做了一些JVM监控与调优的事情,算是第一次实践,还比较陌生,故而先把这一次经验简单记下笔记,这样,对后面学习调优方面时,不至于又想不起来了.本文档主要总结在window本地环境远程对 ...

  3. JVM调优总结:调优方法

    JVM调优总结:调优方法 2012-01-10 14:35 和你在一起 和你在一起的博客 字号:T | T 下面文章将讲解JVM的调优工具以及如何去调优等等问题,还有一些异常问题的处理.详细请看下文. ...

  4. MySQL性能调优思路

    1.MySQL性能调优思路 如果一台服务器出现长时间负载过高 /周期性负载过大,或偶尔卡住如何来处理? 是周期性的变化还是偶尔问题?是服务器整体性能的问题, 还是某单条语句的问题? 具体到单条语句, ...

  5. jvm参数设置和性能调优

    1.Java虚拟机运行时的数据区 2.常用的内存区域调节参数 -Xms:初始堆大小,默认为物理内存的1/64(<1GB):默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40% ...

  6. JVM内存结构、参数调优和内存泄露分析

    1. JVM内存区域和参数配置 1.1 JVM内存结构 Java堆(Heap) Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都 ...

  7. JVM 调优 内存调优 CPU 使用调优 锁竞争调优 I/O 调优

    Twitter 工程师谈 JVM 调优 2016年03月24日 10:22:30 wenniuwuren https://blog.csdn.net/wenniuwuren/article/detai ...

  8. MindSpore模型精度调优实战:常用的定位精度调试调优思路

    摘要:在模型的开发过程中,精度达不到预期常常让人头疼.为了帮助用户解决模型调试调优的问题,我们为MindSpore量身定做了可视化调试调优组件:MindInsight. 本文分享自华为云社区<技 ...

  9. Spark性能调优之Shuffle调优

    Spark性能调优之Shuffle调优    • Spark底层shuffle的传输方式是使用netty传输,netty在进行网络传输的过程会申请堆外内存(netty是零拷贝),所以使用了堆外内存. ...

随机推荐

  1. 【C++】实现D3D9 的 Inline hook

    [C++]实现D3D9 的 Inline hook   简单介绍一下HOOK原理: 函数调用的过程大致是 先push 参数 进去,再执行 call 函数地址 ,进入函数.此时将所调用的函数的前五个字节 ...

  2. File类、FileOutputStream

    day01 File类 File类的每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径) 使用File可以做到: 1:访问其表示的文件或目录的属性信息,例如:名字,大小, ...

  3. 采云端&采云链:从订单协同到采购供应链,让采购供应链互联互通

    采购供应链安全从来没有像现在这样显得如此重要和紧迫,也从来没有像现在这样复杂和敏感,对企业的经营产生决定性的影响.尤其在疫情期间,采购供应链更加牵一发而动全身,成为"运筹帷幄,决胜于千里之外 ...

  4. K8s nginx-ingress 如何配置二级目录转发远程静态服务器基于Vue路由history模式打包的应用程序

    背景 首先这标题有点绕,我先解释下: 首先我们有静态服务器,上面某个目录有Vue路由history模式打包的应用程序(也就是build后的产物): 但是静态服务器一般不做对外域名用的,我们需要在k8s ...

  5. ProxySQL(9):ProxySQL的查询缓存功能

    文章转载自: https://www.cnblogs.com/f-ck-need-u/p/9314459.html ProxySQL支持查询缓存的功能,可以将后端返回的结果集缓存在自己的内存中,在某查 ...

  6. ingress-nginx自带认证功能【nginx自带】

    问题:通过nginx可以给某些web网站设置登录使用的用户名和密码,现在网站部署到k8s中,通过配置nginx-ingress->service->pod来访问的,怎么给这个网站再配置上访 ...

  7. 自定义View6 -塔防小游戏:第三篇防御塔随意放置+多组野怪

    第一篇:一个防御塔+多个野怪(简易版)第二篇:防御塔随意放置第三篇:防御塔随意放置+多组野怪 1.动态addView防御塔 2.防御塔放置后不可以移动 3.弯曲道路 4.素材替换 第四篇:多波野怪 第 ...

  8. 洛谷U81904 【模板】树的直径

    有负边权,所以用树形DP来找树的直径. 1 //树形DP求树的直径 2 #include<bits/stdc++.h> 3 using namespace std; 4 const int ...

  9. NOI2015 洛谷P1955 程序自动分析(并查集+离散化)

    这可能是我目前做过的最简单的一道noi题目了...... 先对e=1的处理,用并查集:再对e=0查询,如果这两个在同一集合中,则为""NO",最后都满足的话输出" ...

  10. 驱动开发:内核取ntoskrnl模块基地址

    模块是程序加载时被动态装载的,模块在装载后其存在于内存中同样存在一个内存基址,当我们需要操作这个模块时,通常第一步就是要得到该模块的内存基址,模块分为用户模块和内核模块,这里的用户模块指的是应用层进程 ...