1. 案例介绍

(1) 系统背景

  • 一个百万计注册用户的在线教育平台;
  • 主要目标用户群体是几岁到十几岁的孩子;
  • 注册用户大概几百万规模,日活用户规模大概在几十万;
  • 用户白天上学,一般晚上放学之后到八九点,还有周末,都是是这个平台最活跃的时候;
  • 晚上两三个小时是这个平台的每天的高峰期,99%的流量都集中在晚上;
  • 这个平台的高频操作就是上课,像浏览课程详情、下单付费、选课排课都是低频行为,低频行为我们暂时忽略掉;

    (2) 核心业务流程
  • 孩子们在上课的时候,核心的业务流程就是大量的互动环节,在娱乐游戏中进行教学;

  • 比如在完成什么任务的时候必须点击很多的按钮,频繁的进行互动,然后系统后台需要接收大量的互动请求,并且记录下来用户的互动过程和互动结果,做了多少个任务,对了几个,错了几个;

    (3) 高峰运行内存压力分析
  • 每秒请求压力

    • 假设晚上高峰3小时共有60万活跃用户,平均每个用户大概使用1个小时上课,那么每小时大概20万活跃用户;
    • 假设每个用户,一分钟进行1次互动操作,即一个小时进行60次互动操作;
    • 20万用户在1小时内会进行1200万次互动操作,即平均每秒大概3000次的操作;
    • 假设部署5台4核8G的机器,每台机器每秒产生600个请求;
  • 每秒内存使用压力
    • 一次互动请求不会有太复杂的对象,主要记录一些用户互动过程,可能跟一些积分有关联的东西;
    • 假设一次互动请求会创建几个对象,占5KB的内存;
    • 每秒会在内存中创建 600*5=3MB 对象;

2. 基于内存使用模型进行 JVM 优化

  • 4核8G的机器,每台机器每秒会有600个请求,会占用 3MB 的内存空间;

    (1) G1 默认内存布局和设置

  • 假设分配给堆内存 4G,其中新生代初始占比5%,最大占比60%,每个线程栈内存为1M,方法区的内存一般为256MB,JVM 参数如下:

    • 每个 Region 大小 4096/2048=2M;
    • 新生代初始 Region 数量 2048*0.05=100 个,共有 200M 空间;
  • 每触发一次 GC 的时候导致的 Stop the World 时间不超过 200ms;

    1. -Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC

    (2) 多长时间会触发新生代 GC?

  • 触发机制与 ParNew 不一样:

    • ParNew 新生代 GC 触发时机:Eden 区域的空间不够了,就触发 Minor GC;
    • G1 新生代 GC 触发时机:根据你预设的“-XX:MaxGCPauseMills”GC 停顿时间,给新生代分配一些 Region,然后到一定程度就触发 GC,并且把 GC 时间控制在预设范围内,尽量避免一次性回收过多的 Region 导致 GC 停顿时间超过预期;
  • 示例:

    • 假设这个系统里, G1 回收 300 个 Region(600M),大概需要 200ms;
    • 系统运行,每秒创建 3M 的对象,大概一分钟左右就会占满新生代的初始空间 100 个 Region,G1 回收区区 200M 可能只需要大概几十 ms,继续等待;
    • 继续给新生代增加一些 Region,在新生代里分配对象,一直到可能 300 个 Region 都占满;
    • 此时通过计算发现回收这个 300 个 Region 大概需要 200 ms,那么就触发一次新生代 GC;

      (3) 新生代 GC 如何优化?

  • 首先需要给 JVM 堆内存分配足够的内存;

  • 合理的设置“-XX:MaxGCPauseMills”GC 停顿时间,尽量让系统的 GC 频率别太高,同时每次 GC 停顿时间别太长,达到一个理想的合理值;

    • 这个合理值,需要结合系统压测工具、GC 日志、内存分析工具一起进行考虑;

      (4) Mixed GC 如何优化?

  • 首先,mixed gc 的触发时机:老年代在堆内存占比超过 45% 就会触发;

    • 优化思路:避免对象过快的进入老年代,避免频繁触发 mixed gc;
  • 其次,核心还是“-XX:MaxGCPauseMills”的合理设置;
    • 在保证新生代 gc 不太频繁的同时,还得考虑每次 gc 过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发 mixed gc;