3.4垃圾收集器
垃圾收集器是内存回收的具体实现。因为Java虚拟机规范中对垃圾收集器应该如何实现没有任何规定,因此不同厂商、版本的虚拟机垃圾收集器实现可能会有很大的差别。下面介绍7种作用于不同分代的垃圾收集器。(如果两个收集器之间存在连线,说明它们可以搭配使用)
Serial收集器:最基本、历史最悠久的收集器。是一个单线程收集器,且在它运行时,会暂停其他的工作线程。但它依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效:对于单CPU环境,由于它没有线程交互的开销,专心做GC自然效率很高;在用户桌面应用中,分配给虚拟机管理的内存也不会很大,Serial收集器的运行时间可以接受,所以在Client模式下运行该收集器是个很好的选择。
ParNew收集器:是Serial收集器的多线程版本。它是运行在Server模式下的虚拟机中首选的新生代收集器。有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器(Concurrent-Mark-Sweep)配合工作。
Parallel Scavenge收集器:是一个并行的多线程新生代收集器,也使用复制算法。它的主要目标是达到一个可控制的吞吐量,吞吐量=运行代码时间/(运行代码时间+GC时间)。
GC停顿时间越短就越适合强交互程序来提升用户体验;而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:控制最大垃圾收集停顿时间的-XX: MaxGCPauseMillis参数,以及直接设置吞吐量大小的-XX: GCTimeRatio参数。
Serial Old收集器:Serial收集器的老年代版本。是一个单线程收集器,使用“标记--整理”算法。主要是被Client模式下的虚拟机使用。在Server模式下,它主要有两大用途:一是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和“标记--整理”算法,从JDK1.6中开始提供。在注重吞吐量及CPU资源敏感的场合,都可以优先考虑与Parallel Scavenge收集器配合使用。
CMS收集器:是一种以获取最短回收停顿时间为目标的收集器。在互联网站或B/S系统的服务端上的Java应用都尤其重视服务的响应速度,CMS收集器就非常符合这类应用的需求。CMS收集器是基于“标记--清除”算法实现的。整个运作过程分为4个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记就是进行GC Roots Tracing的过程;而重新标记则是为了修正并发标记期间,因用户程序持续运行导致标记产生变动的那一部分对象的标记记录,此阶段停顿时间稍长于初始标记,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总的来说,CMS内存回收过程是与用户线程并发执行的。
CMS具有并发收集,低停顿的优点。但是,它也有三个显著的缺点:
1. 对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(CPU资源)而导致应用程序变慢,总吞吐量降低。
2. CMS收集器无法处理浮动垃圾;可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。浮动垃圾是指:伴随程序运行出现在标记过程之后的垃圾,CMS在本次收集中无法处理掉它们;CMS需要预留一部分空间作为GC时程序运作使用,当预留内存无法满足程序要求,就会出现“Concurrent Mode Failure”失败。
3. “标记--清除”算法的收集器在GC结束时会产生大量空间碎片。
G1收集器:相比原来的CMS收集器有两个显著的改进:一是G1收集器是基于“标记--整理”算法实现的,不会产生空间碎片;二是它可以非常精确地控制停顿,几乎已经是实时Java的垃圾收集器了。
G1收集器可以实现基本不牺牲吞吐量的前提下完成低停顿的内存回收,因为它能够极力避免全区域垃圾收集,将整个Java堆划分为多个大小固定的独立区域,并跟踪这些区域里面的垃圾堆积程序,在后台维护一个优先列表,每次根据允许的收集时间,有限回收垃圾最多的区域。(这就是Garbage First名称的由来)
3.5内存分配与回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配给对象的内存。下面会讲解几条主要的给对象分配内存的规则。
对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间时,虚拟机将发起一次Minor GC。Minor GC和Full GC的区别:
新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的)。Major GC的速度一般会比Minor GC慢10倍以上。
大对象直接进入老年代:大对象是指需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组(如常用的byte[]数组)。
虚拟机提供了一个-XX: PretenureSizeThreshold参数,令大于这个设置值得对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝。
要注意,PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。
长期存活的对象将进入老年代:虚拟机采用了分代收集的思想来管理内存,给每个对象定义了一个对象年龄的计数器。当年龄增加到一定程度,就会被晋升到老年代中。
动态对象年龄判定:为了更好适应不同程序的内存状况,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保:在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则直接改为一次Full GC;如果小于,则查看HandlePromotionFailure设置是否允许担保失败:如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。