结合一个计算任务系统,来分析 JVM 堆内存的整个执行过程。

系统背景


这是一个用来处理数据的系统,主要任务就是不断从 MySQL 获取数据,然后进行计算,计算完毕后,存入 MySQL 。

每分钟大致进行100次数据提取和计算任务。每次提取1万条数据到内存进行计算,每次计算大概消耗10秒左右的时间。每次提取的数据对象大概在 10M。

由于是专用来执行这个系统的服务器,配置不高 4核 8G。新生代和老年代分别设置了 1.5G 空间。堆内存按照 8:1:1 分配的话,大致上 Eden 区是 1.2G ,2个 Survivor 区分别是 100M 。

内存分配回收过程分析


新生代多久就会满了?

基于系统的情况,每次计算任务生成 10M 对象,每分钟运行 100次的话,那么1分钟就会产生 100 * 10M = 1000M 左右的对象,新生代基本就满了。 就会触发 Minor GC。

Minor GC 后,有多少对象进入老年代?

第1次进行 Minor GC 的时候,检查老年代的剩余空间,发现有 1.5G ,如果 Eden 区全部对象存活 1.2G ,也可以放下。所以可以直接进行 Minor GC。

假设现在1分钟过去了,进行了 100 次数据的处理,但是有 20 次数据还没有处理完。那就是还有 200M 的对象正在进行计算中,还有引用,这 200M 的对象就是存活下来的对象。 进行 Minor GC 的时候发现存活的对象放不进 Survivor 区,于是通过空间担保机制,这 200M 对象就直接进入了老年代。然后 Eden 区就会被清空。

多久后老年代会被占满?

现在的情况是,每分钟就会产生 200M 的对象进入老年代。运行过程是:

第1次进行 Minor GC ,发现老年代剩余空间(1.5G)可以放下 Eden 区在极限存活下的所有对象(1.2G),直接进行 Minor GC ,存活对象 200M 进入老年代。
第2次进行 Minor GC ,发现老年代剩余空间(1.3G)可以放下 Eden 区在极限存活下的所有对象(1.2G),直接进行 Minor GC ,存活对象 200M 进入老年代。
第3次进行 Minor GC ,发现老年代剩余空间(1.1G)不能放下 Eden 区在极限存活下的所有对象(1.2G),就会去检查剩余空间是否满足之前平均进入老年代的存活对象大小(200M)。发现可以满足,直接进行 Minor GC ,存活对象 200M 进入老年代。
如此进行到第7次后第8次 Minor GC 的时候,老年代只剩下 100M 空间了,剩余空间比评论进入老年代的存活对象(200M)要小了。就会出发一次 Full GC,对老年代空间进行清理。这时候,老年代就恢复 1.5G 的剩余空间,又可以放下存活对象了。

也就是说,这个系统,每8分钟,老年代就会满了。每8次 Minor GC 就会触发一次 Full GC。

进行优化


整个系统的运行流程,最大的问题是,Survivor 区装不下 Eden 区存活的对象,导致大量存活对象直接晋升到老年代。 这时候,如果能扩大整个 JVM 内存就更好,如果不行的话,堆总内存还是 3G 的话,就可以将 Eden 区的空间调大,将老年代的空间调整小。

例如将 2G 分配给新生代,那么 Eden 区的就会有 1.6G ,Survivor 区就会有 200M ,进行 Minor GC 后,存活的对象就会进入 Survivor1 区,而不会进入老年代。 下次进行 Minor GC 的时候,存活的对象就会进入 Survivor2 区,Eden 区和 Survivor1 区就会被清理掉。

如此进行的话,就仅仅会有少量的对象进入到老年代。这样就避免了性能很差频繁的 Full GC 。提升了系统的性能。

只是一个简单的分析过程,忽略了动态年龄机制。