JVM(三)—垃圾回收算法及垃圾收集器


一、哪些内存需要回收

垃圾收集器主要回收堆内存和方法区的对象。

  • 程序计数器、虚拟机栈、本地方法栈3个区域是线程私有的,随线程而生,随线程而灭。所以方法结束或者线程结束时,内存自然就被回收了。
  • Java堆和方法区中只有在运行时才知道创建哪些对象,所以内存的分配和回收都是动态的。
  • 堆区回收的主要是死亡的对象; 
    方法区回收的主要是废弃的常量和无用的类(废弃的常量只需要一次标记即可)。


二、如何判断对象死亡

1. 引用计数法
给一个对象加上一个引用计数器。引用它,计数器+1;引用结束,计数器-1. 
当引用计数器为0时,表面对象死亡。 
缺点:很难解决对象之间互相引用的问题。

2. 可达性分析算法
判断一个对象到GC Roots 有没有引用链。没有的话就表明需要回收。


JVM(三)—垃圾回收算法及垃圾收集器_用户线程


如图,Object4,Object5,Object6之间虽然互相关联,但是到GC Roots是不可达的,所以他们需要回收。

可作为GC Roots的对象:

  • 虚拟机栈中的引用的对象;
  • 方法区类静态属性、常量引用的对象;
  • 本地方法栈引用的对象。

两次标记的过程:
上述可达性分析算法中,不可达的对象不会被立即回收,而是会经历两次标记过程。第一次是在发现其与GC Roots没有引用链的时候,对其进行第一次标记。 
如果对象重写了finalize()方法并且没有被调用,JVM会产生一个低优先级的线程去执行它。只要在finalize()中与GC Roots引用链上任意一个对象产生关联即可被移出“回收集合”。否则被回收。 
注:任意一个对象的finalize()方法只会被系统自动调用一次。可达性分析过程需要暂停用户线程。


三、垃圾回收算法

1. 标记-清除算法(Mark-Sweep)
1)首先标记需要被回收的对象(标记过程就是上述两次标记的过程); 
2)标记完成后对其统一回收。



JVM(三)—垃圾回收算法及垃圾收集器_老年代_02


缺点:
1)容易产生大量空间碎片。标记清除之后会产生大量的不连续的内存,导致分配一个大内存的对象时,无法找到足够的连续的内存(即使总的内存够)从而触发GC。 
2)标记和清除的效率低。

2. 标记-整理算法(Mark-Compact)
1)首先标记需要被回收的对象; 
2)将存活的对象向一端移动,清除端外内存。 
这种算法也叫做标记压缩算法。适合老年代的垃圾回收,因为老年代对象存活率高。



JVM(三)—垃圾回收算法及垃圾收集器_老年代_03

3. 复制算法(copying)
1)它将可用内存按容量分为大小相等的两块。 
2)当其中一块内存用完时,就将存活的对象复制到另外一块上,然后清理掉已使用的内存。



JVM(三)—垃圾回收算法及垃圾收集器_老年代_04

HotSpot虚拟机的做法:

  • 现在不需要按照1:1分配内存空间。而是将内存分为一块较大的Eden区和两块较小的Survivor区S0,S1(比例默认为8:1:1)。
  • 每次使用Eden和其中一块较小的内存S0,发生GC时将这两块存活的对象移动到另外一块Survivor区S1,再清理掉这两块内存E den+S0。
  • 这样只浪费了S1这块的内存,即只浪费了10%的内存。 
    注:当Eden+S0区域内存不够发生GC时,将存活对象移动到S1区时需要空间担保。因为Eden+S0存活的对象有可能大于S1区域,这时候需要放到老年代。

优点:
内存分配时,不用考虑内存碎片问题,直接使用指针碰撞的方法来移动堆顶指针按顺序分配内存即可;实现简单高效。 
缺点: 
1)将内存缩小为原来的一半,太浪费; 
2)存活对象多时,需要大量的复制,效率低; 
3)若内存中对象都存活,就需要额外的空间担保。

4. 分代收集算法
新生代:由于存活对象少,选用复制算法; 
老年代:存活对象多,没有额外的空间担保,所以选用标记-清除或标记-整理算法来回收。


四、垃圾收集器

JDK1.7后HotSpot虚拟机主要提供了如下的收集器:



JVM(三)—垃圾回收算法及垃圾收集器_用户线程_05


新生代收集器:

4.1 Serial 收集器
单线程垃圾收集器,使用复制算法。最基本也是最悠久的收集器,使用一个CPU或一条收集线程去进行垃圾收集,回收垃圾时必须停止其他工作线程,直到他回收结束,即所谓的”Stop the world”。 
工作示意图如下:



JVM(三)—垃圾回收算法及垃圾收集器_用户线程_06

缺点:在用户不可见的情况下将用户正常的线程全部停掉,造成卡顿,用户体验差。 
优点:运行在Client模式下默认新生代收集器。在单CPU的环境下,Serial收集器由于没有线程交互的开销,专心做垃圾收集可以获得最高的单线程收集效率,简单高效。

4.2 ParNew收集器
多线程垃圾收集器,使用复制算法,运行在Server模式下首选的新生代收集器。 
是上述 Serial 收集器的多线程版本,除使用多线程进行垃圾回收外,在收集算法,Stop The World、对象分配规则及回收策略都与 Serial 收集器完全一样。 
工作示意图如下:



JVM(三)—垃圾回收算法及垃圾收集器_垃圾收集_07


在多CPU情况下,只有他能与CMS收集器配合工作。

4.3 Parallel Scavenge收集器
并行的多线程垃圾收集器,也叫做吞吐量优先的垃圾收集器。使用复制算法。

  • 并行是指多条垃圾收集器并行工作,此时用户线程仍然处于等待状态;
  • 并发是指用户线程与垃圾线程同时执行,但是可能是交替执行的。

可控制的吞吐量。

  • 吞吐量=(CPU运行用户代码的时间)/(CPU运行用户代码的时间 + 垃圾收集时间);
  • 吞吐量越大,垃圾收集时间越少,运行在用户代码上的时间就越长,可以高效利用CPU,尽快完成任务提升用户体验。

可通过两个参数来设置:

  • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio:直接设置吞吐量大小

但是不要以为将新生代调小,就会收集的块。GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的,新生代调小,虽然收集时间快,但是导致收集更加频繁。


老年代收集器:

4.4 Serial Old 收集器
是Serial收集器老年代版本,使用单线程和标记-整理算法。工作流程同新生代Serial收集器。 
用途:

  • 与Parallel Scavenge收集器配合使用;
  • 在CMS并发收集失败时,当做其替补使用。

4.5 Parallel Old 收集器
Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾收集。 
工作流程如下:



JVM(三)—垃圾回收算法及垃圾收集器_垃圾收集_08

JVM(三)—垃圾回收算法及垃圾收集器_垃圾收集_09

在注重吞吐量和CPU资源敏感的场合,可以考虑Parallel Scavenge + Parallel Old(吞吐量+并行)的组合。

4.6 CMS 收集器
CMS:Concurrent Mark Sweep,基于标记-清除算法来降低停顿时间的收集器。 
整个工作过程分为4个步骤:

  • 初始标记
  • 并发标记
  • 重写标记
  • 并发清除

工作流程如下:



JVM(三)—垃圾回收算法及垃圾收集器_用户线程_10

1)初始标记仅仅是标记GC Roots能直接关联到的对象,速度很快; 
2)并发标记就是进行GC Roots Tracing(可达性分析)的过程,时间较长,但由于是并发操作,所以不需要暂停用户线程; 
3)重新标记是修正并发标记阶段由于用户线程工作导致标记变动出错的记录,时间远比并发标记短; 
4)初始标记、重新标记都需要Stop The World。

优点:并发执行垃圾收集动作。 
由于执行时间最长的并发标记和并发清除都是和用户线程一起并发执行的,所以总体上可看做是与用户线程并发执行,这也是其主要优点。

缺点:

  • 对CPU资源敏感。在并发阶段,虽然是与用户线程并发执行的,但是也因此占用了一部分的CPU资源,导致应用程序变慢,总吞吐量下降;
  • 无法处理浮动垃圾,可能产生“Concurrent Mode Failure”失败导致一次Full GC的产生。 
    浮动垃圾:在并发清除阶段,由于是与用户同时运行,由于这部分的用户线程仍然在执行,会产生垃圾,这部分垃圾即为浮动垃圾。 
    CMS只能在下一次GC时处理这部分的垃圾。 
    Concurrent Mode Failure:垃圾收集阶段用户线程还在执行,所以要预留足够的内存空间给用户线程使用。当这部分空间不足时,就会产生” Concurrent Mode Failure”错误,这时候会启用Serial Old收集器,导致卡顿严重。
  • 由于采用的是标记-清除算法,会产生大量空间碎片,导致空间内存不连续而易提前触发下一次Full GC.


G1收集器

G1:Garbage-First。最新的NB的前沿成果。 
G1收集器将整个Java堆分成多个大小相等的区域(Region)。虽然保留新生代、老年代的概念,但是他们之间不再是物理隔离的,新生代、老年代都是一部分Region(可以不连续)的集合。



JVM(三)—垃圾回收算法及垃圾收集器_垃圾收集_11


如图,绿代表新生代内存,蓝代表老年代。

G1如何进行垃圾回收?

  • 使用Region划分内存空间(而不是分年轻代老年代);
  • 根据垃圾堆优先级回收垃圾;

即:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region(即Garbage-First)。

与其他GC收集器相比有如下特点:

  • 并行与并发:充分利用多CPU、多核缩短Stop-The-World的停顿时间(并行),通过并发的方式在执行GC动作时依然可以让Java程序继续执行;
  • 分代收集:虽然保存了分代的概念,但是G1可以不需要其他收集器就可以独立管理整个堆;
  • 空间整合:G1从整体来看用的标记-整理算法;局部来看用的复制算法。所以不会产生内存空间碎片;
  • 可预测的停顿:G1与CMS都关注降低停顿时间。但是G1除了追求低停顿以外,还能建立可预测的停顿时间模型,即能指定在一个长度M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

G1收集器工作流程:

  • 初始标记;
  • 并发标记;
  • 最终标记;
  • 筛选回收。



JVM(三)—垃圾回收算法及垃圾收集器_用户线程_12

1)初始标记仅仅是标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行的时候,能在正确可用的Region中创建对象,这个阶段需要暂停线程,时间较短; 
2)并发标记就是进行GC Roots Tracing(可达性分析)的过程,找出存活的对象,时间较长,但由于是并发操作,所以不需要暂停用户线程; 
3)最终标记阶段则是修正在并发标记阶段因为用户程序的并发执行而导致标记产生变动的那一部分记录,这部分记录被保存在Remembered Set Logs中,最终标记阶段再把Logs中的记录合并到Remembered Set中,这个阶段是并行执行的,仍然需要暂停用户线程; 
4)最后在筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划,也是可以做到并发执行。

下面参考网上的一系列图形象来描绘G1收集过程: 
1)标记阶段; 
2)并发标记:在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。



JVM(三)—垃圾回收算法及垃圾收集器_JVM垃圾回收_13


3)最终标记; 

4)筛选回收:复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。


JVM(三)—垃圾回收算法及垃圾收集器_JVM垃圾回收_14


五、GC日志


33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]  
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

代表的意思:

  • 33.125:GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数;
  • [GC 和 [Full GC :说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的;
  • [DefNew 和 [Tenured 和 [Perm:表示GC发生的区域;
  • 3324K->152K(3712K),:GC前该内存区域已使用容量-> GC后该内存区域已使用容量 (该内存区域总容量)”;
  • 3324K->152K(11904K):GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)
  • 0.0025925 secs:该内存区域GC所占用的时间;