GC回收算法--当女友跟你提分手!
Java语言引入了垃圾回收机制,让C++语言中令人头疼的内存管理问题迎刃而解,使得我们Java狗每天开开心心地创建对象而不用管对象死活,这些都是Java的垃圾回收机制带来的好处。但是Java的垃圾回收机制的核心原理是什么呢?今天我们来聊聊GC回收算法吧。
JVM的GC回收场景很复杂,不是单个算法就可以搞定的,大致可以分为可达性分析算法、标记-清除算法、标记-整理算法、分代回收算法、复制算法。
广场上,女朋友突然跟你闹分手,然后头也不回地一个人走了,留下你一个人站在树下,BGM缓缓响起“雪花飘飘 北风啸啸 天地 一片 苍茫~~~”树叶纷纷落下,这时的你仿佛被夏洛特里的元华附身,成了全世界最悲伤的人。当你沉浸在悲伤不可自拔,旁边的环卫大妈一脸嫌弃看着你“年轻人你挪一下,别挡到我扫地”。
对你没猜错,地上的落叶,就是GC垃圾回收算法的核心–可达性分析算法。
可达性分析算法
轻风乍起,泛黄的树叶纷纷掉下,刚分手的你不禁长叹“叶子的离开是风的追求还是树的不挽留”,当叶子从枝头掉落的那一刻,它跟树就再也没有任何关系。同样的,可达性分析算法的基本思路就是JVM内存中的对象以树的形式管理,我们称之为”GC tree”。GC tree的根节点叫做GC Roots,通过一些列GC Roots为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用了。
图中的对象1、2、3、4不管哪个都可以找到与GC Roots相连的引用链,属于存活对象,而对象5、6、7虽然彼此相互有联系,但是他们到GC Roots是不可达的,所以属于死亡对象。
有哪些对象可以作为GC Roots呢?
虚拟机栈(栈桢中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)的引用的对象
标记-清除算法(Mark-Sweep) 分手不是马上分
二次标记
当女朋友跟你闹分手,她是真的要跟你分吗?太天真了少年!!!女人都是感性动物,刀子嘴豆腐心,她只是给你判了个死缓,如果你什么都不做,那我没话说,注孤生吧小伙子;如果你态度端正,那你还有得救!
同样的,当GC线程遍历GC tree检测到无用对象的时候,并不是立马人道毁灭,只是先给它做个标记,告诉对象你已经上了枪毙名单。这里是第一次标记。
挽留爱情该如何做?说情话哄她,拉她去心心念念的馆子吃顿好的,又或者去商场给她买向往已久的迪奥999口红······这些套路我就不说了,反正只要能让女朋友开心,什么付出都是值得的。
当对象第一次被标记的时候,GC线程会去检查此对象是否有必要执行finalize()方法。finalize()方法还记得吧?finalize()定义:finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。什么意思?就是意味着如果我们重写该方法的话,那么在GC回收之前该方法会被执行。
但是有两种情况GC线程会认为没有必要去执行。
1.对象没有覆写finalize()的。女友跟你闹分手,你却像没事人一样回家继续撸游戏,小伙子你心可真大。
2.finalize()已经被虚拟机执行过的。女友此时内心OS:上次吵架你送我一只迪奥999赔罪,这次你又送,你就不知道我这段时间一直想买萝卜丁吗?一定是在敷衍我!呵!男人!
生存还是死亡 要爱情还是要自由 That is a question!
如果对象被判定需要执行finalize()方法,那么它将会放置在一个叫F-Queue的队列里面挨个等待执行,对象自我救赎的机会来了!如果对象想在finalize()中成功拯救自己,只要重新与GC Roots建立关联即可,比如把自己赋值给某个类对象或者对象的成员变量,那么在第二次标记的时候它从“即将回收”的集合中中被移除;如果这时它还没有建立关联,那么它这次真的是GG了,我们用一首《凉凉》给它送别吧。
图解:
以上就是标记-清除算法,不过它有两点不足之处:
1.效率问题,标记和清除过程的效率不高。
2.空间问题,标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致以后在程序中需要分配占用较大连续空间的对象(如数组)时,无法找到足够的连续内存而不得不提前触发下另一次垃圾收集动作。
为了解决这些问题,“复制算法”应运而生。
复制算法(Copying) 物种大逃亡 诺亚方舟!
复制算法思路比较简单:将内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存空间满了,就将还活着的对象复制到另外一块,然后再将之前那块内存空间彻底清空。有点像《圣经》里的一个故事:大洪水要来了,生物纷纷逃上诺亚方舟以躲避灾难。这样玩的话每次都是对整个半区进行内存回收,内存分配的时候也不用考虑内存碎片的情况,简单粗暴让人喜欢!只不过这种算法将内存缩小为了原来的一半,代价太高昂了,我们要知道,内存是很宝贵的资源!
黄金比例 8:1:1
医学研究证明,感冒是由病毒引起的。咳咳开个玩笑!软件团队研究表明,内存中的绝大部分对象都是“朝生暮死”的,所以完全没必要非要按照1:1的比例来玩。而是把内存分成了一块较大的Eden区(伊甸区)和两块较小的Survivor区(幸存区),每次都使用伊甸区和其中一块幸存区(我们取个别名叫幸存者1号吧,另外一块取名幸存者2号)。当回收的时候,将伊甸区和幸存者1号区域里面的对象一次性复制到幸存者2号里面,最后对伊甸区和幸存者1号进行清算,里面的所有对象不管生存还是死亡彻底清除干净,比灭霸打个响指还厉害!
图解:
现在HotSpot虚拟机默认伊甸区和两块幸存区的比例大小为8:1:1,这样平时工作的时候,只有10%的内存会被浪费掉,这样是不是很划算呢?
看到这里有人鞋会问,幸存区为什么要分为两块?比例9:1才是最完美吧?NO!NO!NO!,这里不得不引出另一个算法–分代收集算法了。
分代收集算法(Generational Collection) 历经考验 修成正果
有没有发现,所谓的爱情其实都是要经历无数次的磨合,无数次的考验,在一起的两个人只有经受住了这些磨难,才会走进婚姻的殿堂,有了圆满的结局,而经受不住这些考验,那双方就只有各自安好,相忘江湖。
在我们每次GC回收的时候,都会有一小部分对象活下来,然后一直活到下一次GC再次被检测。最近很火的吃鸡游戏,玩家不管用什么手段,刚枪也好苟也好,只求活下去成为最后的幸存者。而Java对象也是这样,经历GC的层层考验,最终成了打不死的小强。这时候该轮到GC不爽了,你丫的每次都浪费我的时间,小强内心OS”就喜欢看你不爽我又干不掉我的样子!”,对于这批顽固分子,GC作为执法者决定眼不见为净,于是委托JVM专门划分出一块区域给他们颐养天年,从此天涯是路人。而划分出的这块区域就是赫赫有名的“老年代”了,而与之相对应的就是之前GC频繁的“新生代”。
一个对象该如何从“新生代”跑到“老年代”去呢?
我们创建一个对象,它的对象头里面会有一个GC分代标识,每经历一次GC如果能活下来该标识+1,当加到一定次数后,GC会判定该对象是个老流氓,于是乎把它从“新生代”转移到“老年代”了,安排!具体参考我的另一篇博客《假如Java对象是个人······》
新生代每经历一次GC,幸存者2号区域活下来的对象年龄标识自动+1,然后判断是否满15岁(默认值15次),如果满15岁了,那么就从幸存者2号复制到“老年代”里面取颐养天年,如果没有的话,那么就复制到幸存者1号区域里面去,然后幸存者2号区域被清空。
由于老年代对象存活率极高,用不着复制算法这一套。于是有人提出了另外一种算法叫做“标记-整理算法”。
标记-整理算法(Mark-Compact)
标记-整理算法其实基本过程跟“标记-删除”算法差不多,只不过后续的步骤不是对无用对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理到端边界以外的内存。这样就完美解决了“标记-清除算法”内存碎片化的问题。
图解:
讲到这里,JAVA的GC回收算法基本就差不多了。我们的GC就是针对内存中的不同区域,采取合理的算法从而达到自动清理的效果。新生代的对象大多数朝生暮死,就采用“复制算法”,老年代的对象存活率极高,就采用“标记-删除算法”或者“标记-整理算法”。
现在是不是觉得GC回收算法没有想象中那么神秘?希望我的理解能给你带来一点帮助,由于人懒,图片都是网上直接拿来用的。另外,如果现实中跟女友有摩擦,该服软还是得服软,男人就应该表现得大度一点,毕竟两个人相处不易,更何况她还是将和你共度余生的人,不要因为一时冲动而抱憾终生。额······我仿佛又闻到了爱情的酸臭味!
参考资料:《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》