垃圾收集器
如果垃圾收集算法是内存回收的方法论,垃圾回收器就是内存回收的具体实现。JVM规范中没有对垃圾回收器的任何规定。这本书中讲了基于JDK1.7 update14之后的HotSpot虚拟机,这个虚拟机所包含的所有收集器如下图。
HotSpot虚拟机的垃圾收集器
图中展示了7种作用于不同分代的收集器,连线表示两个收集器之间可以搭配使用。收集器所在的区域表示的是它所能作用的区域。
1/1 Serial 收集器:
Serial收集器是最一个单线程收集器,它是作用于新生代的,就是它在进行垃圾回收运作的期间,必须去暂停其他所有的工作线程。这就是“Stop The World”的由来(后面成为STW),全世界都停了,就你自己在这收集垃圾。
下图描述了Serial收集垃圾的过程:
Serial收集器收集垃圾的过程
优点:
简单、效率高(和其他单线程的收集器相比的话)。
缺点:
会造成STW。
应用场景:
对于限定单个CPU的环境来说,Serial收集器由于没有现成交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
1/2 ParNew 收集器:
ParNew 收集器其实就是Serial 收集器的多线程版,除了使用多线程进行垃圾回收之外,还包括了Serial收集器的所有控制参数、收集算法、STW、对象分配规则、等等。 ParNew 收集器的工作过程如下:
ParNew 收集器的工作
ParNew 收集器除了是多线程之外,其他与Serial收集器相比并没有太多创新之处,但是它确是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器,只有它能与CMS收集器(后面会说道)配合工作,ParNew 收集器在单CPU的环境中没有Serial收集器的效果好,但是随着CPU的增加,它的效果会越来越好(现在CPU越来越多了,比如我的电脑就是8个,如果是服务器的话就更多了)。它默认开启的线程数是CPU的数量相同。
并发和并行的概念
这两个名词老是然在一起,我来说说自己的理解吧。
并行:
并行就是在时间上,也就是我们生活中所说的真正意义上的一起运行。
并发:
并发包括并行(就是继承于并行),但是还包括 程序通过CPU的时间片段,实现一个看起来是一起运行的情况。简单说就是并发是并行的子类,它还新增加了一个看起来是一起运行的方法。
1/3 Parallel Scavenge 收集器
Parallel Scavenge 是一个新生代收集器,它是使用复制算法的收集器,英文的意思是并行收集器。它的关注点是达到一个可控制的吞吐量。吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间),比如虚拟机总共运行100分钟,其中垃圾回收花掉1分钟,那吞吐量就是99%。程序的停顿时间越短,那么用户的体验肯定越好(因为不卡顿),但是程序总体的运行时间不一定快,举个栗子,一个程序每隔10秒停顿100毫秒,另一个程序每隔100秒停顿500毫秒。那么用户体验上肯定是第一个程序好(因为用户感觉不到100毫秒的停顿),但是第二个程序运行的更快,也就是吞吐量高。
应用场景:
高吞吐量可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。除此之外,Parallel Scavenge 收集器具有自适应调节策略,它可以将内存管理的调优任务交给虚拟机去完成。自适应调节策略也是Parallel Scavenge与 ParNew 收集器的一个重要区别。
Serial Old 收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理的算法。
应用场景:
Serial Old 收集器主要有两个用途:一个是在JDK1.5之前与Parallel Scavenge 收集器搭配使用(因为Parallel Scavenge 收集器是新生代收集器,需要一个老年代收集器的配合)。另一个是作为CMS 收集器的后备预案,在它发生 Concurrent Mode Failure的时候使用。
Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge 收集器的老年代版本,使用多线程和标记-整理算法,这个收集器是JDK1.6之后才开始提供,从HotSpot虚拟机的垃圾收集器的图中也可以看出,Parallel Scavenge 收集器无法与CMS收集器配合工作(因为一个是为了吞吐量,一个是为了客户体验(也就是暂停时间的缩短))。
应用场景:
与Parallel Scavenge 收集器配合使用,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge 收集器加Parallel Old 收集器。
CMS 收集器
CMS(Concurrent Mark Sweep(意思是并发 标记 清除))收集器是一种以获取最短停顿时间(用户体验好)为目标的老年代收集器。从名字上可以看出,它是使用标记-清除算法来实现的一个并发收集器,它的运行过程分为4个步骤:
步骤:
- 初始标记:此过程需要STW(Stop The Word),它只是标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记:就是GC Roots进行扫描可达链的过程,为了找出哪些对象需要收集。这个过程远远慢于初始标记,但它是和用户线程一起运行的,不会出现STW,所有用户并不会感受到。
- 重新标记:为了修正在并发标记期间,用户线程产生的垃圾,这个过程会比初始标记时间稍微长一点,但是也很快,和初始标记一样会产生STW。
- 并发清理:在重新标记之后,对现有的垃圾进行清理,和并发标记一样也是和用户线程一起运行的,耗时较长(和初始标记比的话),不会出现STW。
由于整个过程中,耗时最长的并发标记和并发清理都是与用户线程一起执行的,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。下面是CMS收集器运行示意图:
image.png
缺点:
CMS虽然是一个并发低停顿的收集器,但是它仍然有以下几个明显的缺点:
- CMS对CPU资源非常敏感:CMS虽然实现了在垃圾收集的时候用户的线程基本上不停顿,但也因为占用了一部分CPU的资源而导致程序变慢,总吞吐量降低。
- CMS无法处理浮动垃圾:由于CMS在并发清理阶段(最后一个阶段)用户线程仍然在运行,那么自然就会产生新的垃圾,这一部分垃圾出现在标记过程之后,CMS无法在当次垃圾收集中处理掉他们,只好留在下一次GC时清理,这一部分垃圾就成为浮动垃圾。
- Concurrent Mode Failure:因为在垃圾收集阶段用户线程任然需要执行,那么就还需要预留足够的内部空间给用户线程使用。因为CMS收集器不能像其他老年代收集器那样等到老年代几乎满了的时候再收集,需要预留一部分空间提供并发收集过程中的程序运行,如果这部分预留的内存还是无法满足程序运行时的需求,就会出现一次“Concurrent Mode Failure”垃圾收集失败,这时虚拟机将启动后备预案(Serial Old收集器中也提到了):临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿的时间就更长了(因为Serial Old是单线程的),反而会降低性能。
- 空间碎片问题:由于CMS是采用标记-清除算法实现的,所以可能在垃圾收集时产生大量的空间碎片(上一节的垃圾回收算法中提到过),这就会给大对象分配造成麻烦,可能会提前进行一次Full GC(Full GC就是全部内存的垃圾回收,包括所有的老年代和新生代)。为了解决这个问题CMS提供了一个参数-XX:UseCMSCompactAtFullCollection(默认是开启的),用于在顶不住压力要进行Full GC时,开启内存碎片整理过程,但是停顿时间会很长。虚拟机设计者还提供了一个额外的参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行了多少次不压缩碎片的Full GC后,跟着执行一次压缩的(默认是每次都压缩)。
G1 收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿的成果之一,不过这本书是JDK1.7的时候,G1收集器并没有被普遍使用,现在已经JKD11了....在JDK9的时候就已经将G1收集器设为默认的收集器了,而且这本书中对这个收集器的描述也不像其他的收集器那样清晰(简直就是天书),所以以下内容可能有我理解不好的地方,欢迎指出。
特点:
G1收集器是面向服务端的收集器,它的思想就是首先回收尽可能多的垃圾(这也是Garbage-First名字的由来),G1的所有过程都是STW的。
- 并行与并发:G1能充分的利用多CPU,多核环境下的硬件优势,使用多个CPU来缩短STW停顿的时间。
- 分代收集:虽然分代的概念(新生代、老年代)在G1中仍然有效,但它是能作用在所有年代(新生代、老年代、永久代)的收集器,可以不通过其他收集器的配合就能独立完成整合GC堆的管理。
- 空间整合:与CMS的标记-清除算法不同,G1从整体上看是采用标记-整理算法,从局部(两个G1分区之间,之后会详细的说)是复制算法。所以都不会产生大量的空间碎片,有利于程序的长期执行。
- 可预测的停顿:这是G1相对于CMS的另一大优势,G1和CMS一样都是关注于降低停顿时间,但是G1能够让使用者明确的指定在一个M毫秒的时间片段内,消耗在垃圾收集的时间不得超过N毫秒。
Region(G1的分区)
虽然G1有分代的概念,但是实际上G1是将堆划分成若干个大小相同的独立分区(默认是2000个),然后将这些分区分到不同的角色上(新生代、老年代),但是哪个代分配多少个是不确定的。
Card(卡片)
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
Remembered Set (已记忆 集合)
在G1收集器中,Region之间的对象引用(以及其他的收集器中新生代和老年代之间的对象引用),虚拟机都是使用RRemembered Set来避免全堆的扫描(生成可达链)。在G1中每个Region都有一个对应的Remembered Set,虚拟机发现程序在堆Reference型的数据进行写操作时,会产生一个Write Barrier(写屏障)暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable(卡片记录表)把相关引用的信息记录到被引用的对象所属的分区的Remembered Set中,当进行垃圾回收时,在GC跟节点的枚举范围中加入Remembered Set即可保证不对全堆扫描。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤(与CMS很类似):
- 初始标记:与CMS的初始标记类似
- 并发标记:与CMS的并发标记类似
- 最终标记:与CMS的重新标记类似
-
筛选回收:这一阶段首先对各个分区的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定GC回收计划,这里其实也是可以和用户线程并发进行的,但是因为只回收一部分分区,时间是用户可控制的,所以暂停用户线程会大大提高收集效率。
下图是G1收集器运行过程示意图:
G1收集器运行过程示意图
优点分析(参考自详解 JVM Garbage First(G1) 垃圾收集器):
G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始化最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。
GC日志
2016-03-20T14:34:55.118-0800: [GC [PSYoungGen: 6123K->400K(38912K)] 6123K->400K(125952K), 0.0012070 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-03-20T14:34:55.119-0800: [Full GC [PSYoungGen: 400K->0K(38912K)] [ParOldGen: 0K->282K(87040K)] 400K->282K(125952K) [PSPermGen: 2622K->2621K(21504K)], 0.0084640 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
最前的数字”2016-03-20T14:34:55.118-0800”和“2016-03-20T14:34:55.119-0800”表示GC发生的时间。
日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,如果有Full表明这次GC发生了STW。
下面的“[PSYoungGen”、“[ParOldGen”表示使用的垃圾收集器的名字
[PSYoungGen表示Parallel Scavenge 收集器,[ParOldGen表示是Parallel OId收集器。
后面括号内的“6123K->400K(125952K)”表示的是:
GC前该内存区域已使用的容量->GC后该内存区域已使用的容量(该内存区域的总容量)。
垃圾收集器参数总结
image.pngG1收集器参数:
image.png
网友评论