JVM的垃圾回收

JVM的垃圾回收

    此文介绍Java的基本垃圾回收机制。

内存数据的分代和分区

    GC主要回收的是堆区,在堆中是有对象分代的,一个对象每“逃”过一次回收,对象代数便+1,新生对象被称作新生代(如果是占据内存较大的对象直接定义为老年代),当代数一定时对象将由新生代变为老年代。同时还有永久代,保存了一些静态变量,当然这只是在1.7之前。总之,内存回收只发生在新生代和老年代之间。除了分代,内存也有分区:

    如图,是内存区域分配,其中Eden存储了新建的小对象,当回收时,将Eden中存活的对象转移到To Survivor区中,将From Survivor中的代数高(一般是15)的存活对象转移到老年代中,代数没达到阈值的存活对象转移到To Survivor中。然后清理掉Eden和From Survivor中的对象,对To Survivor与From Survivor互换身份。

Minor GC与Full GC

     如上文第二段所述是Minor GC的过程,Minor GC只发生在新生代中,当Eden区内存不足会触发Minor GC,是件很频繁的事情。Full GC则是回收老年代,当老年代内存不足时或当触发。

垃圾收集算法

停止-复制

    将存活对象直接转移到新的空间,然后删除旧空间的对象数据。这种算法一般用在新生代的GC中。

标记-清除

    标记存活对象,然后清除掉没被标记的对象。这种算法使用的不多,因为内存不连续,产生很多碎片。

标记-整理

    同样标记存活对象,然后将存活对象整理到内存连续的空间内,再清除剩下的空间边界外的对象。这种算法一般用在老年代的GC中。

GC Root与可达性分析

    GC回收的基本方式采用GC Root的可达性分析法,即从GC Root对象为起点,通过对象的引用链条进行对象可达性分析,通过GC Root可达的对象就就不会被回收。

    GC Root包括:

CMS与G1回收方法

stop-the-world

    垃圾回收器是stop-the-world的,因为每当GC执行时,java所有线程都会停止工作,直到gc线程执行完毕,所以垃圾回收的核心其实是尽量改善GC的时间和次数,这也是GC调优的目的。jdk 8 中的GC主要采用两种方案:CMS和G1。

CMS

    全称为ConcurrentMarkSweep,CMS只发生在老年代中,所以不能独立存在,基于标记-清除实现,执行步骤如下:

  1. 初始标记 (发生stop-the-world,CPU停顿, 很短),初始标记仅标记一下GC Roots能直接关联到的对象,速度很快;

  2. 并发标记 (收集垃圾跟用户线程一起执行) ,并发标记过程就是进行GC Roots Tracing的过程,即从GC Roots向下寻找可达的对象;

  3. 重新标记 (发生stop-the-world,CPU停顿,比初始标记稍微长,远比并发标记短),修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但比并发标记时间短得多;

  4. 执行并发清理。

    整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    CMS能很大程度减小cpu的停顿时间,但是由于并发标记和清理过程依旧占用部分CPU,会导致程序在GC期间卡顿少,然而吞吐量小,同时因为没有进行内存整理,容易产生很多内存碎片,尤其是在老年代中,碎片化的数据不利于分配较大对象的内存,相应地CMS开放了一个可回收后对内存进行整理的开关,但是开启后会导致回收时间变长。

G1

    G1可以回收新生代对象也可以回收老年代对象,被称作是面向服务端的垃圾收集器。

    前文所指的分代回收多是赋值算法和CMS的回收机制,G1收集器弱化了这个概念,将物理内存按逻辑分为了多个region,新生代与老年代物理上在一处空间。执行步骤如下:

  1. 初始标记(STW):同CMS的第一步;

  2. 根区域扫描(root region scan):根分区扫描,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(Root Region Scanning)。

  3. 并发标记:标记线程和用户线程并发执行,标记出根对象的可达路径。从初始标记开始找出所有存活对象(耗时长)。

  4. 重新标记(STW):同CMS的重新标记。

  5. 筛选回收(清除):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

   G1在老年代中采用的是标记-整理算法,当发生GC时,

一些古老的“垃圾”收集器-新生代

    下面介绍一下古老的垃圾收集器,在Java 8以后可能很少见到他们的身影,这些收集器要么有某些缺陷,要么会有效率问题。

Serial

    也称Copy或DefNew。新生代垃圾收集的最初版本,单线程单个cpu执行,专注于回收内存因而高效,但是全程STW。

ParNew

    算是前期用的最多的垃圾收集器,在Java 7中有一个很经典的组合回收方式:CMS+ParNew,在G1出现以前,一般是ParNew担起了他的职责。ParNew其实就是并行版的Serial,但是在单个核心环境下表现会劣于Serial。

Parallel Scavenge

     也称PS Scavenge或PSYoungGen。使用停止-复制算法,是ParNew的变种,相当于可以设定每次最多回收时间和最大吞吐量的ParNew,回收时间更短通常意味着需要回收更加频繁,但有时可能会有更好的用户体验,需要根据实际情况选择。

一些古老的“垃圾”收集器-老年代

Serial Old

    也称MSC,Serial的老年代版本,采用标记-整理算法。

Parallel Old

    Parallel Scavenge的老年代版本,在Java1.6之后出现,采用标记-整理算法。

gc常见组合

    现有的垃圾回收如下图(原图来自《深入理解Java虚拟机:JVM高级特性与最佳实践》一书),可以任选一个年轻代和老年代垃圾回收构成gc组合

    最初的组合是Serial+Serial Old,随后随着需求产生了ParNew+Serial Old组合,在Java 1.6产生Parallel Old之后,Parallel Scavenge回收参数可调的优越性才得以体现出来。

    需要注意的是Parallel是吞吐量可控的垃圾回收,如果不结合Parallel Old使用将不能凸显它的优势。

参考资料:部分内容引自:G1 vs CMS详细对比 - tantexian的博客空间 - OSCHINA《深入理解Java虚拟机:JVM高级特性与最佳实践》


2019-09-20鱼鱼

{{blog.title}}

创建于 {{blog.createTimeStr}}   created  by  {{blog.author}} {{tag}}
最后修改于 {{blog.timelineStr}}
修改文档