JVM中垃圾收集器和垃圾收集算法

概述

	虚拟机在运行过程中会产生很多对象,但如果光产生对象而不清除对象,虚拟机会变得无比臃肿,垃圾
收集则是为了将一些无用的对象回收,释放内存空间供新对象使用。对于线程私有的程序计数器,本地方法
栈和虚拟机栈随线程消失而消失则不用考虑垃圾回收,但堆和方法区则需要考虑,其中堆是发生垃圾收集最
频繁的区域。

垃圾收集需要确定的三件事

  • 哪些内存需要回收?
    • 已死对象需要回收。
  • 什么时候回收?
    • 由具体垃圾收集器确定。
  • 如何回收?
    • 由垃圾收集器使用的算法确定 。

如何判断对象已死

  • 引用计数器

    • 在对象中添加一个引用计数器,有对象引用时计数器加一,取消引用时计数器减一,当计数器为0时,对象已死,优点实现简单,缺点是若两个对象循环引用但无其他对象指向他们,则他们应该被回收但是因为计数器不为0,所以无法回收。如下图。
      在这里插入图片描述
  • 可达性分析

    • 通过一系列GC Roots节点判断是否可到达某个节点,如果不可达则判定此对象已死。如下图所示,灰色则是不可达节点,蓝色则可达。

    在这里插入图片描述

    • 可作为GC Roots节点的有以下几种,而且还可根据所用收集器和收集区域不同,加入其他对象。
      • 虚拟机栈中引用的对象,例如某个方法中的局部变量。
      • 方法区中的类静态属性引用的对象,例如某个类中的静态引用类型变量。
      • 方法区中常量引用的对象,例如字符串常量池。
      • 在本地方法栈中引用的对象,例如Java中的Native方法中使用的对象。
      • Java虚拟机内部引用,例如类加载器,异常对象,基本类型Class对象。
      • 被同步锁持有的对象,例如synchronized关键字,如果修饰非静态方法则此时对象为类的实例对象,若修饰静态方法则是类对象,若修饰某个指定对象则是该对象。
      • ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

垃圾收集算法

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行
设计,基于两个分代假说:

  • 弱分代假说:大部分对象都是朝生夕灭。
  • 强分代假说:存活时间越久的对象越难被消除。

所以大部分收集器将堆划分为不同区域,新生代和老年代。
在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域。包括部分收集(老年代收集(major gc)和新生代收集(minor gc)),还有整堆收集(full gc)。

标记-清除算法

该算法包括两部分,第一部分为标记,第二部分为清除。清除时清除标记中的所有对象。如下图,例如标记灰色是待清除对象。空白为未使用对象,有字代表存活对象。
在这里插入图片描述
在这里插入图片描述
优点:实现简单。
缺点:清除后产生内存碎片多,执行时间不稳定,若一次清除对象过多,则进行大量的标记清除操作。
标记时可以标记待清除对象,清除时清除标记对象,也可以标记存活对象,清除时清除非标记对象。

标记-复制算法

将内存分为两部分,不一定分为50:50,此处是方便画图,每次将存活对象放置另一边,则一边对象全部抹除,优点是实现简单,无内存碎片,缺点是可用空间缩少。
在这里插入图片描述
在这里插入图片描述
HotSpot虚拟机使用该算法收集新生代,并把新生代分为Eden和两个Survivor区,比例是8:1:1,分配空间时只使用一个Eden区和一个Survivor,当发生垃圾清理时把存活对象复制到一个未使用的Survivor区中,并清除Eden区和另一个Survivor区,如存活对象过多Survivor区放不下则需要依赖老年代进行分配担保,将存活对象分配到老年代区中。若存活对象老年代也分配不下则发生一次full gc。

标记-整理算法

该算法大体上和标记-清除算法类似,不过是在清除过后进行一次对象整理,消除内存碎片。移动对象是一件优缺点共存的事情,若整理对象时,需要暂停全部用户线程,若不整理对象当碎片空间足够多时想要分配空间则需要使用分配链表指向各种内存碎片才能分配,所以是否整理根据具体情况判断。
算法运行如下图。
在这里插入图片描述
在这里插入图片描述

垃圾收集器

新生代收集器

Serial收集器

在收集过程中,需要暂停其他用户线程,直到收集结束。使用标记-整理算法。是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。
优点:

  • 简单高效。使用一个线程实现垃圾回收。
  • 消耗内存小。

工作过程
在这里插入图片描述

ParNew收集器

是Serial收集器的多线程版本,其他具体实现和Serial收集器类似,用于新生代并行收集。
在这里插入图片描述
由于大体上和Serial收集器类似,所以当在单核心处理器中,性能有可能不如Serial收集器,但使用处理器核心多时,性能大于Serial收集器。默认开启收集线程数为处理器核心数量。

Parallel Scavenge收集器

与上两个收集器不同,该收集器强调的是高吞吐量,而不在意收集时的停顿时间,此收集器用于并行收集新生代。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即运行用户代码时间/运行用户代码时间+运行垃圾收集时间。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间
的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

  • 停顿时间需要设计一个良好的值,是一个大于0的毫秒数,如果设置此数过小,则每次回收时间确实减少,但是回收次数会增加。
  • 吞吐量是一个(0,100)区间的值,譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。

老年代收集器

Serial Old收集器

此收集器是Serial的老年版,使用标记-整理算法实现,也是一个单线程垃圾收集器,并且收集时需要暂停所有用户线程。
在这里插入图片描述

Parallel Old收集器

此收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实
现。

Concurrent Mark Sweep收集器

CMS收集器是一种以获取最短延迟为目的的垃圾收集器。支持并发收集,基于标记-清除算法实现。
工作过程:

  • 初始标记
    此处仍然需要停止所有用户线程,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记
    此处可以和用户线程共同工作,此处从GC Roots开始实行可达性分析,对于不可达对象进行标记。
  • 重新标记
    此处需要停止所有用户线程,标记在并发标记时对象引用更改的部分,如并发标记时标记为不可达,同时有用户线程又使用了该对象,此时需要把该对象取消标记。
  • 并发清除
    此处可以和用户线程同时工作,清除标记对象。
    在这里插入图片描述
    缺点:
  • 在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计
    算能力)而导致应用程序变慢,降低总吞吐量。
  • 由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Concurrent Mode
    Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
    浮动垃圾:由于并发标记和并发清除与用户线程同时工作,此时用户线程产生的垃圾对象被称为浮动垃圾。
    Stop The World : 停止所有用户线程。
    Concurrent Mode Failure:JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。只有当老年代空间占有92时才会使用CMS收集器收集,若此时分配对象老年代无法承受,则会发生并发失败,此时会暂停用户线程,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
    -XX:CMSInitiatingOccupancyFraction可以设置CMS收集器启动阈值。
  • 因为收集器使用标记-清除算法,所以在运行过程中会产生大量内存碎片。将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,虚拟机提供了两个参数(此参数从JDK 9开始废弃)。
    • -XX:+UseCMS-CompactAtFullCollection开关参数
      用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,但是会暂停用户线程,因为移动存活对象。
    • -XX:CMSFullGCsBeforeCompaction
      这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。
增量更新

解决并发扫描时的对象消失问题,当在并发标记过程中,用户线程插入了一条存活对象到标记对象的直接引用,则标记对象不该消失,每次有这种插入时会将存活对象记录下来,会在重新标记时将记录的存活对象重新作为GC Roots节点扫描一次。

Garbage First收集器

它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。内存布局如下图。将内存区域分为大小相等的Region,每个区域是新生代还是老年代是不固定的。如果一个对象超过该区域的一半则分配到Humongous区中。
每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。
在这里插入图片描述
G1每次回收的选择则是根据每个Region中垃圾的价值,维护一个优先级列表,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

工作过程大致分为4部分:

  • 初始标记
    仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
  • 并发标记
    从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  • 最终标记
    对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收
    负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
    在这里插入图片描述
    G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的。
记忆集(Remember Set)

将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?
使用记忆集来维护每个Region和其他Region的引用,避免全堆作为GC Roots扫描,每个Region都有自己独立的记忆集,在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)

卡表(Card Table)

卡表最简单的形式可以只是一个字节数组,字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以2的N次幂的字节数,卡页中包含不只有一个对象,如果该卡页中对象存在跨代引用,则对应卡页内容为1,即卡页变脏。。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

原始快照(SATB)

G1收集器则是通过原始快照(SATB)算法来实现的解决并发扫描时对象消失问题。此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。

ZGC收集器

希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

 染色指针->是一种直接将少量额外的信息存储在指针上的技术。

内存分配和回收策略

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起
一次Minor GC(新生代回收)。

大对象直接进入老年代

直接分配在老年代,避免如果此对象分配在新生代时,频繁发生gc此对象不被回收,会来回在Eden区和Survivor区复制,且如果分配在新生代会导致新生代有空间提前触发垃圾收集。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配

长期存活对象进入老年代

发生一次Minor GC后存活对象的年龄会加一,默认年龄15则进入老年代。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
虚拟机并不一定要求年龄达到设置值时进入老年代,,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

深入理解Java虚拟机(周志明)阅读笔记,理解有限,如有错误还望指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值