面试官问为什么新生代不用标记清除算法
共 3707字,需浏览 8分钟
·
2021-07-26 22:09
作者丨安琪拉
来源丨安琪拉的博客
杭州某写字楼,安琪拉穿着新买的19.9的皮鞋走进玻璃隔间办公室,准备迎接一场新的表演。
面试官 :看你简历上有些熟悉JVM,是吧?
安琪拉:是的
面试官 :那你跟我讲讲堆内存的分区。
安琪拉:[心想]:这很easy嘛,来,算是回顾一下JVM的基础知识。
我们知道堆分为新生代和老年代,新生代就是我们说的Yong Generation,老年代是 Old Generation。
面试官 :然后呢?
安琪拉:然后什么?
面试官 :讲完啦?下面没有啦?新生代呢?
安琪拉:你想听你可以跟我说嘛,你不说我怎么知道你想听。
新生代又分为Eden区和Survivor区,Survivor由From区域和To区域组成,完整的内存结构,我给你画一下,别抽了,笔递给我一下,我画一下,如下图所示。
面试官 :哦,图可以,那为什么堆要分新生代和老年代呢?
安琪拉:当然是为了更有效的管理内存。
面试官 :怎么说?
安琪拉:假设一下,如果不分新老代,内存就一整块,垃圾收集器每次都要把那些长期存在的对象,和生命周期很短的对象放在一起回收,一般长生命周期的对象可能跟应用生命周期一致,你基本回收不掉的,比如Spring 框架里面的Bean管理相关的对象(ApplicationContext),整个应用运行期间都存在,这种一般经过几次回收最后都放在老年代,但是如果不区分新老代,每次都一起回收,性能消耗很大。
区分新老代之后,老年代放长期存活的对象,新生代就放生命周期短的对象,老年代对象很稳定,新生代回收不影响老年代,回收效率能大大提高。
面试官 :那为什么新生代还要分Eden、From、To区域呢?
安琪拉:[开始慢慢有点意思了]
首先大部分对象生命周期是很短的,如果新生代不分多个区域,新生代可能会有二种回收方案
第一种可能:每次回收都在新生代整块内存上进行,完整的垃圾回收过程分三步:
-
需要先找到需要清理的对象标记; -
清理这些被标记的对象; -
移动剩下的对象,对达到老年代晋升年龄的对象移动到老年代。
对象被回收掉后会产生很多内存碎片(被回收的对象很多),如果要解决内存碎片,需要移动剩下的对象(标记整理算法),整个回收流程效率很低。
第二种可能:如果没有Survivor区(From + To),Minor GC(新生代回收)过程中,存活的对象直接被送到老年代,这样的话老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC),Full GC频繁会影响程序的执行和响应速度。
新生代的回收叫Minor GC, 老年代的回收叫Major GC。
面试官 :为什么要设置两个Survivor区呢?From 和 To
安琪拉:我们来看一下, 如果只有一个Survivor区,新生代内存的回收流程。
我按照上面这张图画的讲,第一次Eden区域满了,内存回收很简单,直接把Eden区域存活对象放到Suvivor区域;
第二次内存回收,需要回收二个地方,Eden区域和Survivor区域。
-
因为Survivor区域也会存活的对象需要被回收,对Survivor区要采用标记整理垃圾收集算法,(先标记需要清理的对象,然后回收,然后把剩下的存活对象放到一起);
-
Eden区域采用复制算法,把Eden区域存放的对象复制到Survivor区域,然后把整个Eden区清除。
看到网上有些文章说这里设置二个Survivor区域的原因是为了避免内存碎片,因为他假设第二次(以及后续)的回收,内存回收是先回收Eden区域,然后是Survivor区域,这样当然会有内存碎片,但是如果真是只有一个Survivor区域,垃圾回收设计者肯定是先回收Survivor区域,再回收Eden区域,等Survivor区回收整理好,再把Eden区存放对象搬到Survivor区,这样存活地址是连续的,没有内存碎片。所以真正的原因还是我下面说的效率问题。
面试官 :这样有什么问题呢?
安琪拉:这样做有几个问题:
-
经过几次回收之后,Survivor区域满了之后怎么办?直接搬到老年代?那老年代很快就爆炸了。搬到Eden区?那内存碎片产生了,可能Survivor区和Eden区回收完之后,还需要再整理一下内存去掉内存碎片,性能消耗也是很大的。 -
一般标记整理算法的性能消耗是比复制算法消耗要大的,尤其是在新生代98%的对象都是“朝生夕死”的,标记清楚的是98%的对象,剩下就2%对象,要整理内存,不然直接把这2%对象放到另一个地方,把整块内存清除,Eden整块内存清除效率很高的。
所以归根结底,二个Survivor区还是为了性能考虑,标记复制算法效率比标记整理效率高。
面试官 :那你跟我详细讲讲标记新生代除了Eden,另外采用二个Survivor区的标记复制算法。
安琪拉:新生代中的对象 98% 是“ 朝生夕死” 的, 所以并不需要按照 1: 1 的比例来划分Eden和Survivor的空间, 而是将新生代分为较大的一块Eden空间和两块较小的Survivor 空间,每次只使用 Eden 和 其中一块Survivor[0](From区域),留出Survivor[1](To区域)用来实现标记复制。
当回收时, 将 Eden 和 Survivor[0] 中还存活着的对象一次性地复制到另外一块 Survivor[1] (To)空间上, 最后清理掉 Eden 和 刚才用过的 Survivor 空间。
另外说明一点:From区域和To区域在每次Minor GC之后都会互转,From区域变成To区域,To区域变成From区域,这只是逻辑标识
HotSpot 虚拟机默认 将Eden 和 Survivor 的大小比例是 8: 1(CMS不适用), 也就是每次新生代中可用内存空间为整个新生代容量的 90%( 80%+ 10%),只有10%的内存会被“ 浪费”(一直有10%的内存(Survivor To区)不存东西)。
标记复制算法流程:
-
Eden区域+Survivor From区满,进行存活对象标记,标记完,把存活对象复制到Survivor To区域; -
Survivor To区域变成From区域(一个逻辑标识),From区域变成To区域; -
内存分配,继续步骤1,复制过程中有达到老年代晋升年龄(默认值15),移动到老年代。
面试官:刚才说了这么多,是不是来之前背题了?
安琪拉:【心想】回答不出来你说我对技术没追求,回答出来了你说我背题,WTF。。
耐心对面试官解释:怎么可能,我只不过是来之前把安琪拉的博客公众号上的文章都看了一遍,嘿嘿。
面试官:在哪看,你分享给我。
面试官:诶诶,还有老年代内存回收策略呢?还有标记整理算法呢?另外讲讲几种常见的垃圾回收器,CMS和G1。
安琪拉:不想讲了,累了,要不放在二面的时候讲吧。
面试官:没事,二面面试官还是我,你直接讲吧。
安琪拉:真不想讲了。
面试官:那今天先到这吧,回去等通知,您出了这个门左拐。
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取