(1) 案例问题

  • 系统运行时,JVM 性能表现:

    • 机器配置:2核4G;
    • 堆内存:1.5G;
    • 新生代:512M,5:1:1,Eden 365M、每个 Survivor 70M;
    • 老年代:1G;
    • “-XX:CMSInitiatingOccupancyFraction=68”,一旦老年代内存占用68%,有680M左右的对象时,触发一次 Full GC;
    • 系统运行6天内发生的 Full GC 次数和耗时:250次,70多秒;
      • 平均每小时2次 Full GC;
    • 系统运行6天内发生的 Young GC 次数和耗时,2.6万次,1400秒;
      • 平均每分钟3次 Young GC;
        1. -Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=5 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=68 -XX:+CMSParallelRemarkEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC
  • 推导系统运行时内存模型:

    • 每隔20秒会让300多MB的 Eden 区占满触发一次 Young GC,一次 Young GC 耗时 50ms 左右;
    • 每隔30分钟会让老年代里 600多MB 空间占满,进而触发一次 CMS 的 GC,一次 Full GC 耗时 300ms 左右;
    • 通过 jstat 的观察,每次 Young GC 过后升入老年代里的对象很少,偶尔一次最多大概几十MB,因为 Survivor 70M,经常触发动态对象年龄判定,导致偶尔一次 Young GC 后有几十MB对象进入老年代;
    • 通过 jstat 追踪观察,并不是每次 Young GC 后都有几十MB对象进入老年代的,而是偶尔一次 Young GC 才会有几十MB对象进入老年代,记住,是偶尔一次!
      • 应该不至于30分钟就导致老年代占用空间达到68%;
    • 通过 jstat 观察到一个现象,老年代里的内存运行状态,会突然就有几百MB的对象进入老年代,大概有五六百MB的对象,一直占据在老年代中;
      • 应该是每隔一段时间就会产生几百MB的大对象进入老年代;
  • 结论
    • 通过推导分析,很有可能是系统运行的时候,每隔一段时间就会突然产生几百MB的大对象,直接进入老年代,不会走年轻代的 Eden 区,然后再配合年轻代还偶尔有 Young GC 后几十MB对象进入老年代,所以才会30分钟触发一次 Full GC!

image.png

(2) 优化思路

  • 如何定位系统的大对象?
    • 通过 jstat 观察系统,什么时候发现老年代里突然进入了几百MB的大对象,就立即用 jmap 工具导出一份 dump 内存快照;
    • 使用 jhat 或 Visual VM 之类的可视化工具来分析 dump 内存快照;
    • 通过内存快照的分析,定位出那几百MB的大对象,就是几个 Map 之类的数据结构;
    • 让负责开发系统的人员分析了一下,发现是从数据库查出来的(因为这个系统主要就是操作数据库);
    • 此时排查,只能使用笨办法,排查系统里的所有 SQL 语句;
    • 发现有条 SQL 特殊情况下会没有 where 条件,查出所有的数据,导致每隔一段时间会搞出几百兆的大对象;
  • 优化方案:

    • 解决代码中的 bug,不允许查询表中全部数据;
    • 调大年轻代,如果 Survivor 就 70MB 很容易触发动态年龄判定,让 YoungGC 后的几十MB对象进入老年代;
      • 调整为 年轻代 700MB,每个 Survivor 150MB,老年代 500MB;
    • 调整“-XX:CMSInitiatingOccupancyFraction=92”,避免过早触发 Full GC;
    • 主动设置 Metadata 256MB,如果不主动设置可能永久代默认就几十MB,很容易导致系统采用反射之类的机制,动态加载的 Class 对象过多,Metadata 区占满,频繁触发 Full GC;
      -Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=5 -XX:PermSize=256M -XX:MaxPermSize=256M -
      XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+CMSParallelRemarkEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC
      
  • 优化之后,大概每分钟一次 Young GC,一次在几十毫秒;大概十天一次 Full GC,一次耗时几百毫秒,频率很低;