一、引用计数法
引用计数法,在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器值就加1;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。
客观的说,虽然引用计数法虽然占用了一些额外的内存空间来进行技术,但他的原理相对简单,判定的效率也高。但是,在Java领域至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要的原因,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确的工作,就例如引用计数就很难解决对象质检相互循环引用的问题。
二、可达性分析算法
当前主流的商用程序语言的内存管理都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活。这个算法基本的思路就是通过系列称为“GC Root”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象不可达时,则证明次对象是不可能再被使用的;
在Java技术体系里面,固定可作为GC Root的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中的引用的对象,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量等;
- 在方法区中类静态变量属性引用的对象,譬如Java类的引用类型的静态变量;
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用;
- 在本地方法栈中JNI(即通常所说的Native方法)引用对象;
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryErrror)等,还有系统类加载器;
- 所有被同步锁(Synchronized)持有的对象;
三、垃圾收集算法
3.1分代收集理论
当前商业虚拟机的垃圾收回器,大多数遵循分代收集的理论进行设计,他们的设计原则为:收集器应该就Java堆划分出不同的区域,然后将回收对象根据年龄(这里的年龄技术对象熬过垃圾回收的次数,最大15)分配到不同的区域中存储。
显而易见,如果一个区域大多数对象都是朝生夕阳灭,难以熬过垃圾回收过程的话,那么把他们集中放在一起,每次回收时只需要关注如何保留少量存活对象而不是去标记那些大量将要被回收的对象,就能以比较低的代价回收到大量的空间,剩下的都是难以消亡的对象,那把他们集中在一起,虚拟机就可以使用比较低的频率来回收这个区域,这样子就能够兼顾了垃圾收集的开销时间和内存的有效利用;
现在的Java虚拟机,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation) 两个区域。顾名思义,就是在新生代中,每次垃圾收集时都会有大量的对象死去,而每次回收后存活少量的对象,将会逐步晋升到老年代中存放。
假如要现在进行一次只局限于在新生代区域的收集(MinorGC),但是我们新生代中有可能是引用到老年代的问题,,然后为了找出对应存活的对象,我们不得不在固定的GC Roots之外还需要额外遍历整个老年代所有对象找出所有所有对象来确保可达性分析的正确性,虽然这种方案可行,但是毫无疑问这种做法会为内存回收带来很大的性能负担。
所以,我们就不应为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录一个对象是否存在那些跨代引用,我们只需要在新生代建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分为若干小块,然后标记出老年代的哪一块内存里会存在跨代引用。
然后发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到Roots GC进行扫描。虽然这种方案需要在对象改变引用关系的时候维护记录数据的正确性,会增加一些运行时的开销,但是比要扫描整个老年代来说还是非常划算的。
3.2 标记-清除算法
标记清除算法,会分为两个阶段:标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收没有被标记的对象。其中标记点的过程就是判断是否是垃圾的过程。
但是他的缺点也比较的明显:第一个就是执行的效率不稳定,如果Java堆中包含大量的对象,而且其中很大的一部门是需要被回收的,这个时候就需要大量的标记和清除动作,导致标记和清除两个过程的执行效率都随着对象的数量而增长。第二个就是内存碎片化问题,标记清除之后产生大量的不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象的的时候无法找到大的内存空间进而导致一次垃圾回收。
3.3标记-复制算法
标记-复制算法简称为复制算法,为了解决标记-清除面对大量可回收对象执行效率低的问题,它算法思想就是将可用内存按容量划分为大小相等的内存块,每次只使用其中的一块,当这块内存用完了,就将还存活的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。如果内存中大多数对象都是存活的,这种算法将会产生大量的对象复制动作。
这种复制算法的代价就是将可用内存减少了一版,空间浪费大。
现在的商用虚拟机大多优先采用了这种收集算法去回收新生代,但是他们提出了一种更优化的半区复制分代策略,他就是把新生代分为一块较大的Eden空间和两块较小的Survivor空间。每次 分配内存只使用Eden空间和其中的一块Survivor空间上。发生垃圾收集时,将Eden和Survivior中存活的对象一次性复制到另外一块还没使用过的Survivor上,然后就直接清理掉Eden和已使用过的Survivor。Hotspot虚拟机默认的Eden和Survivor的大小比例是8:1。当然当Survivor的空间不足以容纳这么多存活的对象的时候,这其中还有一个分配担保的概念,这有兴趣的小伙伴的查下。
3.4标记-整理算法
标记-复制算法在存活对象率较高时就要进行比较多的对象复制,效率会变低,更关键的是,如果不想浪费50%的内存空间就需要额外的空间来进行分配担保,以应对被使用的内存所有对象还存活的极端情况,所以老年代中一般不能使用标记-复制算法。
这个算法是在标记-清除的算法之上进行一下压缩空间,重新移动对象的过程。因为标记清除算法会导致很多的留下来的内存空间碎片,随着碎片的增多,严重影响内存读写的性能,所以在标记-清除之后,会对内存的碎片进行整理。最简单的整理就是把对象压缩到一边,留出另一边的空间。由于压缩空间需要一定的时间,会影响垃圾收集的时间。