首先明确该部分所说的垃圾收集只是针对java堆区域的。
1.标记-清除算法(Mark-Sweep):
最基础的算法,其他回收算法,都是基于他的缺点进行改进
分为两个阶段:
①首先标记出所有需要回收的对象(可达性分析算法)
②在标记完成后,统一回收所有被标记的对象。
两个严重缺陷:
①效率问题,标记和清除两个过程的效率都不高;
②空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时, 无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。
2.复制算法(Copying):
它将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面去,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对半区进行内存回收,内存分配时,也就不需要考虑内存碎片等复杂情况。
缺点:算法的代价是将内存缩小为了原来的一半。
由此,也产生了基于其缺陷的改进方法:
现在的商用虚拟机都采用复制算法来收集新生代,而IBM公司的专门研究表明,新生代的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间(很好理解,垃圾回收时候,需要复制保存存活的对象非常少,那复制区域的内存空间完全没有必要和使用的空间进行1:1的划分)
所以。将内存分配为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收的时候,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。Hotspot虚拟机默认的Eden和Suivivor大小比例是8:1:1;
但是,新的问题又来了,我们没有办法保证每次回收都只有不多于10%的对象存活。
那么,当Survior空间不够用时,需要依赖其他内存(顾名思义,除了新生代,那就是老年代了)进行分配担保。
简单的理解为,如果另外一块survivor内存没有足够的空间存放上一次新生代收集下来的存活对象时,这时候就需要分配担保机制,由老年代去分担,分配担保!
这里,有一个个人理解的关于复制算法的简单记录:
那就是为什么survivor空间需要两块?
问题的根源还是从复制算法的根本逻辑出发,我们现在考虑1:1分配内存进行复制算法的情况,称其为a1,a2两块区域,假如使用的是a1,那么回收时候,由a2去复制a1中存活的对象,进而清除a1。那么此时就是关键,之后会把a2作为使用的区域,再一次回收,由a1复制,然后清除a2.依次反复。
看到这里,其实也就有解决了问题的关键,上述a1和a2是大小相等的。所以来回复制,其实逻辑上是不分彼此的。
那么一旦比例发生改变,我们假设,现在还是两块,大小比例为a1:a2=9:1.那么问题就来了,我们使用的是9分的a1,回收的时候,让a2去复制存活对象,清除a1,这时候,要知道,我们只能使用a1,因为a2是很小的,并且也不可能同时使用a1,a2,那样的话,复制算法完全没有了意义。那要使用a1,就必须再把a2保存的对象在复制会a1,相信说明白了,此时浪费了一次复制
所以,才有了两块suivivor的存在(有时候,任何行业,都很佩服前辈们对于问题解决的逻辑思考),我们不妨假设为eden和suivivor的区域比例为8:1:1,我们命名为a1:a21:a22=8:1:1.我们使用a1和a21,那么回收的时候,我们用a22去复制,自然而然的就产生了,我们下次使用的就是a1和a22了,然后由a21去复制,依次反复。
3.标记-整理算法(Mark-Compact)算法
根据上述复制算法的了解,细心的同学很容易发现,其实我们所有的前提都是,对象存活率很高的情况。否则分配比例就要调整,因此也会浪费很多的使用空间。所以,就像之前所说的一样,复制算法一般用于新生代。
根据老年代的特点(也就是存活率相当高,并且持续存活),产生了标记整理算法。
其实也是对标记清除算法的改进,思路逻辑为:标记过程仍然和标记清除一致,但后续的步骤,不是直接的对可回收的对象进行清理,而是让所有存活的对象都向一段移动,然后直接清除掉端边界(标记和未标记的边界)以外的内存。
4.分代收集算法
当前商业虚拟机采用的都是分代收集算法,其实这并不是一种新的收集算法,倒不如直接理解为一种思路。
只是根据对象存活周期的不同将内存分为几块。一般是吧Java对分为新生代和老年代,这样根据不同划分区域的特点,采用想用适合其回收的算法,在新生代采用复制算法,而老年代采用标记清理或者标记整理算法。