配合案例,理解真个对象分配和流转,以及 Minor GC 和 Full GC 的全过程,搞清楚以下问题:
- 对象在新生代分配
- 什么时候出触发 Minor GC?
- 触发 Minor GC 之前会如何检查老年代可用内存大小和新生代对象大小?
- 如何检查老年代可用内存大小和历次 Minor GC 之后升入老年代的平均对象大小?
- 什么情况下 Minor GC 之前会提前触发 Full GC?
- 什么情况下会直接触发 Minor GC?
- Minor GC 之后又哪几种情况对象会进入老年代?
1. 案例背景:日处理上亿数据的计算系统
- 计算系统功能:
- 不停的从数据源 MySQL 提取大量的数据,加载到自己的 JVM 内存里进行计算处理
- 关键性能因素:
- 每台机器大概每分钟大概执行 100 次数据提取和计算的任务;
- 每次大概会提取 10000 条左右的数据到内存里来计算;
- 每条数据都比较大,大概 20 个字段,平均每条数据大概 1KB 左右的大小;
- 每次计算大概耗时 10s 左右;
- 每台机器是 4核8G 的配置,JVM 内存给 4G,其中新生代和老年代分别是 1.5G 的内存空间;
- 默认,新生代是按照 8:1:1 比例分配 Eden 和两个 Survivor 区域,那么 Eden 区 1.2G、每块 Survivor 区域在 100MB;
2. 分析案例
(1) 系统多久会塞满新生代的内存空间?
- 每执行一次计算任务,就会在 Eden 里创建 10M 左右的对象,那么一分钟执行 100 次任务,基本上一分钟就会在 Eden 里创建 1G 的对象;
- 基本一分钟过后,新生代的 Eden 就快满了;
(2) 新生代触发 Minor GC 的时候会有多少对象进入老年代?
- Eden 在一分钟后就快满了,触发 Minor GC 来回收垃圾对象;
- 首先,检查老年代的可用内存空间是否大于新生代里的全部对象的总大小?
- 老年代有 1.5 G,Eden 里有 1.2G 的对象,即使 Eden 对象全部存活也能放到老年代里;
- 直接执行 Minor GC;
- GC 后,Eden 有多少对象是还存活的?
- 每次执行任务耗时 10s,(为了方便计算)估算只有最后10秒创建的对象是不可回收的,其他4/5的对象都是已经执行完毕的任务留下的垃圾对象
- 那么,估算一分钟后,只有 20个计算任务共计 200M 的数据仍在计算中;
- 一分钟 Eden 满了触发 Minor GC 后,会留下 200M 的存活对象,清理 1G 的垃圾对象;
- GC 后,200M 的 Eden 存活对象,无法转移到 100M 的 Survivor 区域,转移到 1.5G 可用空间的老年代;
(3) 系统多久会塞满老年代的内存空间?
- 每一分钟都是一个轮回,将 Eden 填满触发 Minor GC,然后转移 200M 的存活对象到老年代的内存空间;
- 假设系统运行了2分钟,老年代里转移了 400M 的对象,只有 1.1G 的可用空间;
- 假设现在第3分钟运行完毕,Eden 又满了,需要执行 Minor GC:
- 首先,检查老年代的可用空间是否大于新生代所有对象的大小?
- 老年代可用空间 1.1G,新生代对象有 1.2G,极端情况下,老年代存放不了新生代里的全部对象,进入下一步检查;
- 检查 -XX:-HandlePromotionFailure 是否被打开,一般都会打开,进入下一步检查;
- 检查老年代可用空间是否大于历次 Minor GC 过后进入老年代的对象的平均大小
- 老年代可用 1.1G,历次 Minor GC 都会有 200M 进入老年代;
- 基本可以推测,本次 Minor GC 后,大概率还是 200M 存活对象进入老年代,1.1 可用空间足够了;
- 首先,检查老年代的可用空间是否大于新生代所有对象的大小?
- 放心的执行一次 Minor GC,200M 存活对象进入老年代;
…..
- 假设现在是第7分钟后,7次 Minor GC 过后,有 1.4G 对象进入老年代,老年代只剩不到 100M 的可用空间,几乎快满了;
(4) 系统多久会触发一次 Full GC?
- 大概8分钟运行结束,新生代又满了,执行 Minor GC 之前进行检查,发现老年代只有 100M 可用,比历次进入老年代的对象大小平均值还小,直接触发一次 Full GC;
- 此时老年代里全部都是可回收的垃圾对象,Full GC 一次全部清理掉,老年代可用空间回到 1.5G;
- 按照这个运行模型,基本上平均七八分钟一次 Full GC,这个频率就相当高了。
- 因为每次 Full GC 速度很慢,造成系统性能很差;
3. 该案例如何进行 JVM 优化?
- 根据案例运行时的内存模型,我们可以发现最大的问题,是每次 Survivor 区域空间没有利用起来,Eden 存活对象直接转移到老年代,造成老年代频繁 Full GC;
- 优化思路:
- 调整新生代的内存比例,扩大新生代空间,每次 Minor GC 后存活对象尽量在新生代内流转;
- 3G 的堆内存,其中老年代减少到 1G,新生代增加到 2G,根据 8:1:1 的比例,Eden 1.6G、两个 Survivor 个 200M;
- 这样 Survivor 刚好可以放下 Minor GC 后的存活对象,那么等下一次 Minor GC 的时候,这个 Survivor 区的对象对应的计算任务就结束了,可以回收了;
- 基本上就很少对象会进入老年代,老年代里的对象也不会太多的;
- 这个优化可以把老年代的 Full GC 频率从几分钟一次降低到几小时一次,大幅提高了性能;
- 调整 Eden 和 Survivor 的比例,扩大 Survivor 区域空间,避免动态年龄判断规则直接把 Suvivor 中的对象转移到老年代;
- Eden 里都是新对象,Survivor 都是每次 Minor GC 后存活下来的大龄存活对象,如果年林1+年龄2+…+年龄N的对象大小总和>100M(Survivor的50%),就把年龄N以上的对象都转移到老年代;
- 现在要做的就是,避免这批大龄存活对象进入老年代,那么就可以扩大 Survivor 空间;
- 调整 -XX:SurvivorRatio=8 这个参数,默认是说 Eden 区比例为80%,也可以降低 Eden 区的比例,给两块 Survivor 区更多的内存空间,让对象尽量在新生代内流转;
- 调整新生代的内存比例,扩大新生代空间,每次 Minor GC 后存活对象尽量在新生代内流转;