java垃圾回收那些事
垃圾收集的作用
很明显你会回答通过清除不用的对象来释放内存,但是别忘了垃圾收集的另外一个重要作用就是消除堆内存空间的碎片。
垃圾收集策略
引用计数
这是早期策略。非常简单,对象A被引用,则它的计数Acount就加1,当对A的引用失效了,Acount就减1,当Acount=0时,就可以对A进行垃圾回收。对A进行垃圾回收时,A中引用的其他对象的计数都减1,因此A的回收可能会导致连锁反应。
优点:简单,快
缺点:无法检测循环引用,比如A的子类a引用了A,A又引用了a,因此A和a永远不会被回收。这个缺点是致命的,因此现在这种策略已经不用。
跟踪收集器
又称为“标记并清除”策略。基本思想是从根对象开始遍历整个对象图,仍然被引用的对象打上标记,遍历结束后,没有被打上标记的对象就可以清除。记录标记的策略可以在对象本身记录或者通过位图来记录。
我的疑问:会不会造成“误杀”啊?标记之后清除之前一个没有被打上标记的对象又突然被引用了怎么办?
这个问题想了一下应该不会,因为清除时只针对当前对象图这个范围进行,一个对象由没有被引用到被引用势必会扩大对象图,而这不在本次清除处理的范围之内。
跟踪收集器通常使用两种策略来实现:
1.压缩收集器:遍历的过程中如果发现对象有效,则立刻把对象越过空闲区滑动到堆的一端,这样堆的另一端就出现了一个大的连续空闲区,从而消除了堆碎片。
2.拷贝收集器:堆被分为大小相等的两个区域,任何时候都只使用其中一个区域。对象在同一个区域中分配,直到这个区域被耗尽。此时,程序执行被中止,堆被遍历,遍历时被标记为活动的对象被拷贝到另外一个区域。这种做法用称之为“停止并拷贝”。
这种做法的主要缺点是:太粗暴,要拷贝就都拷贝,粒度太大,整体性能不高。因此就有了更先进的“按代收集的收集器”
按代收集的收集器
基于两个事实:
1)大多数程序创建的大部分对象都有很短的生命周期。
2)大多数程序都创建一些具有非常长生命周期的对象。
因此按代收集策略就是在“停止并拷贝”策略基础之上,把对象按照寿命分为三六九等,不是一视同仁。它把堆划分为多个子堆,每一个子堆为一“代”对象服务。最年幼的那一代进行最频繁的垃圾收集。没经过一次垃圾收集,存活下来的对象就会“成长”到更老的“代”,越是老的“代”对象数量应该越少,他们也越稳定,因此就可以采取很经济的策略来处理他们,简单“照顾”一下他们就行啦。这样整体的垃圾收集效率要比简单粗暴的“停止并拷贝”高。
火车算法
说火车算法之前要先说一下“渐进收集”,按代收集已经比“停止并拷贝”更高效,但是按代收集每次处理的对象范围仍然是整个“堆”,有些情况下,垃圾回收的开销而造成的系统响应迟钝仍然让用户无法忍受,因此就提出了“渐进收集”,采取小步快跑的方式,每次只处理一部分“堆”空间的数据,“神不知鬼不觉”地完成垃圾收集工作。
下面说说火车算法,首先明确一个问题:火车算法要解决什么问题?
火车算法是用来替代按代收集策略的吗?不是的,可以说,火车算法是对按代收集策略的一个有力补充。我们知道按代收集策略把堆划分为多个”代“,每个代都可以指定最大size,但是”成熟对象空间“除外,”成熟对象空间“不能指定最大size,因为它是”最老“对象的最终也是唯一的归宿,除此之外,这些”老家伙“无处可去。而你无法确定一个系统最终会有多少老对象挤进”成熟对象空间“。
火车算法详细说明了按代收集的垃圾收集器的成熟对象空间的组织。火车算法的目的是为了在成熟对象空间提供限定时间的渐进收集。
火车算法把成熟对象空间划分为固定长度的内存块,算法每次在一个块中单独执行。为什么叫”火车算法“?这与算法组织这些块的方式有关。
每块数据相当于一节车厢
每一个数据块属于一个集合,集合内的所有数据块已经进行排序,因此集合就好比是一列火车
成熟对象空间又包含多个集合,因此就好比有多列火车,而成熟对象空间就好比是火车站。
垃圾回收-火车算法-成熟对象区内存组织形式
火车算法根本思想还是”分而治之“,目的是达到渐进回收,算法细节,下次再详细描述一下。
======================================
火车算法的大致执行过程如下:
1)首先检查序号最小的整列火车,如果已经不存在任何引用(成熟对象区之内的&之外的)指向这列火车中任何车厢中的对象,则直接回收整列火车的内存;
2)如果1)的检查结果为false,则注意力集中到序号最小火车的序号最小的车厢上,如果整节车厢的对象都不再被引用,则直接回收;
3)如果序号最小的车厢中的对象还有被引用的,则进行对象转移。如果引用来自其他火车的某个车厢,则把当前对象转移到引用它的那节车厢,如果那节车厢空间不足,则增加新的车厢,并在新的车厢中存放转移来的对象。如果引用来自成熟对象空间之外,则对象被转移到其他火车。如果引用来自本火车的其他车厢,则对象被转移到火车最末尾的车厢。
4)经过3)的对象转移,还剩下的对象就可以进行垃圾回收了。
通过上面的描述,我们会注意到,火车算法在进行垃圾回收时要扫描目标对象,确保没有对象引用指向他们,这是比较耗时的工作,因此火车算法采用了记忆集合来记录所有对一节车厢或者一列火车所有的外部引用。如果发现记忆集合为空,则说明没有引用,直接可以回收车厢或者火车。
finalize方法
作用:一个钩子方法,jvm在对当前对象执行垃圾收集之前,可以通过调用finalize方法来作一些额外的工作。注意:finalize方法是由jvm来调用的。
另外一点需要明确的是finalize方法会增加垃圾回收的复杂度。大致过程是这样的:
1)垃圾收集器通过某些算法找出所有没有被引用的对象,并准备对其进行回收(这个过程称之为“第一趟扫描”);
2)垃圾收集器此时要挨个判断即将被回收的对象是否拥有finalize方法,如果有,则调用它。
3)执行了所有finalize方法之后,垃圾收集器必须再次进行一趟扫描(称之为“第二趟扫描”),来判断即将被回收的对象是否真的应该被回收,因为finalize方法可能会“复活”某些对象,因此这趟扫描是必要的。
关于finalize还有一点需要记住:一个对象的finalize只可能被运行一次(如果提供了的话)。这是因为jvm会记住某对象的finalize方法是否已经被执行过,如果已经被执行过,但是对象仍然有效(比如finalize方法把它自己“复活”了),那么当这个对象再次变得无效时,jvm回收该对象时就不会再执行它的finalize方法了,jvm会把它当作没有提供finalize方法的对象来看待。否则该对象将永远不会被回收了。
对象可触及性的生命周期
在垃圾收集器看来,堆中的每个对象都有6种状态:
强可触及、弱可触及、软可触及、影子可触及、可复活、不可触及。
其中强可触及与较弱的形式(弱可触及、软可触及、影子可触及)直接的差别是前者绝对不允许引用的目标被垃圾回收,而后者则允许。
可复活的:虽然从根节点的对象图中不可触及,但是有可能从垃圾回收器执行某些终结方法时可以触及。
不可触及的:已经没有其他对象引用的。
搞出了这么多状态,有什么用呢?
简单点说,软引用使你可以创建内存中的缓存,它与程序的整体内存需求有关。弱引用使你可以创建规范映射,比如哈希表,它的关键字和值在没有其他程序部分的引用时可以从映射中清除。影子应用使你可以实现除finalize方法之外的更加复杂的临终清理政策。
软引用比较好理解,这里比较难理解的是弱引用,其实只要举一个具体的例子就明白了:
我们经常使用生命周期与整个应用程序生命周期一样长的Map<xxBean,yyBean>来作为缓存持有一些元数据,如果我们使用的是HashMap<xxBean,yyBean>(强引用),那么就会导致Map中的所有key-value的生命周期与Map的生命周期一样长,因为这是强引用。这就为内存泄露埋下了伏笔,比如某一个key:xxBean其实已经不再使用,但是由于其所属的Map一直被引用,就导致这个xxBean也一直不会被垃圾回收。而如果此时我们使用WeakHashMap来代替HashMap就可以解决这个问题,垃圾收集器一旦发现除WeakHashMap还持有对xxBean的引用的话,它就会毫不犹豫地释放到xxBean-yyBean这个键值对。
关于软引用、弱引用和影子引用最后再说一点:由于垃圾收集器可以自行决定他们的命运,因此java提供了引用队列来让开发者可以监听到垃圾收集器对这些对象的处理。
下面的API必须知道:
java.lang.ref.Reference
关于弱引用的一个例子:http://www.ibm.com/developerworks/cn/java/j-jtp11225/
posted on 2011-11-18 00:10 wenfeng762 阅读(1584) 评论(1) 编辑 收藏 举报