新生代为什么要分为三块和复制算法操作

新生代内存分为那三块内存?使用复制算法回收是怎么操作?

我们知道,针对新生代的垃圾回收算法,他叫做复制算法

我们知道,系统运行时,对JVM内存的使用模型,大体上就是我们的代码不停的创建对象然后分配在新生代里,但是一般很快那个对象就没人引用了,成了垃圾对象。

接着一段时间过后,新生代就满了,此时就会回收掉那些垃圾对象,空出来内存空间,给后续其他的对象来使用。

其实绝大多数的对象都是存活周期非常短的对象,可能被创建出来1毫秒之后就没人引用了,他就是垃圾对象了。

所以大家可以想象一下,可能一次新生代垃圾回收过后,99%的对象其实都被垃圾回收了,就1%的对象存活了下来,可能就是一些长期存活的对象,或者还没使用完的对象。

所以实际上真正的复制算法会做出如下优化,把新生代内存区域划分为三块:

1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说假设堆内存是2G,分配给新生代是1G。那么Eden区有800MB内存,每一块Survivor区就100MB内存,如下图。(为了方便说明,我把左边的survivor区称之为s1,右边的survivor区称之为s2)

image-20191216163330899

平时可以使用的,就是Eden区和其中一块Survivor区(s1),另一块survivor是用来在ygc后保存ygc存活的对象,那么相当于就是有900MB的内存是可以使用的,如下图所示。

image-20191216163412407

但是刚开始对象都是分配在Eden区内的,如果Eden区快满了,此时就会触发垃圾回收

此时就会把Eden区中的存活对象都一次性转移到一块空着的Survivor区(s1)。接着Eden区就会被清空,然后再次分配新对象到Eden区里,然后就会如上图所示,Eden区和一块Survivor区(s1)里是有对象的,其中Survivor区(s1)里放的是上一次Minor GC后存活的对象

如果下次再次Eden区满,那么再次触发Minor GC,就会把Eden区和放着上一次Minor GC后存活对象的Survivor区(s1)内的存活对象,转移到另外一块Survivor区(s2)去。转移过后,s1区被清空了,因为里面的对象已经被转移到s2区

如果再发生第三次GC,那么就会把。eden区和s2区的,存活对象,都转移到s1区,那么这个时候s2区被清空。

以此类推,你会发现,每次GC,survivor的两个分区,总会有一个是空着的。

所以这里大家就能体会到,为啥是这么分配内存空间了。

​ 因为之前分析了,每次垃圾回收可能存活下来的对象就1%,所以在设计的时候就留了一块100MB的内存空间来存放垃圾回收后转移过来的存活对象。

比如Eden区+一块Survivor区有900MB的内存空间都占满了,但是垃圾回收之后,可能就10MB的对象是存活的。

​ 此时就把那10MB的存活对象转移到另外一块Survivor区域就可以,然后再一次性把Eden区和之前使用的Survivor区里的垃圾对象全部回收掉,如下图。

image-20191216163625597

接着新对象继续分配在Eden区和另外那块开始被使用的Survivor区,然后始终保持一块Survivor区是空着的,就这样一直循环使用这三块内存区域。(重要)

这么做最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了

无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都非常的好。

总结

​ 新对象的创建,永远是放在eden区。

​ 每次gc,都会空出一个survivor,清空eden和另一个survivor。保留一个survivor的目的是保存下次GC,从eden和另一个survivor存活下来的对象。

问题

为什么新生代要分成三块,分成两块行不行,9:1,一块eden和一块survivor

没有survivor区可不可以?

如果没有两个survivor区,只有新生代(只有eden区)和老年代,可不可以?肯定不可以:

  • 如果发生ygc也就意味着,存活对象直接进入老年代,那么可能因为老年代空间不够频繁gc。
  • 但是如果你说, 增加老年代空间,那么虽然说老年代gc频率下降,但是gc所需要时间变长(内存越大,扫描对象越久 - g1垃圾回收器分partition的原因

​ 我们可以得到第一条结论:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

那为什么需要两个survivor区?一个不行?

​ 如果只有一个eden区和一个survivor区,那么假设场景,当发生ygc后,存活对象从eden迁移到survivor,这样看好像没什么问题,很棒,但是假设eden满了,这个时候要进行ygc,那么发现此时,eden和survivor都保存有存活对象,那么你是不是要对这两个区域进行gc,找出存活对象,那么你想想是不是难度很大,还容易造成碎片,如果你使用复制算法,那么难度很大,如果你使用标记清除算法,那么容易造成内存碎片,如果你使用标记清除算法,那么耗时很长。

​ 所以如果存在两个survivor区,那么工作就非常的 轻松,只需要在eden区和其中一个survivor(b1)找出存活对象,一次性放到另一个空的survivor(b2),然后再直接清除eden区和survivor(b1),这样效率是不是很快?快的一。

万一垃圾回收过后,存活下来的对象在Survivor区域中放不下咋整?

现在有一个比较大的问题,就是如果在young GC之后发现剩余的存活对象太多了,没办法放入另外一块Survivor区怎么办?如下图。

image-20191216172242291

​ 比如上面这个图,假设在发生GC的时候,发现Eden区里超过150MB的存活对象,此时没办法放入Survivor区中,此时该怎么办呢?

​ 这个时候就必须得把这些对象直接转移到老年代去,如下图所示。

image-20191216172256106

如果新生代里有大量对象存活下来,自己的Survivor区放不下了,必须转移到老年代去,那么如果老年代里空间也不够放这些对象呢?

​ 首先,在执行任何一次young GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。

​ 为啥检查这个呢?因为最极端的情况下,可能新生代young GC过后,所有对象都存活下来了,那岂不是新生代所有对象全部要进入老年代?如下图。

image-20191216173021448

如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次young GC了,因为即使young GC之后所有对象都存活,Survivor区放不下了,也可以转移到老年代去。

但是假如执行young GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了。

​ 那么这个时候是不是有可能在young GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?

理论上,是有这种可能的。

​ 所以假如young GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个“-XX:-HandlePromotionFailure”空间担保参数是否设置了,没设置,那么进行fullgc

如果有这个参数,那么就会继续尝试进行下一步判断。

下一步判断,就是看看老年代的内存大小,是否大于之前每一次young GC后进入老年代的对象的平均大小。

举个例子,之前每次young GC后,平均都有10MB左右的对象会进入老年代,那么系统会认为这次young gc后,也会有大概10M对象进入老年代。那么此时老年代可用内存如果大于10MB。

这就说明,很可能这次young GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的,看下图。

image-20191216173214294

如果上面那个步骤判断失败了,或者是“-XX:-HandlePromotionFailure”空间担保参数没设置,此时就会直接触发一次“Full GC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行young GC。

如果上面两个步骤都判断成功了,那么就是说可以冒点风险尝试一下young GC。此时进行young GC有几种可能。

第一种可能,young GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可。

第二种可能,young GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。

第三种可能,很不幸,young GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC”。

Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。

​ 因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让young GC过后剩余的存活对象进入老年代里面。

如果要是Full GC过后,老年代还是没有足够的空间存放young GC过后的剩余存活对象,那么此时就会导致所谓的“OOM”内存溢出了

因为内存实在是不够了,你还是要不停的往里面放对象,当然就崩溃了。

特别提示

需要注意的是 “-XX:HandlePromotionFailure”参数在JDK 1.6以后就被废弃了,所以现在一般都不会在生产环境里设置这个参数了。在JDK 1.6以后,只要判断“老年代可用空间”> “新生代对象总和”,或者“老年代可用空间”> “历次young GC升入老年代对象的平均大小”,两个条件满足一个,就可以直接进行Minor GC,不需要提前触发Full GC了。

万一我们突然创建了一个超级大的对象,大到啥程度?新生代找不到连续内存空间来存放,此时咋整?

直接放到 老年代

到底一个存活对象要在新生代里这么来回倒腾多少次之后才会被转移都老年代去?

默认最大是15次(为什么是15次?请看《java核心技术杂记》中synchronize章节中的java对象头的相关信息)

如果你感觉文章对你又些许感悟,你可以支持我!!
-------------本文结束感谢您的阅读-------------