一、垃圾收集算法
1.1、标记-清除算法
标记-清除算法是最基础的收集算法,算法分为“标记”、“清除”两个阶段,首先标记出所有需要回收的对象(使用可达性分析算法),再标记完成后统一收回,这是最简单的垃圾回收算法,同时也是后续算法的基础,后续的收集算法都是基于这种思路并对其的不足之处加以改进,算法执行过程如图:
回收前状态:
回收后状态:
由上图可以看出,此算法在执行后可能会造成大量内存碎片,有可能导致后续分配大对象是无法找到最够的连续空间而导致触发另一次的垃圾收集,同时标记和清除两个阶段的效率都不高,造成程序效率不高。
1.2、复制算法
为了解决效率问题,一种称为"复制"的收集算法出现了,他将可用内存容量划分成大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另一块,然后再把使用过的内存空间一次性清理掉,这样就使得每次清理都是对整个半区进行回收,提高了回收效率,同时内存分配也不用考虑内存碎片等复杂情况,算法执行如图:
回收前状态:
回收后状态:
从上图可以看到,复制算法提高了效率,并解决了内存碎片问题,但是每次都需要将内存预留一半作为保留区域,代价太高了。在现在的java虚拟机中普遍采用复制算法来回收新生代的对象,根据IBM的研究表明,新生代的对象98%是朝生夕死的,也就是说,大部分情况下每次GC后存货的对象不到10%,所以没必要预留50%的空间作为预留空间。在现在大部分商用虚拟机中,基本是将新生代内存划分为划分为三部分,较大的Eden空间和两块较小的Survivor空间,比例为8:1:1,两块Survivor空间分别叫做from和to,在GC开始前,对象只会存在于Eden空间和名为from的Survivor空间中,GC开始的时候,Eden空间和from空间中存活的对象会被复制到to空间,然后清理掉Eden空间和from空间,然后将from空间和to空间互换角色。这种方法使得新生代中的内存浪费只有10%,大大提高了内存利用率。执行过程如下图:
回收前状态:
回收后状态:
两个小问题:
1、如果GC的时候存活的对象大于10%?
如果每次GC的时候存活的对象大于10%,那么Survivor的to空间中的对象会被移到老年代中。
2、为什么需要Survivor空间,而且需要两个?
首先,如果没有Survivor空间的话,每次GC的时候,Eden空间存活的对象就只能被移到老年代,这样会使得老年代的空间很快被使用完毕,触发老年代的GC,在虚拟机中,老年代是一块很大的内存,对老年代进行GC所需时间远远大于新生代,频繁进行老年代的GC会影响程序的执行和响应速度。
至于为什么Survivor空间需要两块,这主要是为了防止出现内存碎片,如下图
只有一块Survivor空间的情况:
使用两块Survivor空间的情况
1.3、整理-标记算法
复制收集算法解决了效率和内存碎片问题,但是在对象存活率较高的时候必须进行大量复制操作,更关键的是,需要50%的预留空间或者一个分配担保空间来应对内存中对象100%存活的情况,所以这种算法适合新生代这种对象“死亡”比较迅速的分代,但是老年代就不适合使用这种算法进行GC操作。
根据老年代的特点,java大神们想出来了另一种“标记”-“整理”算法,标记过程与标记-清除算法一样,但是后续不是直接对对象进行清理,而是让所有存活的对象都向一端移动,最后清理掉边界以外的内存,算法执行过程如下图:
回收前状态:
回收后状态:
1.4、分代收集算法
分代收集算法并不是一种新的算法思想,只是根据对象的不同身存周期把内存分成不同的几块,一般是分为新生代和老年代,这样就可以根据其不同的特点使用不用的收集算法,一般来说,新生代的对象死亡率高,每次GC存活的对象不多,选用复制算法,而老年代中因为对象存活率高,没有而外的空间对其进行分配担保,一般采用标记-清除算法或者标记清理算法。
二、垃圾收集器
垃圾收集算法是内存回收的理论方法的话,垃圾收集器就是对垃圾收集算法的实现,java虚拟机规范没用对垃圾收集器的实现做任何的规范,所以各厂商都有自己的垃圾收集器,本文主要是对一些常见的垃圾收集器进行描述。
1、Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器。这是一种单线程的收集器,这里的单线程不仅仅只是说他只会使用一个CPU或者线程去完成垃圾收集工作,更重要的是,Serial垃圾收集器在工作时必须暂停其他线程,知道垃圾收集结束,对于程序来说,这会导致程序的执行效率大幅下降,值得注意的是,垃圾收集器导致的停顿问题一直是困扰java虚拟机设计者的一个难题,到目前为止,还没有一种真正意义上的消除时间停顿的垃圾收集器出现。
虽然Serial收集器已经很久远,并且会出现严重的时间停顿问题,但是它依然是虚拟机在Client模式下的默认新生代垃圾收集器,主要是它在单个cpu的情况下,没有线程交互的开销,只需要专心收集垃圾就可获得最高的单线程收集效率。同时,在桌面应用场景中,分配给虚拟机的内存一般比较小,停顿的时间也比较短,一般这种停顿时间可以接受。
2、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余的行为后于Serial收集器的完全一样。值得一提的是ParNew收集器是许多运行在Server模式下单虚拟机中首选的新生代收集器,其中一个与性能无关的原因是,除了Serial收集器外,目前只有它能和CMS收集器配合工作。
3、Parallel Scavenge收集器
这也是一个新生代收集器,和ParNew收集器一样,是一个使用复制算法的收集器,同时也是并行的多线程收集器。Parallel Scavenge收集器的特点是,其他的垃圾收集器关注的点是尽可能的收集垃圾是用户线程的等待时间,但是Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,也就是运行用户代码的时间和CPU消耗的总时间的比值,即:吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间),如虚拟机一共运行100分钟,垃圾收集花掉一分钟,那吞吐量就是99%。
当程序的停顿时间越短的时候,就越适合需要用户交互的程序,这样用户的等待时间就会缩短,提高用户的体验,但是一些需要后台计算的程序,他们更注重高效利用CPU时间,尽快完成运算任务,这时候高吞吐量的收集器就显得很适合。Parallel Scavenge收集器提供两个参数用户精确控制吞吐量,分别是控制最大垃圾停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis允许的值是一个大于0的毫秒数,垃圾收集器将尽可能地保证内存回收花费的时间不超过设定的值 ,这里需要注意的是,这个值越小并不一定表示垃圾收集速度变快,应为GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,收集300M空间的时间比收集500空间的时间短,但是这也使得GC变得更频繁,导致吞吐量下降。
-XX:GCTimeRatio参数的值是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比例,如设置为19,那么GC的时间就占总时间的1/(1+19)也就是5%。
除了控制GC时间的参数外,Parallel Scavenge收集器还有一个-XX:+UserAdaptiveSizePolicy参数值得一提,这是一个开关参数,打开之后,就不需要手动指定新生代大小中Eden和Survivor的比例等参数,虚拟机会自动根据当前系统的运行情况收集性能监控信息,动态调整合适的停顿时间或者最大的吞吐量,这种调节方式称为GC的自适应的调节策略,这也是Parallel Scavenge与ParNew收集器的一个重要区别。
4、Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器使用标记-整理算法,也主要实在client模式下的虚拟机使用,同时他还有一个作用就是在server模式下作为CMS器的后备预案,在CMS收集器发生Concurrent Mode Failure时使用。
5、Parallel Old收集器
Paraller Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法,这是JDK1.6才开始提供的收集器,在此之前,Parallel Scavenge收集器配套使用的老年代收集器只有Serial Old收集器,但是Serial Old收集齐器的性能问题,导致使用Parallel Scavenge后整体性能未必好,Paraller Old出现之后,在注重高吞吐量的场合,可以优先考虑Parallel Scavenge收集器加Paraller Old收集器。
6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种获取最短回收停顿时间的垃圾收集器,基于标记-清除算法,目前很大一部分的java应用都使用此种垃圾收集器,特别是重视服务器的相应速度,给用户带来更好的体验,CMS收集器就非常符合这类应用的需求。这种收集器的执行过程分为四部:
1、初始标记
2、并发标记
3、重新标记
4、并发清除
其中,初始标记、重新标记两个阶段仍然需要中断用户线程,初始标记只是标记GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行可达性分析,重新标记就是标记并发标记期间因用户程序而运作导致产生变动的那一部分记录,重新标记的时间会大于初始标记,但是远远少于并发标记。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以和用户线程一起工作,所以总体上来说CMS收集器的回收过程是与用户线程一起并发执行的,通过下图可以直观了解CMS收集器的执行过程
CMS收集器是一款很优秀的收集器,它的有点很明显:并发收集、低停顿。但是CMS收集器也有以下的缺点:
1、CMS收集器或者说基于面向并发设计的收集器都有一个缺点——对CPU资源非常敏感。在并发阶段,他虽然不会导致用户线程停顿,但是会因为占用了一定的CPU资源而导致程序变慢,总吞吐量降低。CMS默认的回收线程数是(CPU数量+3)/4,可以看到当CPU数量在4个以上时,并发回收时垃圾收集线程占用的CPU资源在25%左右,但是当CPU线程不足四个,CMS收集器对程序的影响就相当大,在只有两个CPU时,CMS收集器甚至会占用50%的CPU资源,当你的程序执行速度忽然降低一半,这是让人无法接受的。为了应对这种情况,虚拟机提供了一种叫做“增量式并发收集器”的CMS收集器变种,也就是在并发标记和并发清理阶段让用户线程和GC线程交替运行,使得GC的时间变长,但是对程序的影响会减少。不过遗憾的是,这种收集器的效果在实际生产中并没有预期的那么好,目前版本中,这种垃圾收集器已经不再提倡使用。
2、无法清理浮动垃圾。由上图我们可以看到,当GC线程执行并发清除的时候,用户线程还在并发运行,这就导致这段时间产生的垃圾无法被即使清除,当垃圾堆积得过多之后可能出现"Concurrent Mode Failure"失败而去触发一次Full GC。同时也还是因为并发清除阶段用户线程还需要运行,就是对CMS垃圾收集器不能像其他垃圾收集器那样等到老年代几乎快满了采取收集,需要预留一部分空间提供给用户线程使用。在JDK1.5时期,默认是老年代使用68%的空间后就会激活CMS收集器,在JDK1.6中这个阈值被提升至92%,当然。如果CMS运行期间预留的空间不够,虚拟机就会启动Serial Old收集器来重新进行老年代的收集,不过这样就会使得程序的停顿时间变长,我们可以使用-XX:CMSInitiatingOccupancyFraction参数来设置这个阈值。
3、最后一个缺点就是CMS收集器是基于标记-清除算法实现的收集器,上面我们讲过,这种算法会导致大量内存碎片的产生,将会给大对象的内存分配带来麻烦,这使得程序不得不进行Full GC。为了解决这一问题,CMS提供了一个-XX:UseCMSCompactAtFulCollection开关参数(默认开启),用户在CMS收集器进行Full GC的时候来时内存碎片的合并整理,但是内存碎片的合并整理无法并发执行,所以这种情况也会导致停顿时间变长。虚拟机还提供另一个参数:-XX:CMSFullGCsBeforeCompaction,这个参数适用于设执行多少次不压缩的Full GC后执行一次带压缩也就是带空间整理的Full GC(默认值0,也就是每次Full GC都是会进行内存碎片的整理)
7、G1垃圾收集器
G1垃圾收集器是在JDK7u4时被sun公司正式纳入商用收集器的的,HotSpot开发团队赋予他的使命时在未来可以替换掉JDK1.5中发布的CMS垃圾收集器,与其他GC收集器相比,G1收集器有如下特点:
1、并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短停顿时间,同时G1收集器可以通过并发的方式让java程序在GC的过程中继续运行
2、分代收集:与其他收集器一样,G1收集器仍然保留分代的概念。虽然G1收集器可以不需要和其他收集器就可以管理整个GC堆,但是他仍然可以采用不同的方式去处理新创建的对象和存活了一段时间、熬过多次GC的旧对象以获得更好的收集效果。
3、空间整合:与CMS收集器不同,G1收集器从整体看采用标记-整理算法实现,但是从局部来看是采用了复制算法,但是无论如何,都意味着G1运行期间不会产生内存碎片。
4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不到超过N毫秒。
在G1之前的其他收集器的收集范围几乎都是整个新生代或者老年代,而G1不再是这样使用G1收集器是,Java堆的内存被分为很多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域垃圾扫描,G1跟踪各个Region里面垃圾堆的价值大小(回收获得空间和需要时间的比值),在后台维护一个优先列表,每次回收价值最大的Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1收集器的运行大致可以分为以下几个步骤:
1、初始标记
2、并发标记
3、最终标记
4、筛选回收
他的前几个步骤和CMS收集器有很多相似之处,初始标记仅仅记录GC Roots能直接关联到的对象,并发标记就是进行可达性分析,这里耗时较长还是可以和用户线程并发执行,最终标记是为了修正并发标记期间产生的那部分变动记录,虚拟机将这段时间对象的变化记录在线程Remembered Set Logs里面,最终标记阶段就是把Remembered Set Logs的数据合并到Remembered Set中,这个阶段需要停顿线程,但是可以并发执行,最终筛选阶段就是对各个Region的回收价值和成本进行排序,更具用户期望的GC时间制定回收计划。执行过程如下图:
垃圾收集器的使用范围和搭配关系图: