临近双11期间,大家都忙着发布各种优化版本,程序猿手起键落,满意的敲下最后一个回车键,心里想着这就是双十一最终版了,然而不知道等着他的是下一个双十一最终版……

版本上线后,观察了几天,程序猿发现一个异常现象,之前一直非常平稳的JVM老生代突然在上线后以有了明显的增长,而且是持续的增长。于是开始了这次老生代过快增长的问题排查……

揪出导致老生代快速增长的对象分析内存对象

先得找个好用的工具,淘宝开源的TBJMap对jmap做了个增强版,可以将JVM新老生代的各个对象实例个数和大小的Histgram打印出来。

想看出老生代里是什么对象在不断增长,用TBJMap的改版(同事做的优化版)分别dump两天的老生代对象实例出来。 第一天的dump:

从这份dump中发现JDBC4Connection这个类有些奇怪,程序里是用了连接池的,但这里的连接实例个数却有3027个,远远超过连接池配置的最大连接数,带着这个疑问,再看看第二天的dump:

两份dump对比,不难发现老生代里明显增长的对象就是这个JDBC4Connection(其他几个都是和这个类相关引用的)。 到底是哪里来的这些数据库连接呢?

注:这里采用连接池是c3p0, 版本是c3p0-0.9.5-pre8 紧跟用jmap将整个heap dump下来,通过MAT进行分析,JDBC4Connection这个对象一共有3792个实例,占用了100多M内存。

其中一个实例的GC Root Path:

可见它被两个对象引用,一个是连接池BasicResourcePool的unused成员变量,一个是MySQL JDBC Driver的ConnectionPhantomReference,跟踪到源码中看看:

BasicResourcePool.java

可见unused对象是连接池里的正常可用的连接。

NonRegisteringDriver.java

protected static void trackConnection(Connection newConn) {
ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl) newConn, refQueue);
connectionPhantomRefs.put(phantomRef, phantomRef);
}

这个虚引用是在JDBC Driver在构造connection时用来track这个connection的,在被GC回收前做一些clean up(释放资源)的事情,所以每个connection被构造出来后,都会被track,这是Driver为了防止有资源随着连接回收而未释放的手段。

可见,这个JDBC4Connection实例是堂堂正正的连接池里的连接实例,呆在老生代是理所当然的。

再来看另一个:

这个实例除了ConnectionPhantomReference引用外,还有一个也是连接池BasicResourcePool里的对象:formerResources,顾名思义:曾经在连接池里呆过的对象。

再检查其他的JDBC4Connection实例的GC Root Path,大部分都是第二个这种,被formerResources引用到的。

看到这里,基本已经清晰了,这些多出来的连接对象,应该是曾经被连接池里创建出来,用完后被抛弃掉的连接,被放到formerResources,这些对象熬过了几轮YGC,到了老生代,等着被老生代GC回收的”退休”对象。

真相渐渐浮出水面

继续跟进加入formerResources的相关代码:

private void removeResource(Object resc, boolean synchronous){
    ...
    unused.remove(resc);
    destroyResource(resc, synchronous, checked_out);

addToFormerResources( resc );

asyncFireResourceRemoved( resc, false, managed.size(), unused.size(), excluded.size() );
  ...}private void cullExpired(){
  ...
    if ( shouldExpire( resc ) )
    {
        if ( logger.isLoggable( MLevel.FINER ) )
            logger.log( MLevel.FINER, "Removing expired resource: " + resc + " [" + this + "]");

target_pool_size = Math.max( min, target_pool_size - 1 ); //expiring a resource resources the target size to match

removeResource( resc );
    }
  ...}public Object checkoutResource( long timeout ){

Object resc = prelimCheckoutResource( timeout );...}private synchronized Object prelimCheckoutResource( long timeout ){
  ...
    Object  resc = unused.get(0);
  ...
    else if ( shouldExpire( resc ) ){
      removeResource( resc );
      ensureMinResources();
      return prelimCheckoutResource( timeout );
}
}

重点关注removeResource这个方法,这个是连接池管理连接的一个重要方法,大致浏览了下调用它的地方,注意到cullExpired和checkoutResource这两处:

cullExpired这个方法是c3p0的一个定时器里执行的方法,用来检查过期连接的,如果配置了相关的连接池参数(max_resource_age,max_idle_time,excess_max_idle_time,destroy_unreturned_resc_time),就会对过期连接进行清理;

再结合应用的配置,maxIdleTime设置了1800秒,结合代码逻辑,c3p0默认会1800/4=450秒一次定时对idle连接进行连接池过期检查,若连接过期(空闲了1800秒),则会被remove,被加入到formerResources中,如果最后剩下的idle连接超过或小于minIdle的连接数,也会相应的进行缩减(shrink)或者扩充(explode)连接池的连接,达到minIdle个连接。

有同学可能会问:“如果设置了minIdle和idleConnectionTestPeriod,这里的连接有keep alive,是否能逃过被清理的检查?”

回答是:“无法逃过,idleConnectionTest是维持连接池的idle连接和MySQL之间的心跳,防止MySQL server端踢掉应用的连接”,而前面提到的连接池过期检查则是c3p0对连接归还后是否长时间没被再次借出为依据来判断连接是否已过期。”

所以,即时是静静地呆着啥也不干,若配置了过期检查参数,这些keep alived的连接也会被认为过期而清理掉,然后重新生成一批新的,但啥也不干的时候,YGC也会很久一次,所以这些定期被清理掉的连接很可能熬不到老生代就被YGC回收了。

checkoutResource 则是从连接池中获取连接的方法,从代码中可以看出每次获取连接时,都会对该连接进行过期检查的校验,同样还是上面那些参数,比如配置了maxIdleTime是1800秒,检查时若发现连接上次借出归还后超过1800s没有再次被借出,则认为连接已过期,调用removeResource,将连接加入到formerResources中。

最后,结合应用对MySQL的调用场景:数据先从缓存(Redis)中获取,若发生异常,则回源MySQL。优化版的改动增加了回源MySQL的概率,所以,若是已经过期的连接,之前是定时450秒做一次检查,也就是可能会等450秒才被remove,然后再次生产新的连接;结果现在,可能没到450秒,而是通过checkout的同时就做了检查,加快了过期连接的失效和创建新连接的速度,导致formerResources里的”退休”连接变多,最终加快了老生代的增长。

如何解决

直接去掉maxIdleTime的配置,这个并不影响连接保活心跳,c3p0里对连接保活是在另外一个线程进行的,只要配置了idleConnectionTestPeriod这个参数即可。

双11后的更新

从这个具体的问题中跳出来,其实这是一类问题:持久化对象在JVM中的存活生命周期问题,连接池对象,本地缓存对象都是,这些对象存活时间久,处于JVM的老生代中,应用希望尽可能的重用它们,但若结合具体场景的配置或使用不合理,导致这些对象并未最大化被重用,比如上面提到的过期检查导致不断有新的对象被创建出来,因为是持久化对象,很容易就进入到了老生代,霸占了资源。

同样的问题,JedisPool也存在,双11刚过,在流量高峰期,发现应用的老生代不断涨:

很明显,在晚高峰20:00开始前,老生代是很安静的,从买买买开始,就不正常了,直到0点过后又恢复平静。

一样的思路找下去,发现也是同样的问题,这是JedisPool默认的配置:

每30秒进行一次扫描,如果发现有idle超过60秒的连接,则进行清除,若少于minIdle个连接,则再创建新的。

而清除掉的连接若未及时的被YGC掉,就会溜进老生代。再看看这段时间YGC的频率:

从20:00开始到0点,每分钟YGC 3-4次,而JVM配置的-XX:MaxTenuringThreshold=4,所以idle的连接被清除掉后,再创建出来新的连接,到1分钟后的下次检查到过期期间,很容易熬过4次YGC,晋升到老生代。

为什么高峰期还有idle的连接,配置的minIdle=8,然而Redis的响应太快,1个连接就可以轻松处理>1000QPS的请求量,再加上100个redis实例,每个实例1个连接,即可处理10WQPS(若请求是均匀散落到redis上),所以在高峰期,仍然有部分的连接是idle的。

怎么解决呢?继续看代码:

在commons-pool的过期策略中发现minEvictableIdleTimeMills有一个孪生兄弟softMinEvictableIdleTimeMillis(即idleSoftEvictTime),如果idle的时间超过这个配置值,且当前idle的连接个数比minIdle要多,则进行清除。

所以解决办法是配置minEvictableIdleTimeMills=-1,softMinEvictableIdleTimeMillis=60000,这样就不会对minIdle的连接进行清理,只有当连接数超过minIdle后,才进行清理工作。

优化版本上线后的附加效果

[优化版本] 12小时老生代变化趋势:

[未优化版本] 12小时老生代变化趋势:

除了解决老生代增长问题外,新生代的YGC也有明显改善:

[优化版本]gc.log:

2017-11-16T09:54:04.202+0800: 68118.496: [GC 68118.496: [ParNew: 1680716K->2934K(1887488K), 0.0074850 secs] 1786721K->108960K(3984640K), 0.0077080 secs] [Times: user=0.07 sys=0.00, real=0.01 secs]
Total time for which application threads were stopped: 0.0082120 seconds
Application time: 854.0418560 seconds
2017-11-16T10:08:18.252+0800: 68972.546: [GC 68972.546: [ParNew: 1680758K->3042K(1887488K), 0.0081860 secs] 1786784K->109098K(3984640K), 0.0083960 secs] [Times: user=0.07 sys=0.00, real=0.01 secs]

[未优化版本]gc.log:

2017-11-16T09:53:26.996+0800: 67675.370: [GC 67675.370: [ParNew: 1718298K->39709K(1887488K), 0.0267350 secs] 1870355K->192401K(3984640K), 0.0269810 secs] [Times: user=0.40 sys=0.00, real=0.02 secs]
Total time for which application threads were stopped: 0.0274670 seconds
Application time: 784.6489160 seconds
2017-11-16T10:06:31.672+0800: 68460.046: [GC 68460.046: [ParNew: 1717533K->38543K(1887488K), 0.0248460 secs] 1870225K->192008K(3984640K), 0.0250960 secs] [Times: user=0.35 sys=0.00, real=0.03 secs]

新版YGC停顿时间: 7-8ms

旧版YGC停顿时间: 20+ms

另外,新版的YGC频率也降低了。

避免了JedisPool里minIdle中池化对象的不断创建/销毁动作,YGC完后剩余的对象少了很多,YGC的搬迁工作减轻不少,所以停顿时间也大幅缩短了。

JVM老生代增长过快问题排查相关推荐

  1. JVM 新生代老生代

    堆大小 = 新生代 + 老年代.默认下,新生代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小: 新生代 ( Young ) 被细分为 Eden 和 两 ...

  2. 闲谈JVM(二):浅析新老生代参数配置

    文章目录 前言 Heap区新老生代 新生代参数配置 NewSize MaxNewSize Xmn NewRatio SurvivorRatio 新生代的GC 老生代参数配置 对象何时进入老生代? 老生 ...

  3. 一次完整的JVM堆外内存泄漏故障排查记录

    前言 记录一次线上JVM堆外内存泄漏问题的排查过程与思路,其中夹带一些JVM内存分配机制以及常用的JVM问题排查指令和工具分享,希望对大家有所帮助. 在整个排查过程中,我也走了不少弯路,但是在文章中我 ...

  4. 堆及新生代老生代(学习笔记)

    来自颜群老师的JVM课程 小结 新生代特点 大部分对象都存在新生代 新生代的回收频率高,效率高 老生代特点 空间大,增长速度慢 垃圾回收频率低 划分新老生代的意义 可以根据项目中对象大小的数量,设置新 ...

  5. Java新生代 与老生代

    堆大小 = 新生代 + 老年代.默认下,新生代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小: 新生代 ( Young ) 被细分为 Eden 和 两 ...

  6. jvm_2_新生代老生代含义

    题眼: JVM使用的是分代垃圾回收的方式,可以将Java对象分为"年轻"对象和"年老"对象. JVM将内存堆(Heap)分为两个区域,一个是"年轻&q ...

  7. jvm参数调优_3_问题排查

    相关文章: http://www.importnew.com/21441.html(Java系列笔记(4) - JVM监控与调优 - Daniel·广 - 博客园) https://lanjingli ...

  8. JVM分代回收机制和垃圾回收算法

    JVM系列文章目录 初识JVM 深入理解JVM内存区域 玩转JVM对象和引用 JVM分代回收机制和垃圾回收算法 细谈JVM垃圾回收与部分底层实现 Class文件结构及深入字节码指令 玩转类加载和类加载 ...

  9. java 监控 native 内存_记一次java native memory增长问题的排查

    1.摘要 最近排查了一个比较灵异的线上jvm内存持续增长的问题,排查过程异常艰辛,但是最后竟然是用最简单的办法搞定了-- 2.现象 线上机器部署了两个java实例,在运行几天后java开始吃swap空 ...

最新文章

  1. vs2015编译linux源码,使用Visual Studio 2017(VS2017)编译OpenCC 1.0.4 (Open Chinese Convert)源代码...
  2. 软件工程概论课堂作业3
  3. angularJs关于指令的一些冷门属性
  4. ImportError: libgdal.so.20: cannot open shared object file: No such file or directory
  5. tab和TabHost
  6. PLC的编程与应用学习笔记
  7. 关于MyEclipse 10 破解程序打开的原因
  8. 不用工具直接从微软官网下载Win10正式版ISO镜像的技巧
  9. hiberfil.sys是什么文件?可以删除吗?
  10. 操作系统:手把手带你扫盲 操作系统 的那些必知必会!
  11. Linux系统接口ioc,Linux下SCSI API研究及应用
  12. E450连接蓝牙耳机
  13. ios, android平台手机游戏,《王者荣耀》ios和安卓怎么转平台 ios和安卓转平台攻略...
  14. 5G NR - MAC RLC PDCP SDAP
  15. linux的c文件报错for,linux c 编译错误 conflicting types for 的解决办法
  16. 【聚类算法】基于matlab划分法k-means聚类算法【含Matlab源码 1941期】
  17. 秃头大牛一文竟然就把SpringCloudStream(SCS)给讲明白了?
  18. Autoencoder-based Zeroth Order Optimization Method for Attacking Black-box Neural Networks
  19. u-boot编译与烧录(二)
  20. 零基础想学好编程!C语言最难啃的 3 块硬骨头,你全吃透了吗?

热门文章

  1. 微信扫描二维码跳转页面
  2. STM32芯片替代方案 | 从原厂资料中获取GD32F103VET6通信I2C接口电路设计 | 第四集
  3. MB10F-ASEMI整流桥MB10F
  4. [XCTF-pwn] 31_ciscn-2018-Quals_house_of_grey
  5. Vue的属性,为什么无法冻结?
  6. 写代码不行,考研没考上,怎么办?
  7. Python实例——身体质量指数BMI
  8. 宿州计算机学院录取分,宿州学院录取分数线2021是多少分(附历年录取分数线)
  9. 从数组中找出最大的两个数
  10. Windows Server 2008 安装SVN