一.概述:
主要讨论:引用计数法,标记压缩法,标记清除法,复制算法和分代分区的思想。
二.引用计数法(Reference Counting):
概念:
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的应用计数器的值为0,则对象A就不可能再被使用。
优势:简单。
问题:
1.无法处理循环引用的情况。因此,在java的垃圾回收器中,没有使用这种算法。
2.引用计算器要求每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。
循环引用的情况:
一个简单的循环引用问题描述如下:有对象A和对象B,对象A中含有对象B的引用,对象B含有对象A的引用。此时,对象A和对象B的引用计数器都不为0。但是,在系统中,却不存在任何第3个对象引用了A或B。就是说,A和B是应该被回收的垃圾对象,但由于垃圾对象间相互引用,使垃圾回收器无法识别,引起内存泄漏。如下图:
什么是根对象?
JVM垃圾回收的根对象的范围有以下几种:
(1)虚拟机(JVM)栈中引用对象
(2)方法区中的类静态属性引用对象
(3)方法区中常量引用的对象(final 的常量值)
(4)本地方法栈JNI的引用对象.
由于单纯的引用计数算法隐含着循环引用以及性能问题,Java虚拟机并未选择此算法作为垃圾回收算法。
名词解释:
- 可达对象:指通过根对象进行引用搜索,最终可以达到的对象。
- 不可达对象:通过根对象进行引用搜索,最终没有被引用到的对象。
三.标记清除法(Mark-Sweep):
标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法可能产生的最大问题就是空间碎片。
左图是根据一个根节点获取被引用的对象,并把被引用的对象标记为可达对象,然后系统回收所有不可达的空间。
回收后的空间是不连续的,在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续的空间。因此,这是该算法的最大缺点。
四. 标记-压缩(Mark-Compact)
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从跟节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法避免了碎片的产生。
五.复制算法(copying)--针对年轻代的算法
- 过程:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
- 优势:与标记-清除算法相比,复制算法是一种相对高效的回收方法。适用于存活对象不多,不可达对象很多的场合,因为复制需要成本。
- 劣势:
1.复制空间不能太大,因为时刻准备有一块大小相同的空间。
2.不适用于存活对象较多的场合如老年代。同时因为老年代有大对象占据的空间过多,而复制空间不大。
-
进入老年代的对象有两种:
1.eden区的大对象直接进入老年代。
2.survival区的多次回收后还存活的对象,被认为是长期引用的对象,放入老年代。 -
eden区和from区剩余的小的而且年轻的对象会复制到to区中去。
如上图,新生代只算eden区和一个from区,to区浪费。
六.分代思想(Generational Collecting)
背景:
前面提到的复制,标记删除,标记压缩等垃圾回收算法中,没有一个算法可以完全替代其他算法,它们都具有自己独特的优势和特点。因此,根据垃圾回收对象的特性,使用核实的算法回收,才是明智的选择。
- 依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。
- 新生代--少量对象存活,适合复制算法。朝生夕灭的对象适合复制算法,因为复制要有大量的拷贝,大量存活对象会降低拷贝的效率。
- 老年代--大量对象存活,适合标记清理或者标记压缩。
如果用复制算法处理老年代,一方面浪费一半的空间,而且老年代对象存活率高,造成拷贝工作量大。
分代:
分代思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
卡表结构(hotspot具有的特性):
对于新生代来说,新生代回收频率很高,但是每次回收的耗时很短,而老年代回收的频率比较低,但是会耗费更多的时间。为了支持高频的新生代回收,虚拟机可能使用一个叫做卡表(Card Table)的数据结构。卡表作为一个比特位集合,每个比特位用来表示老年代的某一个区域的所有对象是否持有新生代对象的引用。
卡表好处:
在新生代gc时,要确定老年代的对象内是否有新生代对象的引用。用卡表结构可以不用花大量的时间扫描所有老年代对象,可以先扫描卡表为1的区域,卡表为0的区域不用扫描(因为没有对新生代对象的引用)。这样可以大大加快新生代的回收速度。
七.分区算法:
一般来说堆越大,一次GC所需要的时间越长,从而产生的停顿越长。为了更好地控制GC产生的停顿时间,分区算法将按照对象的生命周期长短划分为两个部分,分区算法将整个堆空间划分为连续的不同小区域,每个区域独立使用独立回收。
好处:
控制一次回收多少个区域。每次合理的回收若干个小区域,而不是整个堆的空间,从而减少一次GC所产生的停顿。如下图:
八.总结:
- 引用计数--没有被Java采用
- 标记-清除--老年代使用
- 标记-压缩--老年代使用
- 复制算法--新生代使用