9-1 JVM调优:调什么?如何调?

调什么
  • 内存方面
    • JVM 需要的内存总大小
    • 各块内存的分配,新生代,老年代,存活区
    • 选择合适的垃圾回收算法,控制 GC 停顿次数和时间
    • 解决内存泄漏问题,辅助代码优化
    • 内存热点:检查哪些对象在系统中数量最大,辅助代码优化
  • 线程方面
    • 死锁检查,辅助代码优化
    • Dump 线程详细信息,查看线程内部运行情况,查找竞争线程,辅助代码优化
    • CPU 热点:检查系统哪些方法占用了大量 CPU 时间,辅助代码优化

如何调优
  • 监控 JVM 状态,主要是内存,线程,代码,I/O 这几个部分
  • 分析结果,判断是否需要优化
  • 调整,垃圾回收算法和内存分配;修改并优化代码
  • 不断重复监控,分析和调整,直至找到优化的平衡点

9-2 调优的目标,调优的策略和调优冷思考

调优的目标
  • GC 的时间足够小
  • GC 的次数足够的少
  • 将转移到老年代的对象数量降低到最小
  • 减少 Full GC 的执行时间
  • 发生 Full GC 的间隔足够的长

常见的调优策略
  • 减少创建对象的数量
  • 减少使用全局变量和大对象
  • 调整新生代,老年代的大小到最合适
  • 选择合适的 GC 收集器,并设置合理的参数

JVM调优冷思考
  • 多数的 Java 应用不需要在服务器上进行 GC 优化
  • 多数导致 GC 问题的 Java 应用,都不是因为参数设置错误,而是代码问题
  • 在应用上线前,先考虑将机器的 JVM 参数设置到最优(最适合)
  • JVM 优化上到最后不得已才采用的手段
  • 在实际使用中,分析 JVM 情况优化代码比优化 JVM 本身要多得多
  • 如下情况,通常不需要进行优化:
    • Minor GC 执行时间不到 50ms
    • Minor GC 执行时间不频繁,约 10s 一次
    • Full GC 执行时间不到 1s
    • Full GC 执行频率不算频繁,不低于 10 分钟 1

9-3 JVM调优经验,内存泄漏分析

JVM调优经验
  • 要注意 32 位和 64 位机的区别,通常 32 位的仅支持 2 - 3g 左右的内存
  • 要注意 Client 模式和 Server 模式的选择
  • 要想 GC 时间小必须要一个更小的堆;而要保证 GC 次数足够少,又必须保证一个更大的堆,这两个是有冲突的,只能取其平衡
  • 针对 JVM 堆堆设置,一般可以通过 -Xms -Xmx 限定其最小,最大值,为了防止垃圾收集器在最小,最大之间收缩堆而产生额外的时间,通常把最大,最小设置为相同值
  • 新生代和老年代将根据默认的比例(1 : 2)分配堆内存,可以通过调整二者之间的比率 NewRatio 来调整,也可以通过 -XX:newSize -XX:MaxNewSize 来设置其绝对大小,同样,为了防止新生的堆收缩,通常会把 -XX:newSize -XX:MaxNewSize 设置为同样大小
  • 合理规划新生代和老年代的大小
  • 如果应用存在大量的临时对象,应该选择更大的新生代;如果存在相对较多的持久对象,老年代应该适合增大。在抉择时应该本着 Full GC 尽量少的原则,让老年代尽量缓存常用对象,JVM 的默认比例 1 :2 也是这个道理
  • 通过观察应用一段时间,看其在峰值时老年代会占多少内存,在不影响 Full GC 的前提下,根据实际情况加大新生代,但应该给老年代至少预留 1/3 的增长空间
  • 线程堆栈的设置:每个线程默认会开启 1M 的堆栈,用于存放栈帧,调用参数,局部变量等,对大多数应用而言,这个默认值太大了,一般 256K 就足够用。在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程

内存泄漏的分析处理

内存泄漏导致系统崩溃前的一些现象,比如:

  • 每次垃圾回收的时间越来越长,Full GC 时间也延长到好几秒
  • Full GC 的次数越来越多,最频繁时隔不到 1 分钟就进行一次 Full GC
  • 老年代的呢你村越来越大,并且每次 Full GC 后老年代没有内存被释放
  • 老年代堆空间被占满的情况
    • 这种情况的解决方式:一般就是根据垃圾回收前后情况对比,同时根据对象引用情况分析,辅助去查找泄漏点
  • 堆栈溢出的情况
    通常抛出 java.lang.StackOverflowError 例外
    一般就是递归调用没退出,或者循环调用造成的

9-4 JVM调优实战:认识待调优的应用

调优实战

  • 重点是调优的过程,方法和思路
  • 内存调整,数据库连接调整,内存泄漏查找

9-5 JVM调优实战:录制JFR并分析结果

jmc 建立连接:

  • 主机:192.168.1.113
  • 端口:6666

VisualVM 建立连接:

  • 主机:192.168.1.113
  • 端口:6666

运行项目,在 jmc 监控下的 CPU 与 内存变化结果如图所示:

第九章 JVM调优 - 图1

我们可以看到,CPU 的占用率一直为 100%;并且在内存分析中,我们看到,出现了频繁 GC 的情况

VisualVM 反馈的结果也是完全一致的:

第九章 JVM调优 - 图2

在对 jmc 的飞行记录器分析中我们可以发现:

第九章 JVM调优 - 图3

2 min 的飞行记录,G1 New 发生了 70 多次 GC !这很有可能因为新生代分到的内存太小,所以导致这么频繁的发生 GC

修改 VM options 参数:

  1. -Xms800m -Xmx800m -Xmn350m

将新生代分配的内存扩大至 350m,重新开启 jmc 2min 的飞行记录

第九章 JVM调优 - 图4

我们看到 GC 的次数从原来的的 70 多次降低到了 50 多次,最大的暂停时长为 80 多毫秒,这种程度的优化还是远远不够的。

9-6 JVM调优实战:按照分析结果调整JVM运行时内存参数

我们再次改动 VM options 参数:

  1. -Xms1000m -Xmx1000m -Xmn500m

将堆内存扩大至 1G 左右,新生代分配 500M 和老年代的比例为 1 :1

数据库部分的优化:

通过分析 Druid ,分析数据库等待连接次数高达:38939 次;说明数据库连接设置过小

设置数据库的连接池个数为 100

  1. set GLOBAL max_connections=100

重启 Tomcat

飞行记录器的 2min 记录结果如下:

第九章 JVM调优 - 图5

我们看到,每次 GC 的最长暂停从原来的 80 多毫秒提升到了 20 多毫秒

我们继续调大数据库连接池个数为 1000

  1. set GLOBAL max_connections=1000

9-7 JVM调优实战:查找内存泄漏点,分析并处理内存泄漏

对程序的内存泄漏分析也是对 JVM调优重要的手段

我人为地在项目中添加内存泄漏点:

GoodService

  1. import java.util.List;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Service;
  4. import org.springframework.transaction.annotation.Transactional;
  5. import com.cc.common.service.BaseService;
  6. import com.cc.goodsmgr.dao.GoodsDAO;
  7. import com.cc.goodsmgr.vo.GoodsModel;
  8. import com.cc.goodsmgr.vo.GoodsQueryModel;
  9. import com.cc.pageutil.Page;
  10. @Service
  11. @Transactional
  12. public class GoodsService extends BaseService<GoodsModel,GoodsQueryModel> implements IGoodsService{
  13. private GoodsDAO dao = null;
  14. @Autowired
  15. private void setDao(GoodsDAO dao){
  16. this.dao = dao;
  17. super.setDAO(dao);
  18. }
  19. @Override
  20. public Page<GoodsModel> getByConditionPage(GoodsQueryModel qm){
  21. List<GoodsModel> list = dao.getByConditionPage(qm);
  22. qm.getPage().setResult(list);
  23. // 添加内存泄漏点
  24. MyData.addList();
  25. return qm.getPage();
  26. }
  27. }

MyData

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. public class MyData {
  4. private static List<MyModel> list = new ArrayList<MyModel>();
  5. public static void addList() {
  6. list.add(new MyModel());
  7. }
  8. }

MyModel

  1. public class MyModel {
  2. private byte[] bs = new byte[100*1024];
  3. }

重新启动项目,观察 MBean 的变化趋势:

第九章 JVM调优 - 图6

我们可以看到,堆内存中有特别明显的上升趋势,已经撑到了我们设置的堆最大内存空间

对于这种情况,我们需要怎样分析?

我们可以打开 VisualVM 的对象占用内存情况:

第九章 JVM调优 - 图7

我们看到最高的占用内存就是 byte[] 数组,我们可以对程序跟踪所有涵盖 byte[] 数组的地方,逐一分析,就可以找到内存泄漏源。

当然,更简单的手段就是录制 JFR ,看飞行记录结果进行分析:

第九章 JVM调优 - 图8

同样地,通过 JFR 我们可以看到,byte[] 数组占用的内存为 1.13G,内存逐步升高,很有可能发生了内存泄漏。

看内存部分:

第九章 JVM调优 - 图9

我们通过堆栈跟踪,就可以看到是哪个类,哪个方法,出现了内存泄漏的可能。