15_日处理上亿数据系统案例
约 1450 字大约 5 分钟
2024-08-10
1、日处理上亿数据的计算系统
为了集中注意力理解这个系统 JVM 相关的东西,简化说明这个系统
系统会不停的从 MySQL 数据库以及其他数据源提取大量数据,加载到 JVM 内存中进行计算,大概每分中需要执行 500 次数据提取和计算任务
这是一套分布式运行的系统,部署了多台机器,每台机器每分中负责执行 100 次数据提取和计算任务
每次提取大概 1 万左右数据到内存中计算,平均每次大概需要耗费 10 秒钟左右的时间
每台机器配置是 4核8G,JVM 内存配置了 4G,新生代和老年代分别是 1.5G 内存
2、这个系统新生代多久塞满?
每台机器上部署的实例,每分中执行 100 次数据计算任务,每次计算 1 万条数据需要 10 秒时间,那我们可以看一看每次 1 万条数据大概占用多少内存
可以认为每条数据大小平均在 1KB 左右,那么每次计算任务的 1 万条数据就对应了 10MB 大小
如果新生代按照 8:1:1 的比例来分配 Eden 区和 Survivor 区,那么 Eden 区就是 1.2GB,每块 Survivor 在 150MB 的样子,如下图

按照上面分析的内存来计算,每次执行一个计算任务,就会在 Eden 区分配 10MB 左右的对象,那么一分钟大概对应 100 次计算任务
基本上一分钟过后,Eden 区里就全是对象,基本了满了
3、触发 Young GC 时会有多少对象进入老年代
1、假设
1分钟过后新生代塞满了对象,进行Young GC2、进行
Young GC之前,判断老年代可用内存空间是否大于新生代全部对象3、此时老年代是空的,大概有
1.5GB的可用内存空间,新生代就算他有1.2GB的对象也是可以放下的,直接进行Young GC4、每分钟执行
100个计算任务,假设80个计算任务都执行完毕,那么剩余20个计算任务,也就是200MB对象是存活的,不能给垃圾回收掉5、
200MB是大于Survivor区的150MB的,存活对象无法放入Survivor区,所以这200MB对象直接进入老年代

4、系统运行多久后老年代被填满
1、按照上述计算,每分钟就是一个轮回,大概每分钟就会把新生代
Eden区填满,触发一次Young GC,然后大概200MB左右数据进入老年代2、第
3分钟运行后进行Young GC,此时老年代有400MB内存占用,只有1.1 GB内存可用,如果-XX:-HandlePromotionFailure参数打开了(一般都会打开),就会判断老年代空间是否大于历次Young GC后进入老年代对象的平均大小3、历次
Young GC进入老年代的对象平均200MB,老年代1.1GB内存是大于的,此时可以放心的执行一次Young GC4、转折点是大概运行了
7分钟过后,7次的Young GC执行已经让大概1.4G的对象进入老年代了,老年代剩余空间100MB的样子

5、这个系统多久触发一次 Full GC
接着上面分析,大概在第 8 分钟运行结束的时候,新生代又满了,执行 Young GC 之前检查,发现老年代只有 100MB 内存可用了,比之前每次 Young GC 后进入老年代的 200MB 对象要小,此时会触发一次 Full GC
假设老年代被占据的 1.4G 内存空间的对象都是可以回收的,那么此时一次性就会把这些对象给回收掉,接着继续执行 Young GC,200MB 对象再次进入老年代
按照这个运行流程,基本上平均七八分钟触发一次 Full GC,这个频率相当高了,每次 Full GC 速度都很慢的,严重影响系统性能
6、JVM 优化
按照前面分析的内容,最大的问题是每次 Young GC,Survivor 区放不下存活对象
我们可以调整新生代的内存比例,3GB 左右的堆内存,可以分配 2GB 给新生代,1GB 留给老年代
这样 Survivor 区大概就是 200MB,差不多刚好可以放下 Young GC 过后存活对象了
还需要注意一点的就是,动态年龄判断提前进入老年代的规则,如果 Survivor 区中一批对象总大小超过了 Survivor 区内存的一半,就直接进入老年代了
所以这里的优化仅仅是做一个示例说明,意思是要增加 Survivor 区的大小,让 Young GC 后的对象进入 Survivor 区中,避免进入老年代
实际上为了避免动态年龄判定规则把 Survivor 区中的对象直接放到老年代,如果这里新生代内存有限,我们可以调整 -XX:SurvivorRatio=8 这个参数
Eden 区默认比例是 80%,可以降低 Eden 区的比例,给两块 Survivor 区更多的内存空间,让每次 Young GC 后的对象进入 Survivor 区中,还可以避免动态年龄判断规则直接让对象进入老年代