垃圾收集算法
引用计数
堆中的每一个对象都有一个引用计数,当对象被引用时引用计数加1,当对象的引用被又一次赋值或超出有效区域时引用计数减1,当一个对象被回收后,它所引用的对象的引用计算减1。当一个对象的引用计数变为0时就被回收。
引用计数的长处:
垃圾收集器能够非常快地运行,当一个对象的引用数为0时就能够回收这个对象,垃圾收集交织在程序的正常运行过程中,不用长时间中断程序的正常运行。
引用计数的缺点:
- 每次引用计数的添加和降低会带来额外的开销
- 无法检測出循环引用
根搜索算法
垃圾检測通过建立一个根对象的集合(局部变量、栈桢中的操作数,在本地方法中引用的对象,常量池等)并检查从这些根对象開始的可触及性来实现。根对象总是可訪问的,假设存在根对象到一个对象的引用路径,那么称这个对象是可触及的或活动对象,否则是不可触及的,不可触及的对象就是垃圾对象。
标记清除
分为标记和清除两个阶段,在标记阶段,垃圾收集器跟踪从根对象的引用,在追踪的过程中对遇到的对象打一个标记,终于未被标记的对象就是垃圾对象,在清除阶段,回收垃圾对象占用的内存。能够在对象本身加入跟踪标记,也能够用一个独立的位图来设置标记。
标记清除法是基础的收集算法,其它算法大多时针对这个算法缺点的改进。
有两个缺点:
- 效率
- 存在内存碎片
复制算法
将内存划分为大小相等的两个区域,每次仅仅使用当中的一个区域,当这个区域的内存用完了,就将可触及的对象直接拷贝到新的区域并连续存放以消除内存碎片,当可触及对象复制完后,清除旧内存区域,改动引用的值。
这样的算法的缺点非常明显,可使用内存变为了原来的一半,太过浪费。
普通情况下,新生代中的对象大多生命周期非常短,也就是说当进行垃圾收集时,大部分对象都是垃圾,仅仅有一小部分对象会存活下来,所以仅仅要保留一小部分内存保存存活下来的对象即可了,用不着使用一半的内存。在新生代中一般将内存划分为三个部分:一个较大的Eden空间和两个较小的Survior空间(一样大小),每次使用Eden和一个Survior的内存,进行垃圾收集时将Eden和使用的Survior中的存活的对象拷贝到还有一个Survior空间中,然后清除这两个空间的内存,下次使用Eden和还有一个Survior,HotSpot中默认将这三个空间的比例划分为8:1:1,这样被浪费掉的空间就仅仅有总内存的1/10了。
这种内存空间划分是基于这样一种如果,即每次垃圾收集时大部分对象都是垃圾,仅仅有少部分对象存活。如果遇到例外的情况怎么办,在某次垃圾收集时存活下来的对象超过了预留的那个Survior空间的总大小,这就须要依赖其它的内存进行分配担保了(參考分代收集,前面的描写叙述中也说了这是新生代中的方法)
标记整理
普通的标记清除会在内存中留下内存碎片,复制算法假设不想浪费掉50%内存就须要有内存分配担保,通常是内存分代,但总有一代是没有其它代为它担保的。标记整理算法中标记的过程同标记清理一样,但整理部分不是直接清除掉垃圾对象,而是将活动对象统一移动一内存的一端,然后清除边界外的内存区域,这样就避免了内存碎片。也不会浪费内存,不须要其它内存进行担保
分代收集
大多数程序中创建的大部分对象生命周期都非常短,并且会有一小部分生命周期长的对象,为了克服复制收集器中每次垃圾收集都要拷贝全部的活动对象的缺点,将内存划分为不同的区域,很多其它地收集短生命周期所在的内存区域,当对象经历一定次数的垃圾收集存活时,提升它的存在的区域。通常是划分为新生代和老年代。新生代又划分为Eden区,From Survior区和To Survior区。
自适应收集器
监听堆中的情形,而且相应地调用合适的垃圾收集技术。
垃圾收集器
Serial
一个单线程的收集器,在进行垃圾收集时会暂停其它线程的工作,不适合用到Server端的虚拟机,但Client模式的模拟机还是能够用的,由于Client模式下的应用分配到的系统内存一般不大,垃圾收集能够非常快完毕。长处就是简单高效,没有线程交互开销,能够获得最高的单线程收集效率。
ParNew
Seria的多线程版本号,能够多个线程收集垃圾,但假设CPU仅仅有一核且没有超线程,效果就不一定比Serial好了,假设是多核或有超线程,能够保证效果好于Serial,除Seria之外,这是唯一能与CMS收集器配合的垃圾收集器
Parallel Scavenge
使用复制算法的新生代多线程垃圾收集器,Parallel Scavenge收集器的关注点和其它收集器不同,其它收集器的关注点是尽可能缩短垃圾收集时用户线程等待的时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput),即CPU用于执行用户代码的时间与CPU总消耗时间的比值。以缩短用户线程等待时间的收集器适合用于须要与用户交互的程序,而以吞吐量为目标的收集器适合用于不须要和用户太多的交互,以后台运算为目标的任务。
Parallel Scavenge能够通过參数设置每次垃圾收集须要停顿的时间和吞吐量目标,但停顿时间并非越小越好,这是以牺牲吞吐量和新生代空间为代价的,由于要使垃圾收集停顿时间缩小,仅仅能进行少量多次收集,或减小须要收集的空间大小。
另一个-XX:UseAdaptiveSizePolicy參数,指定这个參数后,就不须要手工指定新生代的大小、Eden区和Survior区的比例大小和晋升老年代对象年龄等细节參数了,虚拟机会依据收集到的信息动态调整这些參数,这称为自适应策略。
Serial Old
Serial的老年代版本号,单线程收集器,使用”标记-整理”算法,主要被Client模式下的虚拟机使用,当被使用在Server模式时主要有两个用途:
- 与Parallel Scavenge配合使用
- 作为CMS收集失败时的备选方案。
Parallel Old
Parallel Scavenge的老年代版本号,使用”标记-整理”算法,JDK1.6后提供的,在此之前,假设新生代选择了Parallel Scavenge,老年代仅仅能选择Serial Old,因为Serial Old是单线程的垃圾收集器,可能会影响收集性能。Parallel Old出现后,就能够分别在新生代和老年代选择Parallel Scavenge和Parallel Old组合了。
CMS(Concurrent Mark Sweep)
以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法,整个回收过程分为下面4步:
- 初始标记(CMS Initial Mark)
- 并发标记(CMS Current Mark)
- 又一次标记(CMS Remark)
- 并发清楚(CMS Concurrent Sweep)
初始标记与又一次标记阶段仍会暂停用户线程的执行。
初始标记仅仅是记录下GC Root能直接关联到的对象,速度非常快。
并发标记就是GC Roots Tracing了,速度较慢,但能够和用户线程同一时候执行。
又一次标记是修正并发标记时因为用户线程执行导致的标记记录变动,这个阶段会使用户线程停顿,停顿时间比初始标记略长,但仍小于又一次标记。
并发清除就是清除垃圾对象了,耗时较长,但可与用户线程同一时候工作。
CMS的缺点
- 对CPU资源敏感,并发阶段和用户线程同一时候执行,影响server的响应速度,尤其是CPU核心数少时
- 无法处理浮动垃圾,因为并发阶段用户线程同一时候在执行,可能会在垃圾收集过程中产生新的垃圾,CMS无法处理这部分浮动垃圾,因为在进行垃圾收集时用户线程同一时候在执行,须要额外的内存空间,所以不能等到内存满时再进行GC,须要预留一部分空间,假设预留的这部分空间不够GC时用户线程创建新对象使用,就会使用预备方法,使用Serial Old进行一次Full GC。
- CMS基于“标记-清除”算法,进行垃圾回收后会存在内存碎片,当申请大的连续内存时可能内存不足,此时须要进行一次Full GC,能够通过參数指定进行Full GC后或进行多少次Full GC后进行一次内存压缩来整理内存碎片。
G1(Garbage First)
基于”标记-整理”算法,避免了内存碎片的问题,并可精确地控制垃圾回收时的停顿。
G1收集器能够实现基本不牺牲吞吐量的前提下完毕低停顿的内存回收,不同于之前的垃圾回收器,G1收集器的回收区域不是整个新生代或老年代,而是将整个Java堆划分为多个固定大小的区域,并跟踪这些区域里的垃圾堆积程度,在后台维护一个优先列表,优先回收垃圾最多的区域。区域的划分使每次回收时间变短,而优先级的划分使得每次回收的区域能够回收最多的垃圾,这就使用G1收集器能够在有限的时间内获取最高的收集效率。
内存分配与回收策略
对像优先在新生代Eden区分配,当Eden区没有足够的内存时会发生一次Minor GC(新生代GC,Major GC或Full GC是老年代GC)
大对象能够直接在老年代分配内存,能够通过參数指定一个大小,大于这个大小的对象直接在老年代中分配内存。
进行Minor GC时,Eden区和一个Survior区中存活的对象会被拷贝到还有一个Survior区,一个对象每在一次Minor GC中存活下来一次后这个对象的年龄就加1,当这个对象的年龄大于一定值(默认15)就会进入老年代。
假设Survior中同样年龄的对象占用的空间大于Survior空间的一半,那么年龄大于或等于这个年龄的对象会直接进入老年代,而不用等到达到特定年龄
当进行Minor GC时,虚拟机会检測之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,假设小于,推断是否开启了HandlerPromotionFailure同意担保失败,假设开启了就仅仅进行Minor GC,否则进行Full GC。因为使用的之前Minor GC时的平均大小,假设某一次突然大小变大,导致老年代剩余空间不够,即担保失败,会再进行一次Full GC。
finalize
GC时会对活动对象进行标记,没有被标记的对象就是垃圾对象,但垃圾对象不会直接被清除,垃圾收集器还会推断是否须要运行对象的finalize方法,假设对象没有覆写finalize方法或它的finalize已经被运行过一次,那么是没有必要运行的,否则就觉得是有必要运行的,当被推断为有必要运行时,这个对象会被放入一个F-Queue队列中,由一个后台的低优先级的Finalizer线程运行队列中的对象的finalize方法,对象能够在这种方法中中复活自己,即又一次被其它对象引用,但这个函数仅仅会被垃圾收集器运行一下,第二次回收这个对象时这个函数不会再被调用。稍后GC会对F-Queue队列中的对象运行第二次标记。