本文介绍了常见的三种垃圾回收算法(mark-sweep,mark-compact,mark-copy),是java虚拟机各类垃圾收集器的算法基础,为以后学习hotspot虚拟机的垃圾收集器打下基础java
1 开发者可能过早的回收依然在引用的对象,这种状况将引起悬挂指针问题。算法
2 开发者可能在程序将对象使用完毕以后未将对象释放,从而致使内存泄漏;缓存
1 不会有二次释放的问题;安全
2 下降了程序的耦合度,开发者只需关注自身模块,或只关注其余相关模块的少许代码,显示的内存管理没法知足软件工程的低耦合的原则,须要引入一些额外的接口。数据结构
3 完整性与及时性并发
理想状况下,垃圾收集过程应当是完整的,即堆中的全部垃圾都应当获得回收,但这一般是不现实的,从性能方面考虑,再一次回收过程当中只处理堆中部分对象或许更加合理,例如分代回收器会依照堆中的年龄将其划分为两代或者更多代,并把经历集中在年轻代,这样不只提升了回收效率,也能够减小单次回收的停顿时间;在并发垃圾回收器中,赋值器与回收器同时工做,其目的在于避免或者尽可能减小用户程序的停顿,此类回收器会遇到浮动垃圾的问题,即对象在回收过程启动后才变成垃圾,那么该对象只能在下一个回收周期内获得回收,所以在并发回收器中,衡量完整性更好的方法是统计全部垃圾的最终回收状况,而不是单个回收周期的回收状况。布局
4停顿时间性能
许多回收器在进行垃圾回收时须要终端赋值器线程,所以会致使在线程执行过程当中停顿,回收器应当尽可能减小对程序的执行过程的影响,所以停顿时间越短越好,例如分代式回收器经过频繁且快速的回收较小的,较为年轻的对象来缩短停顿时间,而较大的,较为年老的对象回收则只是偶尔进行(真是个喜欢年轻对象的渣男)。学习
5 空间开销优化
内存管理的目的是安全且高效的使用内存空间,不管是显示的内存管理仍是自动的内存管理,不一样的管理策略均会产生不一样程度的空间开销,某些垃圾收集器须要在每一个对象内部占用必定的空间(例如保存引用计数),还有些收集器会服用 对象现有的布局上已经存在的域(转发指针记录在用户数据上)。回收器也可能引入堆级别的内存开销,好比copying式回收器须要将对分为两个半区,任什么时候候赋值器只能使用一个半区,另外一个半区会被回收器所保留,并在回收过程当中将存活对象复制到其中;回收器有时也须要一些辅助的数据结构,好比追踪式的回收器要经过标记栈来引导堆中指针图表的遍历,也就是一般所说的根节点枚举,根据gc root set遍历全堆,回收器标记对象时也可使用额外的位图(bitmap),对于一些须要将堆分为数个独立区域的回收器,须要额外的记忆集来保存赋值器所修改的指针和跨区域的指针的位置。
标记-清扫(mark-sweep)、标记-复制(mark-copy)、标记-整理(mark-compact),引用计数时4种最基本的垃圾回收策略,大多数回收器会以不一样的方式对这些基本回收策略进行组合
标记-清扫算法与赋值器的接口十分简单:若是线程没法分配对象,则唤起收集器,而后再次尝试分配
回收器在遍历对象图以前必须先构造标记过程须要用到的起始工做列表(gc root set),即对每一个根对象进行标记并将其加入工做列表,回收器能够经过标记对象头的某个位(或者字节)的方式对其进行标记,该位(字节)也可位于一张额外的表中
须要注意的是:标记-清扫回收器要求堆布局知足必定的条件:
1 位图标记
回收器能够将对象标记位保存在器头部的某个字中,也可使用一个独立的位图来维护标记位,位图能够有一个,也能够存在多个,好比在块结构的堆中,回收器能够为每一个内存块维护一个独立的位图,这一方式能够避免因为堆不连续致使的内存浪费。
使用位图标记能够减小回收过程当中的换页次数,任何由回收器致使的换页行为一般都是不可接受的,对象每每成簇诞生并成批死亡,而许多分配器也会吧这些对象分配在相邻的空间。使用位图来引导清扫能够批量读取/清空一批对象的标记位,并且经过位图标记能够简单的判断某一内存块中的全部对象是否都是垃圾,进而能够一次性回收整个内存块
对于使用标记位放置在对象头部这一策略,位图可使得标记位更加密集;对于使用sweep的回收器,标记过程只需读取存活对象的指针域而不会修改任何对象;清扫器不会对存活对象进行任何的读写操做,只会在释放垃圾对象的过程当中覆盖某些域,所以位图能够减小内存中须要修改的字节数,并且减小了对高速缓存的写入
2 懒惰清扫
标记过程的时间复杂度是O(L),其中L是堆中存活对象的数量,清扫过程的时间复杂度是O(H),H为堆大小,虽然H>L,但mark过程的内存访问时不可预测的,而清扫过程的可预测性就要高的多,并且清扫对象的开销也比追踪对象的开销小的多,mark-sweep算法中一般会将大小相同的对象分布在连续的空间内,此时回收器能够依照固定的步幅对大小相同的对象进行清扫。
Lazy sweeping 该方案利用分配器来扮演清扫器的角色,即把寻找可用空间的任务转移到allocate过程当中,从而不须要单独的清扫阶段,最简单的清扫策略是,allocate简单的向前移动清扫指针,直到在连续的未标记对象中找到一块足够大的空间
分配器一般只会在一个内存块中分配相同大小的对象,每一个空间大小分级都会对应一个或多个用于分配的内存块,以及一个待回收内存块链表,在回收过程当中,回收器依然须要将堆中全部存活对象标记,但标记或回收器不急于清扫整个堆,而是简单的将彻底为空的内存块归还给块分配器,同时将其余内存块添加到其所对应空间大小分级的队列中,一旦stop the world结束,赋值器马上开始工做,对于任意内存的分配需求,allocate方法首先尝试从合适的空间大小分级中分配一个空闲槽,若是失败则调用清扫器执行懒惰清扫,即从该空间大小分级的回收队列中取出一个或多个内存块进行清扫,知道知足分配要求为止,若是没有内存块可供清扫,或者清扫的内存块不包含热河空闲槽,分配器便尝试从更低级别的块分配器中获取新内存块
1 吞吐量
mark-sweep式回收器在执行过程当中须要刮起全部赋值器线程,回收停顿时间取决于应用程序的运行及其输入
2 空间利用率
与mark-copy相比,mark-sweep具备更高的空间利用率,但sweep回收会产生一些空间碎片,cms的作法是能够隔一段时间采用compact算法整理一次空间碎片
3 是否移动对象
sweep最大的优点就是不会移动对象,
分代回收中,年老代被GC时对象存活率可能会很高,并且假定可用剩余空间不太多,这样copying算法就不太合适,因而更可能选用另两种算法,特别是不用移动对象的mark-sweep算法。
不过HotSpot VM中除了CMS以外的其它收集器都是会移动对象的,也就是要么是copying、要么是mark-compact的变种。
为了解决sweep中的内存碎片问题,须要对堆中存活对象进行整理以下降内存碎片的回收策略,compact能够极为快速的顺序分配
咱们所了解的整理式回收器大多遵循任意顺序和滑动顺序
任意顺序实现简单,且执行速度快,特别是全部对象大小相等的状况,但任意顺序整理可能会将原来相邻的对象分散到不一样的高速缓存行或者虚拟内存页中,从而下降赋值器空间局部性。因此现代compact回收器均使用滑动整理顺序
下面介绍几种不一样类型的整理算法
1 双指针整理算法
起始阶段,指针free指向区域始端,指针scan指向区域末端,在第一次遍历过程当中,回收器不断向后移动指针free,直到在堆中发现空隙为止;指针scan从后往前移动,知道发现存活对象,若是两个指针交错,则该阶段结束,不然便将指针scan所指向的对象移动到指针free的位置,同时将原对象中的某个域修改成转发地址,而后继续移动指针。双指针的有点事简单快速,且遍历的过程操做较少,但肯定是双指针重排序对象的顺序是任意式的,所以破坏了赋值器的局部性,而后因为相关对象老是成簇诞生,成批死亡,咱们能够将连续存活对象总体移动到较大空隙中,而不是逐个移动
lisp2算法
该算法要通过三次遍历,但每次遍历作的工做很少;
在标记结束的第一次遍历中,回收器计算出每一个存活对象的最终地址,并保存在对象的forwardingAddres域中,
第二次遍历过程当中,回收器将使用对象头域中记录转发地址来更新赋值器线程根以及被标记对象的引用,该操做将确保他们指向对象的新位置
第三次遍历过程当中,relocate最终将每一个存活对象移动到新的目标位置
与标记mark-sweep相比,mark-compact算法须要更屡次遍历,所以吞吐量较差,每次遍历的开销都很大,一个通用的解决方案是,尽可能长久的使用mark-sweep算法,在碎片化达到必定程度后,才使用mark-compact算法回收一次
Mark-compact算法能够选择不去整理“沉积区”内的对象,付出的代价是存在少许的内存碎片
采用不一样的compact算法会获得不一样的局部性,在虽然随机算法简单高效,但会破坏赋值器的局部性,因此滑动式的整理算法对局部性更友好
sweep回收的开销较低,但其存在内存碎片的问题,在一个良好的系统中,垃圾回收一般只占总体执行时间的一小部分,赋值器的执行开销将决定整个程序的性能,所以应设法下降赋值器的开销,特别是尽可能提高它的分配速度,compact能够消除内存碎片,但须要屡次遍历,copy算法回收器在复制过程当中进行堆整理,从而提高了赋值器的分配速度,可是堆的可用空间下降了一半
分配
通过整理的堆分配内存速度很快,分配过程简单;
空间与局部性
copy算法的肯定就是须要维护第二个半区,在内存大小必定,半区复制算法的可用空间是整堆回收的通常,这致使复制式的回收器所需的回收次数要比其余回收器更多;
而局部性首遍历方式所影响广度优先遍历顺序有将父子节点分开的趋势,而深度优先遍历则趋向于子节点预期赋节点排列的更近
移动对象
是否使用复制式取决于移动对象的开销,在分代式的回收器中,老年代存活数量多,而且有大对象,不适合使用copy算法,单年轻代存活数量少,且对象比较小,适合使用mark-copy
分代式GC里,年老代经常使用mark-sweep;或者是mark-sweep/mark-compact的混合方式,通常状况下用mark-sweep,统计估算碎片量达到必定程度时用mark-compact。这是由于传统上你们认为年老代的对象可能会长时间存活且存活率高,或者是比较大,这样拷贝起来不划算,还不如采用就地收集的方式。Mark-sweep、mark-compact、copying这三种基本算法里,只有mark-sweep是不移动对象(也就是不用拷贝)的,因此选用mark-sweep(cms)。
简要对比三种基本算法:
mark-sweep | mark-compact | copying | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 一般须要活对象的2倍大小(不堆积碎片) |
移动对象? | 否 | 是 | 是 |
关于时间开销: mark-sweep:mark阶段与活对象的数量成正比,sweep阶段与整堆大小成正比 mark-compact:mark阶段与活对象的数量成正比,compact阶段与活对象的大小成正比 copying:与活对象大小成正比
若是把mark、sweep、compact、copying这几种动做的耗时放在一块儿看,大体有这样的关系: compaction >= copying > marking > sweeping 还有 marking + sweeping > copying (虽然compactiont与copying都涉及移动对象,但取决于具体算法,compact可能要先计算一次对象的目标地址,而后修正指针,而后再移动对象;copying则能够把这几件事情合为一体来作,因此能够快一些。 另外还须要留意GC带来的开销不能只看collector的耗时,还得看allocator一侧的。若是能保证内存没碎片,分配就能够用pointer bumping方式,只有挪一个指针就完成了分配,很是快;而若是内存有碎片就得用freelist之类的方式管理,分配速度一般会慢一些。)
在分代式假设中,年轻代中的对象在minor GC时的存活率应该很低,这样用copying算法就是最合算的,由于其时间开销与活对象的大小成正比,若是没多少活对象,它就很是快;并且young gen自己应该比较小,就算须要2倍空间也只会浪费不太多的空间。 而年老代被GC时对象存活率可能会很高,并且假定可用剩余空间不太多,这样copying算法就不太合适,因而更可能选用另两种算法,特别是不用移动对象的mark-sweep算法。
不过HotSpot VM中除了CMS以外的其它收集器都是会移动对象的,也就是要么是copying、要么是mark-compact的变种。
这一篇写了很久,参考了一些垃圾算法的数据和文献,下一篇想写hotspot的垃圾收集的细节,和hotspot中的垃圾数据器的优缺点以及查看gc日志,优化垃圾收集时代码中的注意点。