文章目录
如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。
1 概念补充
1.1 单线程与多线程
单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程。
1.2 串行、并发和并行
串行
:多个任务在单个核心运行,执行完一个再执行另一个。并发
:多个线程在单个核心运行,同一时间一个线程运行,系统不停切换线程,看起来像同时运行,实际上是线程不停切换。并行
:多个线程分配给不同的核心,多个线程同时运行。
并发和并行的区别
- 并发体现在
单个核心
,逻辑上同步
运行,并发的多个任务之间是互相抢占资源
的。 - 并行体现在
多个核心
,物理上同步
运行,并行的多个任务之间是不互相抢占资源
的。 只有在多CPU的情况中,才会发生并行
。否则,看似同时发生的事情,其实都是并发执行的。
- 串行:在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束( “Stop The World” )。
- 并行:多条垃圾收集线程在协同工作,通常默认此时用户线程处于等待状态。
- 并发:同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
1.3 吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
1.4 client模式与server模式
当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。
可通过java -version命令查看虚拟机采用的是哪种工作模式,下图表示-server模式。
2 经典垃圾收集器
下图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。垃圾收集器所处区域表示它是属于新生代收集器还是老年代收集器。自JDK 9开始,取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持。
新生代收集器:Serial、ParNew、Parallel Scanvenge
老年代收集器:Serial Old、CMS、Parallel Old
通用收集器:G1
常用组合:Serial+Serial Old, Parallel Scavenge+Parallel Old,ParNew+CMS,G1(不需要组合其他收集器)。
jdk8默认的垃圾收集器是Parallel Scavenge 和 Parallel Old,jdk9默认的垃圾收集器是G1
2.1 Serial与Serial Old收集器
- Serial是
单线程
的收集器,只会使用一个线程进行垃圾收集工作,使用标记-复制算法。 - Serial Old是Serial收集器的老年代版本,它同样是一个
单线程
收集器,使用标记-整理算法。
这个收集器的主要意义也是供客户端(Client)模式下的HotSpot虚拟机使用。如果在服务端(Server)模式下,它也可能有两种用途:一种是在
JDK5以及之前的版本中与Parallel Scavenge收集器搭配使用
,另外一种就是作为CMS收集器发生失败时的后备预案
,在并发收集发生Concurrent Mode Failure时使用。
- 收集区域: Serial (新生代),Serial Old(老年代)
- 使用算法: Serial (标记复制法),Serial Old(标记整理法)
- 缺点:在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(
Stop The World
)。 - 优点:与其他收集器的单线程相比,Serial
简单而高效
。
- 对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
- Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
- 适用场景:运行在
客户端(Client )模式
下的虚拟机。
为什么复制算法会“Stop The World”
- 在进行标记的时候,如果工作线程不停止的话,那么肯定会有新对象生成。这些对象是没有被标记的,里面可能有存活的对象,也可能有已经没有被引用的垃圾对象。复制算法回收的时候,是需要把存活对象移到Survivor中的。而那些没有被标记的,有存活的,也有垃圾对象,显然不能全部移动到survivor中的。
- 另外如果工作线程没有停止工作,那么工作线程和回收线程是一起执行的。工作线程可能会打乱对象之间的引用关系,这个对回收线程进行标记是有影响的。
2.2 ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本
,ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处。在多核机器上,其默认开启的收集线程数与cpu数量相等。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
优点:多线程
收集,CPU多核环境下效率要比Serial高,是新生代收集器中唯一能与CMS配合
的收集器。
- 在JDK5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。
- 在JDK5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器。
- 自JDK9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。
2.3 Parallel Scavenge 和 Parallel Old收集器
- Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是
达到一个可控制的吞吐量
。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作吞吐量优先收集器
。 - Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
- 收集区域: Parallel Scavenge (新生代),Parallel Old(老年代)
- 使用算法: Parallel Scavenge (标记复制法),Parallel Old(标记整理法)
- 优点: 高吞吐量,且能进行精确控制吞吐量、自适应调节策略
Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为垃圾收集的
自适应调节策略
。
- 适用场景:在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
2.4 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间
为目标的收集器,基于标记清除算法。它的运作过程相对于前面几种收集器来说要更复杂一些,过程分为四个步骤:
1)初始标记(CMS initial mark)
初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快。
2)并发标记(CMS concurrent mark)
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
3)重新标记(CMS remark)
重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
4)并发清除(CMS concurrent sweep)
并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”
。由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
- 优点:并发收集、低停顿
- 缺点:
1)CMS收集器对处理器资源非常敏感
CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
2)CMS收集器无法处理“浮动垃圾”
在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
3)有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生
同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案,冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集
,但这样停顿时间就很长了。
4)CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,CMS收集器提供了一个
-XX:+UseCMS-CompactAtFullCollection开关参数
(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程
,由于这个内存整理必须移动存活对象,是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore-Compaction
(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理
(默认值为0,表示每次进入Full GC时都进行碎片整理)。
- 适用场景:关注服务的响应速度,希望系统停顿时间尽可能短
2.5 Garbage First(G1)收集器
Garbage First(简称G1)收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
G1收集器跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的比值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
收集器能够对扮演不同角色的Region采用不同的策略去处理。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为
只要大小超过了一个Region容量一半的对象即可判定为大对象
。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中
。
- 优点:可以指定最大停顿时间、分Region的内存布局、不会产生内存碎片
- 缺点:在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS要高
- 适用场景:
- 针对
大内存、多处理器的机器
,比如堆大小至少6G或以上 - 超过50%的堆空间都被活动数据占用
- 在要求
低延迟
的场景,也就是GC导致的程序暂停时间要比较少,0.5-1秒之间 对象在堆中分配频率或者年代升级频率变化比较大
,防止高并发下应用雪崩现象的场景
- 针对
2.5.1 G1新生代垃圾回收
G1的新生代也有Eden和Survivor,其触发垃圾回收的机制也是类似的,随着不停在新生代Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%。
- 假设堆4G,最大2048个region,每个region为2M,新生代最大60%=2.4G。一旦新生代达到了设定的占据堆内存的最大大小60%,按照上面的数据大概就是有1200个Region,里面的Eden可能占据了1000个Region,每个Survivor是100个Region。
对象进入老年代的条件跟之前几乎是一样的
。 - 当Eden区满了,此时触发新生代的GC,G1就会采用
复制算法
来进行垃圾回收,进入“Stop the World”状态
,然后把Eden对应的Region中的存活对象复制到Survivor对应的Region中,接着回收掉 Eden对应的Region中的垃圾对象
。 - 但这个过程与之前是有区别的,因为
G1是可以设定目标GC停顿时间的
,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms,那么G1会通过对每个Region追踪回收它需要多少时间,可以回收多少对象来选择回收一部分Region,保证GC停顿时间控制在指定范围内,尽可能多地回收对象
。
2.5.2 G1老年代垃圾回收
不计算与用户线程并发执行的过程,运作过程大致可划分为以下四个步骤:
1)初始标记(Initial Marking)
仅仅只是标记一下GC Roots能直接关联到的对象,这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2)并发标记(Concurrent Marking)
从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB(原始快照)记录下的在并发时有引用变动的对象。
3)最终标记(Final Marking)
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4)筛选回收(Live Data Counting and Evacuation)
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
除了并发标记外,其余阶段都是要完全暂停用户线程(Stop The World)
,它并非纯粹地追求低延迟,它的目标是在延迟可控的情况下获得尽可能高的吞吐量。
从局部来说G1是使用的标记复制法,把存活对象从一个Region复制到另外的Region,但从整个堆来说G1的逻辑又相当于是标记整理法,每次垃圾收集时会把存活的对象整理到其他对应区域的Region里,再把原来的Region标记为可回收区域记录到CSet里,所以G1的每一次回收都是一次整理过程,所以也就不会产生空间碎片问题。
2.5.3 G1回收失败时的Full GC
在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,把各个Region中存活的对象复制到其他空闲的Region中。如果出现复制时没有空闲Region可以存放存活对象了,就会导致Full GC。
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
3 各个垃圾收集器比较
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在万能的收集器,所以我们选择的只是对具体应用最合适的收集器。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/71486.html