垃圾收集算法
标记-清除算法
过程:
首先标记出要回收的对象,然后标记完成后统一对被标记对象进行回收。标记过程有“引用计数算法”,“可达性分析算法”。
存在问题:
效率不高:标记和回收的效率都不高
空间问题:容易产生大量的空间碎片,当虚拟机需要分配大块内存时,可能触发下一次垃圾回收(GC).
复制算法
过程:
将内存划分为两个区间,在任意时间点,所有动态分配的内存只能分配在其中一个区间(活动区间) 而另外一个区间(被称为空闲区间) 是空闲的。
当有效内存耗尽的时候,JVM将暂停运行,启动复制算法GC的线程,接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址顺序排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。
此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。
优缺点:
很明显,复制算法弥补了标记/清除算法中,内存布局混乱的缺点。不过与此同时,它的缺点也是相当明显的。
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。
所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
一般在新生代垃圾回收时,采用这种算法,并采用分配担保机制,当活动区间内存不足的时候,对象由新生代进入老年代。
标记-整理算法
由于复制算法需要分配担保机制
标记:与标记-清除算法一摸一样,均是遍历GC Roots,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址一次排列,然后将末端内存地址以后的内存全部回收。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
不难看出,标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价,可谓是一举两得,一箭双雕,一石两鸟,一。。。。一女两男?
不过任何算法都会有其缺点,标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法
它们的共同点主要有以下两点。
1、三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。因此,要想防止内存泄露,最根本的办法就是掌握好变量作用域,而不应该使用前面内存管理杂谈一章中所提到的C/C++式内存管理方式。
2、在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
它们的区别按照下面几点来给各位展示。(>表示前者要优于后者,=表示两者效果一样)
效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记/整理算法>标记/清除算法。
内存利用率:标记/整理算法=标记/清除算法>复制算法。
可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。
分代收集算法
现代商业虚拟机所采用的算法。
一般将java堆分为新生代和老年代。
新生代中,每次都有大批量对象死去,只有少量存活,那就使用 复制算法。(三块 8:1:1)90%.
老年代中,对象存活率高,没有额外分配担保空间,就要使用标记-整理,或者标记-清除算法.