JVM垃圾回收策略与垃圾收集器

本文是在读完深入理解Java虚拟机(周志明著)后的总结,有很多部分借鉴了原书的说法,如果想深入了解这些内容,推荐看原书

JVM垃圾回收策略

垃圾回收主要包括确定垃圾和回收垃圾两步,JVM采用可达性分析算法分析哪些是废弃对象需要回收,然后采用GC算法进行垃圾清理(GC算法)。由于堆内存的使用情况影响了垃圾回收,所以JVM将堆内存划分成了几个区域,不同区域采用不同的垃圾收集方式

1.确定对象是否可被清除

1)引用计数算法

引用计数算法的实现方式是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;计数器为0的对象就是不可能再被使用的

引用计数算法的优点是实现简单,判定效率高。缺点是没有办法处理循环引用问题

循环引用是指两个对象相互引用了对方,这样即使他们不再被其他对象调用,已经成为需要被清除的垃圾时,他们的引用计数器值均为1而不是0,从而无法被判定为垃圾并进行清除。Java没有采用这种算法确定垃圾对象

2)可达性分析算法

可达性分析(Reachability Analysis)算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。可达性分析算法没有循环引用问题,他是JVM采用的判断对象是否可被清除的算法
这里写图片描述

2.JVM堆内存划分

JVM将运行时内存分成了几个区域,其中堆区由于其存储的是共享数据(对象),所以需要不断地进行内存分配和垃圾回收,而其他区域则很少或不用进行这些操作

为了更好地分配和回收堆内存,JVM将堆内存划分为几个部分,便于进行内存分配和垃圾回收。划分的各部分内存各有不同的用途,采用的垃圾收集器也不相同。而且,各部分内存的大小和采用的垃圾收集器都可以通过参数进行调整,以达到最优的运行状态

首先堆内存划分为老年代和新生代,新生代存储刚生成的对象,老年代存储已经生成并使用了一段时间的对象。新生代又可以分为两个区域,分别为Eden区和Survivor区,其中Survivor区又分为三个部分,一个Eden区和两个Survivor区,两个Survivor区分别名为From Survivor和To Survivor

各个区域都有默认的大小(可以通过参数进行调节),其中老年代占整个堆内存的2/3,新生代占1/3,Eden区占新生代的8/10,From Survivor和To Survivor各占1/10
这里写图片描述

JVM中的垃圾收集器

Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器有很大差别,各个收集器有不同的工作原理工作方式,适合不同的工作场景,具体场景下使用哪种收集器需要根据情况选择

新生代和老年代的垃圾收集情况差异较大(新生代中产生的垃圾远多于老年代),所以大部分收集器工作于新生代或老年代中的一个,但是G1收集器可以同时胜任这两个区域。下图展示了垃圾收集器的适用区域,例如Serial收集器适合在新生代工作。由于一种收集器一般只胜任一个区域,所以大多情况下需要两款收集器配合工作,一款收集老年代,一款收集新生代。下图中连起来的两个收集器可以配合工作
这里写图片描述

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个单线程的收集器,只会使用一个CPU开启一条收集线程去完成垃圾收集工作。还有更重要的一点是,它在工作时必须关闭其他正在工作的用户线程,直到它收集结束,这种工作方式被称作:“Stop The World”

相对于之后的CMS等收集器可以在不关闭用户线程的情况下运行垃圾回收线程,“Stop The World”的方式不适合处理待处理如虚拟机Server模式下那种垃圾较多的场景,因为这种情况下用户线程就得停顿不少时间,而如果待处理垃圾不多,如在虚拟机Client模式下,“Stop The World”可以在极短时间内完成垃圾回收,不影响用户体验

关闭用户线程进行垃圾回收的效率要比用户线程和垃圾处理线程并行更高,并行的垃圾处理方式也有更高的执行频率(即因为回收效率低所以要频繁进行垃圾回收),会在后台频繁开启垃圾回收线程,这会消耗CPU资源,使得程序运行速度减慢,不过为了能够不让程序停顿,这点牺牲是值得的,Serial收集器之后的很多收集器,都是围绕着如何缩短用户停顿的时间设计的

Serial收集器的优点是简单而高效,虽然已经有很长的历史,但它还是虚拟机运行在Client模式下的默认新生代收集器

这里写图片描述

ParNew收集器

ParNew收集器和Serial收集器非常相似,唯一的不同就是ParNew收集器使用多线程进行垃圾收集,在JDK1.5中它经常和CMS收集器配合使用,在新生代工作

由于存在线程交互的开销,ParNew收集器在单CPU的环境中工作效率不如Serial收集器,但是随着可以使用的CPU的数量的增加,它的多线程优势就渐渐体现出来。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数
这里写图片描述

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它使用复制算法,并行多线程收集。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)

吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Serial Old收集器

Serial Old是收集器Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器和Serial收集器一样,其主要用于Client模式下的虚拟机
这里写图片描述

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。Parallel Old收集器的主要用途是和Parallel Scavenge收集器配合,在注重吞吐量以及CPU资源敏感的场合使用
这里写图片描述

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
这里写图片描述
CMS收集器虽然做到了并发执行,低停顿,但是为了达到这两个特性它牺牲了一些性能,CMS收集器有几个明显的缺点:

第一点是CMS收集器对CPU资源非常敏感,它采用并发收集,占用了一部分线程(或者说CPU资源)而导致应用程序变慢,系统总吞吐量会降低。在CPU不多时,这种速度降低就很明显

第二点是CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉,这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用

第三点是由于CMS是一款基于“标记—清除”算法实现的收集器,使用这种GC算法会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象不得不提前触发一次Full GC,频繁地触发Full GC会很大地降低效率

G1收集器

G1(Garbage-First)收集器是CMS的改进型和替代者,GI收集器和CMS收集器一样,采用多线程分代收集,但是GC算法没有采用和CMS一样的“标记—清除”算法,而是采用了“标记—整理”算法,避免了收集后产生空间碎片的情况。同时G1收集器可以设定停顿时间,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1收集器另一个优于CMS收集器的方面是它除了可以工作于老年代,还可以在新生代工作

G1收集器能有以上诸多优点,是因为它的回收方式很特别,G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏

这里写图片描述

猜你喜欢

转载自blog.csdn.net/eagleuniversityeye/article/details/80196463