“哎,今天面试被面试官虐惨了,一个劲儿的问垃圾回收的问题。开头还能答几个,后来都招架不住了”
刚面试完的小橙一边走进厕所,一边在电话里和朋友抱怨着。
“一开始从JVM问我Java堆的内存划分,后来又问为什么要分年轻代年老代,哪些对象会被认为是垃圾,垃圾回收的 Root Set 有哪些。你说咱们不都是背个年轻代年老代,再记住个Eden、Survivor这些吗...”
听着小橙的抱怨,旁边正在清理厕所地面的老大爷手中的拖把慢慢的慢了下来。
看着小橙挂断电话还是一别「义愤填膺」的样子,老大爷过来聊了起来。
“小伙子,还在死记硬背,看样子你还没理解Java 的垃圾回收呀”
“哟,您也懂Java ?” 小橙吃惊的看着老大爷,虽说平时常看到老大爷在打扫厕所,但还是第一次知道他也懂编程。
“不瞒你说,我之前也做过开发”。老大爷一边说着一边望向窗外的远方,仿佛想起了以前的岁月。
“那您后来怎么不做了呢?” “老大爷~”
小橙的几句话,又把老大爷拉回到现实中。
“哎,以后有机会再和你说。今天我大概给你说说我对Java 垃圾回收的理解吧,看看对你有没有帮助”
“好啊” 小橙将信将疑。
伴着厕所内空气清新剂的味道,老大爷深吸一口气,讲了起来。
“就以我这每天打扫的厕所来说吧。Java 里一般的「堆」内存不是要分几个「代」嘛,对象内存分配尽量在年轻代进行。”
听到「堆」、「年轻代」这些专业词汇,感觉还真懂的样子,小橙赶忙点了点头。
“其实对象内存的分配和垃圾的回收之间,并不是独立的,回收空间的方式也影响分配的方式”
“而这一切都是为了效率,和我们平时的打扫工作类似,如果不讲究方法,就会一直忙个不停,效率还很差。你比如Java的垃圾回收过程中,为了不让新的内存分配影响现有的垃圾回收工作,会有Stop-The-World,在这段时间,垃圾回收工作,分配对象的线程都得停止。”
“我们保洁就像垃圾回收线程一样,你们上厕所的,就像分配内存的线程一样。你应该有印象,为了尽快打扫干净厕所给你们使用,有时候我们保洁也会在厕所门口立个小牌子,「清扫中,请勿使用」。没错,这就是我们的STW,你们不进来「捣乱」,我们就能很快的墩完地,擦干净洗手池,后面你们就又能愉快的使用了”
“那每次都放个牌子,大家着急用也不方便啊”,小橙已经进入了这个生活的场景了。
“是的,所以像早上,中午大家在频繁使用的时候,我们虽然也在打扫,但却不能放个牌子。这个时候用过的池子我会清洗,因为用过的,我能一眼看出来,就像第一步的垃圾标记一样,在清洗的时候,不影响大家使用其他的,就像垃圾回收CMS的并行执行一样。”
“为了进一步提升大家上厕所的效率,我们也给厕所分了代。左边的一排是给大手留的,像老年代一样,右边一排是小手的,像年轻代。这样大对象直接分配到大手老年代,要占用时间长,而「短作业」直接在右边快速解决快速离开,短作业的区域,我们也可以再分成两半,我中间放个牌子,这样保证分配的时候都是连续的空间,使用完一半再开放另一半。”
“万一小手解到一半要玩大的的朋友,请到左边。当然你说像在年轻代使用的「复制算法」,在我们这儿略有点不适用,不能一哥们进行到一半的时候,我说朋友,你挪到边上那个,我把这个清洗一下。他非得打我不成。”
“标记-清除这个是最常用的,一般在工作时间,用过的便池,我们会人工标记一下,可能是记下来,也可能是往里放个标记,之后我们会马上清洗。”
“标记-整理这个倒是也有类似的,比如我们也常会把几个池子找个线分隔开一下,大家使用时先用这边的,我把那边不干净的几个统一收拾完了,再全部放开。可能这么说你不好类比。要不这么着,你就想着有个哥们他尿几下要换一个地儿,所以他需要连续的几个池子,所以我得把一个干净一个脏这种不连续的清洗好了,给他们用”
“大爷,我现在有点乱儿”,听着老大爷说了一箩筐的内容,小橙一会儿明白一会糊涂。
“来,我再给你捋一捋”
“你看啊,首先你记住最关键这一点,不同的垃圾回收方式,也决定了内存的分配方式,这两个不是独立的。同时为了更高效的完成工作,通常也会组合几种回收方式一起用。比如「复制算法」,一般就用在年轻代这儿,因为毕竟一般的对象生存周期都不长,同时只要把标记出来的存活对象直接复制走就行,复制过去之后,空间还连续,多好。其余的直接清除就行了。所以在年轻代复制几轮之后该清除的也就都清了。留下来的再搬到老年代”
“那老年代回收过程中,难免会出现清了这个对象,但它旁边还被占用这种情况,那下次从年轻代过来一个大块头,或者年轻代过来两情侣,人家要挨着坐,放不下怎么办?所以还得把这些不连续的空间,给整理一下,存活的都排整齐了,其它地方空出来给新来的’,这时候就会用到咱们说的「标记-整理」”
“所以你应该发现了,不同的垃圾回收算法,因为要做的事情不同,所以费的时间也不同,针对高吞吐的和针对低延迟的必定不可兼得,只能权衡。所以你发现有些车站等,在人少的时候,可能会锁上几个,或者拉个线标记只能使用某部分,以此减小回收时间”
“说了半天垃圾回收,哪些是垃圾呢?我们一般是推开门,看哪个有人在用,我们会暗暗记住,哪个有用过的「痕迹」,都认为是垃圾。门是整个查找的「根」,每层楼里的各个厕所的门,都是我们查找「垃圾」的「根」。在垃圾回收里不是也叫「根搜索算法」,也叫「可达性分析」嘛,从「根」开始找,一直向下,能链接到的,叫可达对象,是存活的,没人用的是垃圾。在 Java 里会把「虚拟机栈和本地方法栈」正在引用的对象、静态属性引用的对象和常量看做是「厕所的门」”。
“像一些小点的厕所,我们一个人打扫就行,那车站、机场这种大的场所内,一个人打扫不过来,为了更快收拾好,就会同时出动好几个保洁,这就像垃圾回收里的 Parallel 收集器,多个线程一起,更快。”
“哦,对了, 垃圾回收里还有个「安全点」的概念,就是只有执行到这里再进行垃圾回收,才不会出错,为什么呢?主要是为了让垃圾回收的根集枚举更精确,编译器可以给每条指令都记一些信息,在需要STW的时候使用,可如果每个都记,代价太高,所以就选了一小部分,就是在安全点。如果厕所内某个时刻视为可以暂停的点,也就是女保洁需要清洗男厕,这个时候,需要观察和询问里面有没有人,当某个时刻正好处于安全的时候,保洁开始工作,大家先停止使用”
看到又有几个小伙走进厕所,“咱们以后有时间再聊”,说着,老大爷又像平时一样,快速开始挥动拖把,认真的开始了「垃圾回收」。