浅谈新生代为什么要分三块区域并且比例为什么是8:1:1

发布时间 2023-07-31 19:38:36作者: 木木他爹

       如题,最近在网上看到了一个某大厂的面试题:“新生代为什么分三块区域且比例为什么是8:1:1"?网上答案比比皆是,我是没搜到什么有价值的答案,今天结合这个题目谈谈自己的粗浅想法,如有不对还望指正;另外需要说明的是,接下来聊的都是基于G1之前的垃圾收集器;
       首先,我们假设新生代如果不分代会发生什么:如果不分代的话那么堆内存就是由一块新生代,一块老年代组成,当发生mionrGc时,收集器不管采用的是古老的Serial还是Parallel Scavenge,都会使用标记-复制算法将新生代中存活的对象直接复制到老年代;这时候会产生如下问题:

  1. 原先本来可作为担保空间的老年代直接用来容纳了新生代每次gc存活的对象,导致了标记-复制算法没有了担保空间,大大新增了fullGC发生的概率和次数
  2. 无论对象年龄与否,只要熬过一次垃圾回收即可进入老年代,使进入老年代的成本降低,加大了老年代的负担,同样会增加fullGc的次数

tips
正常情况下,对象何时进入老年代呢?
1、对象熬过一定的垃圾回收次数(不同的收集器要求不同,最大不会超过15)后进入老年代
2、动态年龄判定:新生代每次进行垃圾回收时,当e+s0或1区对象复制到另一个s1或0区时,会将对象年龄按从小到大的顺序进行累加,当对象大小累加到超过s1或0区的目标存活率时(默认50%),年龄大于等于当前计算对象年龄的对象直接进入老年代
3、分配在eden区的对象是大对象时直接到老年代分配

所以由上可得,新生代必须分代,此时我们假设将新生代分成一个e区一个s区,如图:

       我们来看下在此分代模式下进行mionrGC会发生什么,第一次gc时存活对象由e区进入s区然后清空e区,此时老年代也可以起担保作用,一切看起来都很正常;但是如果再发生第二次gc会怎样呢,此时e区有存活对象,而容纳第一次gc时存活对象的s区也肯定会有存活对象,此时需要将e,s区的存活对象复制到老年代;此时,问题也就显而易见了,与不分代的时候情形一样了,对象只能进入老年代;
       所以我们要将新生代再开辟一块区域来当作一个拦截器,不让不达年龄的对象直接接入老年代;结构如图:

       此时新生代分成了e区,s0区和s1区;这时候再发生mionrGc就不会有上面出现的问题了:第一次GC时存活对象由e进入s0,然后清空e区;第二次gc时将e区和s0区的存活对象复制到s1区,清空e区和s0;第三次将e区和s1的对象复制到s0,清空e和s1......如此反复,直到对象年龄达标或者s区(s0或s1)容不下存活对象时再晋升到老年代,这样也就降低了老年代的压力,减少了fullGc的次数;

  • 以上的分析假设了各种不分块或分两块的利弊,所以个人认为的新生代分区或分三区的原因如下:
    1、降低老年代的内存分配压力,通过设置两个s区来对年轻对象进行拦截,降低fullGc的次数
    2、分三代能使老年代作为担保来应付s区容不下存活对象的情况

tips
GC的分代假说:
1、弱分代假说:大部分对象都是朝生夕灭的
2、强分代假说:熬过越多次垃圾收集的对象越难被回收
3、跨代引用假说:跨代引用的对象相比同代引用属于极少数

接下来我们来讨论下为什么e:s0:s1默认是8:1:1?
       根据<深入理解虚拟机>一书里的描述,IBM的一项研究表明新生代中有98%的对象是朝生夕灭的,换言之,每次mionrGC后存活的对象应该小于等于2%,所以看起来采用复制算法的新生代似乎可以不用将内存分成大小相等的两块了,但考虑到实验偏差以及实际情况的多样性,jvm默认预留了10%的内存用于存放存活对象,此时结合上文描述的,新生代最优应该分成三块,所以得再预留一块10%的内存给s区,那么自然剩下的80%就是e区的大小了;