JVM垃圾回收器啥工作?

南风 2020年10月22日 87次浏览

一、如何确定JVM垃圾?

1、引用计数法

引用计数法的逻辑是:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。

弊端:容易产生”孤岛“,即如果AB相互持有引用,导致永远不能被回收

2、可达性分析

通过GC Root的对象,开始向下寻找,看某个对象是否可达。能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用,本地方法栈的变量等。通过GC Root为节点,向下查找,当一个对象没有被GC Root的引用链相连时,将被标记为可回收对象。假设撒个对象互相持有引用,但是到GC Root是不可达的,所以将会被判断为可回收对象。

二、JVM垃圾回收算法有几种?

1、标记-清除(Mark-Sweep)

标记:标记所有可回收的对象内存块
清除:清除已经标记的内存块
缺点:耗时,产生很多不连续的内存碎片,当需要分配较大对象内存时,找不到连续内存,则需要提前执行一次垃圾回收,浪费资源。

2、标记-清除(Mark-Sweep)

将内存块一分为二,一部分使用,一部分空闲,当垃圾回收时,将非回收对象复制到空闲的一部分,另一部分就是待回收的对象,直接回收。
缺点:不能充分利用资源

3、标记-整理(Mark-Compact)

标记处所有待回收的对象,将存活的对象放置一边标记边界,清除边界之外的内存块。

4、分代收集算法

  • Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
  • Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

三、垃圾回收器集锦

 上回说到运行时数据区堆空间分为老年代年轻代。刚创建的对象存放在年轻代,而老年代中存放生命周期长久的实例对象。年轻代中又被分为Eden区和两个Survivor区(From Space和To Space)。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次GC仍然存活的,就会被转移到老年代。

0、Minor GC 和 Full GC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。具体原理见上一篇文章。

  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

JVM垃圾回收器

1、新生代回收器

Serial收集器

 Serial(串行)单线程收集器,执行时其它线程暂停,使用的回收算法是复制算法,是JVM的-client模式下的默认收集器。
 Serial收集器是串行收集器,因为少了多线程切换的开销,相较于其他收集器能够更加专注于垃圾回收,在单核场景下效率极高,并且在回收较小内存(几十或者一两百兆)时,停顿时间是毫秒级的。推荐使用场景:年轻代占用几十兆到一两百兆的桌面应用。

ParNew收集器

 新生代的收集器,可以把它理解为Serial收集器的多线程版本,和Serial同属比较霸道的回收器,在工作的时候,非回收器线程都停止。ParNew在多核服务器运行下效率比Serial高,但是单核环境下,由于有多线程开销,比Serial效率低。ParNew是Server模式下的虚拟机中首选的新生代收集器。

Parallel Scavenge收集器

 Parallel Scavenge收集器是“吞吐量优先”的收集器,相比于其它收集器,它更关注JVM的吞吐量,停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可用最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

-XX:MaxGCPauseMillis         停顿时间设置
-XX:GCTimeRatio              设置吞吐量大小
-XX:+UseAdaptiveSizePolicy   自动分配内存设置

2、老年代回收器

Serial Old收集器

Serial Old收集器是单线程收集器,执行时暂停所有其它线程,但是与新生代Serial回收器不同的是使用了标记-清除算法回收算法。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,回收算法使用标记-清除算法,吞吐量优先。

CMS收集器

 CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。采用的是"标记-清除算法",整个过程分为4步:

  • 初始标记 CMS initial mark 标记GC Roots能关联到的对象 Stop The World--->速度很快
  • 并发标记 CMS concurrent mark 进行GC Roots Tracing
  • 重新标记 CMS remark 修改并发标记因用户程序变动的内容 Stop TheWorld
  • 并发清除 CMS concurrent sweep

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

优点:并发收集、低停顿
缺点:采用标记-清除算法会产生大量空间碎片,并发阶段会降低吞吐量

3、新时代垃圾回收器---G!收集器

G1(Garbage-First)是目前JVM最前沿的垃圾收集器,它作用于新生代和老年代,与其它收集器相比,它有以下优点:

  • 并行和并发: G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
  • “标记-整理”算法:空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
1、可处理整个堆区

G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。

2、可预测的时间模型

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

3、避免全堆扫描——Remembered Set

G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。

为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作。

检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

四、GoLang与Java的垃圾回收机制有何区别?

Java与GoLang不同的回收算法:三色标记算法
三色标记算法是对标记阶段的改进,原理如下:

  • 1、起初所有对象都是白色。
  • 2、从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
  • 3、从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
  • 4、重复3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
    三色标记算法

(全文完)