目录

一、判断对象的两种算法

【1】引用计数算法

【2】可达性分析算法

二、Java 中的四种引用类型

【1】强引用(Strong Reference)

【2】软引用(Soft Reference)

【3】弱引用(Weak Reference)

【4】虚引用(Phantom Reference)

三、垃圾收集

【1】分代收集理论

【2】垃圾收集算法

1.标记-清除算法 ( Mark-Sweep )

2.复制算法 ( Copying )

3.标记-整理算法 ( Mark-Compact )

【3】综上所述

四、垃圾收集器

【1】Serial 收集器(新生代)

【2】Serial Old 收集器(老年代)

【3】ParNew 收集器(新生代)

【4】Parallel Scavenge 收集器(新生代)

【5】Parallel Old 收集器(老年代)

【6】CMS 收集器(老年代)

【7】G1 收集器(老年代)

1.什么是G1收集器

2.G1 垃圾收集器工作流程

3.G1 垃圾收集器的特点


一、判断对象的两种算法

【1】引用计数算法

引用计数算法(Reference Counting)基本思路:

  • 在对象内部,添加并维护一个引用计数器;

  • 每当有一个地方引用它的时候,计数器就加+1;

  • 每当有一个引用失效的时候,计数器就减-1;

  • 当计数器的值为0的时候,那么该对象就是可被GC回收的垃圾对象

引用计数算法存在的问题:对象循环引用
        a 对象引用了 b 对象,b 对象也引用了 a 对象,a、b 对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是 0,所以引用计数算法就无法回收它们。

【2】可达性分析算法

        可达性分析算法(Reachability Analysis)基本思路:通过定义了一系列称为“GC Roots”的根对象作为起始节点集,从GC Roots  开始,根据引用关系往下进行搜索,查找的路径我们把它称为 "引用链" 。当一个对象到  GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象。
        可达性分析算法也是JVM 默认使用的寻找垃圾算法。


二、Java 中的四种引用类型

【1】强引用(Strong Reference)

        强引用是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足时,JVM 宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

Object strongReference = new Object();

如果强引用对象不使用时,需要弱化从而使GC能够回收。

        弱化方式1:显式地设置strongReference对象为null,则gc认为该对象不存在引用,这时就可以回收这个对象。但是,具体什么时候收集这要取决于GC算法。例如,strongReference是全局变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

        弱化方式2:让对象超出作用域范围。

【2】软引用(Soft Reference)

        如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。所以,软引用可用来实现内存敏感的高速缓存。

// 强引用
String strongReference = new String("abc");

// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);

// 访问软引用
softReference.get();

当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收

【3】弱引用(Weak Reference)

        只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

        创建弱引用,使用WeakReference:

String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;

System.gc();
		
// 一旦发生GC,弱引用一定会被回收
System.out.println(weakReference.get());

【4】虚引用(Phantom Reference)

        虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收。
        虚引用,主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知。

public class PhantomReference<T> extends Reference<T> {
    public T get() {
        return null;
    }
}

三、垃圾收集

【1】分代收集理论

        目前主流JVM虚拟机中的垃圾收集器,都遵循分代收集理论

  • 弱分代:绝大多数对象都是朝生夕灭
  • 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡

垃圾收集类型划分

  • 部分收集(Partial GC):没有完整收集整个Java堆的垃圾收集,其中又分为:
    • 新生代收集(Minor GC / Young GC)
    • 老年代收集(Major GC / Old GC)
    • 混合收集(Mixed GC):收集整个新生代和部分老年代的垃圾收集。
  • 整堆收集(Full GC):收集整个Java堆的垃圾收集。


【2】垃圾收集算法

1.标记-清除算法 ( Mark-Sweep )

        “标记-清除”算法实现思路

        该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收

掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。

        “标记-清除”算法会带来两个明显的问题

  1. 执行效率不稳定问题:如果执行垃圾收集的区域,大部分对象是需要被回收的,则需要大量的标记和清除动作,导致效率变低。

  2. 内存空间碎片化问题:标记清除后会产生大量不连续的碎片,空间碎片太多,会导致分配较大对象时,无法找到足够的连续空间,从而会触发新的垃圾收集动作。

2.复制算法 ( Copying )

        “复制”算法实现思路
        该算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把已使用的空间一次清理掉。

        “复制”算法特点
        如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销。
        所以,复制算法适合仅需要复制少数存活对象的场景,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

        “复制”算法的问题

  1. 对象存活率较高,需要进行较多的内存间复制,效率降低

  2. 浪费过多的内存,使现有的可用空间变为原先的一半

3.标记-整理算法 ( Mark-Compact )

        “标记-整理”算法实现思路:

        标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向内存空间一端移动, 然后直接清理边界以外的内存,这样清理的机制,不会像标记-整理那样留下大量的内存碎片。

【3】综上所述

        当前虚拟机的垃圾收集都基于分代收集思想,根据对象存活周期的不同,将内存分为几个不同的区域,在不同的区域使用不同的垃圾收集算法

        例如: Heap 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
        在新生代中,每次收集都会有大量垃圾对象被回收,所以可以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
        在老年代中,对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以选择“标记-清除”或“标记-整理”算法进行垃圾收集。


四、垃圾收集器

【1】Serial 收集器(新生代)

        Serial(串行)收集器是最基本、历史最悠久的垃圾收集器,采用“标记-复制”算法负责新生代的垃圾收集。它是Hotspot虚拟机运行在客户端模式下的默认新生代收集器。

        它是一个单线程收集器。它会使用一条垃圾收集线程去完成垃圾收集工作,并且它在进行垃圾收集工作的时候,必须暂停其他所有的工作线程( "Stop The World" ),直到收集结束。

        这样的设计,带来的好处就是:简单高效。对于内存资源受限制的环境,它是所有收集器中额外内存消耗最小的收集器。


【2】Serial Old 收集器(老年代)

        Serial Old收集器同样是一个单线程收集器,采用“标记-整理”算法负责老年代的垃圾收集,主要用于客户端模式下的HotSpot虚拟机使用。


【3】ParNew 收集器(新生代)

        ParNew 收集器是一个多线程的垃圾收集器。它是运行在 Server模式下的虚拟机的首要选择,可以与 Serial Old ,CMS 垃圾收集器一起搭配工作,采用“标记-复制”算法。


【4】Parallel Scavenge 收集器(新生代)

        Parallel Scavenge 收集器是也是一款新生代收集器,使用“标记-复制”算法实现的多线程收集器.

        Parallel Scavenge 收集器预其它收集器的目标不同,CMS等其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间。但是Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值


【5】Parallel Old 收集器(老年代)

        Parallel Old 收集器是一个多线程的垃圾收集器,使用“标记-整理”算法,是Parallel Scavenge收集器的老年代版本。
在注重吞吐量或者处理器资源较为稀缺的应用场景,都可以优先考虑 Parallel Scavenge 收集器 + Parallel Old 收集器这个收集器组合。   


【6】CMS 收集器(老年代)

        CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

工作流程
        整个过程包括四个步骤:

  1. 初始标记(CMS initial mark):标记一下GC Roots 能直接关联到的对象,速度很快;

  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

  3. 重新标记(CMS remark):重新标记阶段,是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间长,远远比并发标记阶段时间短

  4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

优点和缺点
主要优点:并发收集、低停顿。
主要缺点

  • 影响用户线程的执行效率:并发标记和并发清除时,是和用户线程一起运行的,收集过程中肯定占用了用户程序的CPU资源。CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU数量在4个以上时,垃圾回收线程占用不少于25%的CPU资源,势必影响用户线程的执行效率。

  • 无法处理浮动垃圾:在并发清除阶段,用户线程并没有停止,所以还会继续产生新的垃圾,只能等待下一次收集时才能进行回收,这部分垃圾被称为“浮动垃圾”。

  • 产生大量空间碎片:因为CMS收集器是基于“标记-清除”算法实现的,所以在进行大量的垃圾回收时,会产生很多不连续的内存空间。这是使用“标记-清除”算法都会有的缺点。


【7】G1 收集器(老年代)

1.什么是G1收集器

        G1 ( Garbage-First ) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器、大容量内存的机器。它不再严格按照分代思想进行垃圾回收。
        G1 采用局部性收集的思想,对于堆空间的划分,采用Region为单位的内存划分方式:
        G1 垃圾回收器把堆划分成若干个大小相同的独立区域(Region)(按照JVM的实现,Region的数量不会超过2048个):每个 Region 都会代表某一种角色,H、S、E、O。E代表Eden区,S代表 Survivor 区,H代表的是 Humongous(G1用来分配大对象的区域,对于 Humongous 也分配不下的超大对象,会分配在连续的 N 个 Humongous中),剩余的深蓝色代表的是 Old 区,灰色的代表的是空闲的 region。
        这种思想上的转变和设计,使得G1可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的 Mixed GC模式,即混合GC模式

2.G1 垃圾收集器工作流程

  • 初始标记(Initial Marking):这个阶段仅仅只是标记GC Roots能直接关联到的对象,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。

  • 并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。

  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后遗留记录。

  • 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成【回收集】,然后把回收的那一部分Region中的存活对象==>复制==>到空的Region中,最后对那些Region进行清空。

3.G1 垃圾收集器的特点

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。

  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。

  • 空间整合:G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“标记-复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。


        感谢你花时间读到这里~ 如果你觉得这篇内容对你有帮助,不妨点个赞让更多人看到;如果有任何想法、疑问,或者想分享你的相关经历,欢迎在评论区留言交流,你的每一条互动对我来说都很珍贵~ 我们下次再见啦!😊😊

更多推荐