JVM垃圾收集器

1.判断对象是否存活

1.1.引用计数算法

基本思路

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

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

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

当计数器的值为0的实收,那么该对象就是可被GC回收的垃圾对象
引用计数算法存在的问题:对象循环引用

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

1.2.可达性分析算法

在JVM中,可达性分析算法用于:

  1. 判断对象是否"存活"

  2. 确定垃圾回收的边界

  3. 支持不同强度的引用类型(强、软、弱、虚)

可达性分析算法基本思路:

通过定义了一系列成为"GC Roots"的跟对象作为其实节点集,从GC Roots开始,根据引用关系往下进行搜索,查找的路径我们把他称为"引用链"。当一个对象到GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象。

可达性分析算法也是JVM默认使用的寻找垃圾算法。

Object 6,Object7、Object 8彼此之前有引用关系,但是没有与"GC Roots"相连,那么就会被当做垃圾所回收。

1.3.java中的四种引用

1. 强引用 (Strong Reference)

  • 特点:最常见的引用类型,只要强引用存在,对象就永远不会被回收

  • 语法Object obj = new Object();

  • 回收条件 :当没有任何强引用指向对象时,需要弱化从而使GC能够回收

  • 使用场景:普通对象引用,需要长期保持的对象

java 复制代码
 String str1=new String("absc");

弱化方式1:直接将str1对象设置为null;

java 复制代码
str1=null;

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

2. 软引用 (Soft Reference)

  • 特点:内存敏感的高速缓存,内存不足时才会回收

  • 语法SoftReference<Object> softRef = new SoftReference<>(new Object());

  • 回收条件:当内存不足且只有软引用指向对象时

  • 使用场景:实现内存敏感的高速缓存(如图片缓存)

java 复制代码
import java.lang.ref.SoftReference;

public class Demo01 {
    public static void main(String[] args) {
        String str1=new String("absc");

        //软引用
        //str2=str1;
        SoftReference<String> str2=new SoftReference<String>(str1);

        //弱化强引用
        str1=null;

        //Notify GC
        System.gc();

        try{
            byte[] buff1=new byte[900000000];//内存充足
            byte[] buff2=new byte[900000000];//内存不足
            byte[] buff3=new byte[900000000];//内存不足
            byte[] buff4=new byte[900000000];//内存不足
            byte[] buff5=new byte[900000000];//内存不足
        }catch (Error e){
            e.printStackTrace();
        }

        //内存充足时,GC不会回收,可以正常访问
        //内存不足时,GC回收,str2.get()返回null
        System.out.println(str2.get());
    }
}

3. 弱引用 (Weak Reference)

  • 特点:比软引用更弱,只要发生GC就会被回收

  • 语法WeakReference<Object> weakRef = new WeakReference<>(new Object());

  • 回收条件:当发生GC且只有弱引用指向对象时

  • 使用场景:实现规范化映射(如WeakHashMap)、临时性对象存储

java 复制代码
import java.lang.ref.WeakReference;

public class Demo02 {
    public static void main(String[] args) {
        String str1=new String("absc");

        //弱引用
        WeakReference<String> str2=new WeakReference<String>(str1);
        str1=null;
        System.out.println(str2.get());
        System.gc();
        //无论内存是否充足,只要发生GC,就会被回收
        System.out.println(str2.get());
    }
}

4. 虚引用 (Phantom Reference)

  • 特点:最弱的引用,无法通过虚引用获取对象实例

  • 语法PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);

  • 回收条件:对象被标记为可回收状态时

  • 使用场景:用于跟踪对象被垃圾回收的状态(必须配合ReferenceQueue使用)

对比表格

引用类型 强度 回收时机 是否可获取对象 典型用途
强引用 最强 无强引用时 普通对象引用
软引用 次强 内存不足时 内存敏感缓存
弱引用 较弱 每次GC时 规范化映射
虚引用 最弱 回收前入队 回收跟踪

2.垃圾收集算法

2.1.分代收集理论

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

*弱分代:绝大多数对象都是朝生夕灭

*强分代:经历与多次垃圾收集过程的对象,越难以回收,难以消亡。

按照分代收集理论设计的"分代垃圾收集器",所采用的设计原则:收集器应该将java堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储。

2.1.1.分代存储

如果一个区域中大多数对象都是朝生夕灭(新生代),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,之关注如何保留少量的存活对象,而不是去编辑大量将要回收的对象,京能比较低代价回收到大量的空间。

如果一个区域中大多数对象都是难以回收(老年代),那么把它们集中放在一起,JVM虚拟机就可以使用较低的频率,来对这个区域进行回收。

2.1.2 分代收集

堆区按照分代存储的好处:

在java堆区划分成不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些区域,所以才有MinorGC,MajorGC,FullGC等垃圾收集类型划分。

在java堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-赋值,标记-清除,标记-整理算法等

2.2.垃圾收集算法

2.2.1.标记-清除算法

标记-清除算法实现思路:

该算法分为"标记"和"清除"阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。

问题:

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

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

1. 标记-清除法:贴标签扔垃圾 🏷️

就像这样:

  1. 贴标签:把你还要用的东西都贴上"保留"标签

  2. 扔垃圾:把所有没贴标签的东西都扔掉

优点 :简单直接
缺点:扔完后房间很乱,东西散落各处(内存碎片)

就像:妈妈来你房间,把你还要的东西贴上便签,然后把其他都扔了

2.2.2.复制算法

"复制"算法实现思路:

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

"复制"算法特点:

如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销。

所以,复制算法适合仅需要复制少数存活对象的场景,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效。

"复制"算法的问题:

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

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

2. 标记-复制法:搬家整理 🚚

就像这样:

  1. 把房间分成两半:A区和B区

  2. 平时只在A区放东西

  3. 打扫时:把A区还要的东西搬到B区

  4. 然后把A区整个清空

优点 :搬过去后B区很整齐
缺点:浪费空间,只能用到一半房间

就像:你有两个一模一样的抽屉,每次只用一个,满了就把要的东西搬到另一个抽屉

2.2.3.标记-整理算法

实现思路:标记过程仍然和与"标记-清除"算法意向,但是后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象象内存空间一段移动,然后直接清理边界以外的内存,这样清理的一直,不会像标签整理那样留下大量的内存碎片。

3. 标记-整理法:重新摆放 🪑

就像这样:

  1. 贴标签:标记还要的东西

  2. 挪位置:把所有还要的东西都推到一边,摆整齐

  3. 清空间:把空出来的地方一次性清理干净

优点 :房间整齐,空间都利用上了
缺点:挪动东西很费时间

就像:整理书柜,把所有书都拿到一边,然后重新整齐放回

2.3.综上所诉

🎯 简单总结

方法 好比 优点 缺点
标记-清除 贴便签扔垃圾 简单 房间变乱
标记-复制 搬家到新房间 很整齐 浪费空间
标记-整理 重新摆放物品 整齐又省空间 很费劲

📦 实际应用

电脑内存管理就是这样:

  • 小物件(新生代):常用"搬家法"(标记-复制),因为小东西搬起来快

  • 大物件(老年代):常用"重新摆放法"(标记-整理),因为大东西不常动

3.垃圾收集器

3.1.Serial收集器(新生代)

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

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

这样的设计,带来的好处就是:简单高效。对于内存资源受限制的环境,它是所有收集器中额外内存消耗最小的收集器。适合单核处理器或处理器核心数较少的环境,每次收集几十 MB 甚至一两百 MB 的新生代内存,垃圾收集的停顿时间完全可以控制在十几毫秒或几十毫秒,最多一百多毫秒。

3.2.Serial Old(老年代)

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

3.3.ParNew收集器(新生代)

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

3.4.Parallel Scavenge收集器(新生代)

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

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

吞吐量 = 运行用户代码时间 / (用户代码时间 + 运行垃圾收集时间)

如果虚拟机完成某个任务,用户代码加上垃圾收集点共耗费了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。停顿时间越短,就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

3.5.Parallel Old收集器(老年代)

Parallel Old 收集器是一个多线程的垃圾收集器,使用"标记-整理"算法,是 Parallel Scavenge 收集器的老年代版本。

在注重吞吐量或者处理器资源较为稀缺的应用场景,都可以优先考虑 Parallel Scavenge 收集器 + Parallel Old 收集器这个收集器组合。

这个收集器是直到 JDK6 时才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于相当尴尬的状态,原因是如果新生代选择了 Parallel Scavenge 收集器,老年代除了 Serial Old 收集器以外别无选择,其他表现良好的老年代收集器,如 CMS 无法与它配合工作。由于老年代 Serial Old 收集器在服务端应用性能上的"拖累",使用 Parallel Scavenge 收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比 ParNew 加 CMS 的组合来得优秀。

3.6.CMS收集器

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

🔄 CMS工作原理(四步曲)

第一步:初始标记(Initial Mark)
  • 动作 :标记GC Roots能直接关联到的对象

  • 特点 :需要Stop-The-World,但速度极快

  • 耗时:很短,通常几毫秒

第二步:并发标记(Concurrent Mark)
  • 动作 :从GC Roots开始遍历整个对象图

  • 特点 :与用户线程并发执行

  • 耗时:较长,但不会停顿用户线程

第三步:重新标记(Remark)
  • 动作:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

  • 特点 :需要Stop-The-World

  • 耗时:比初始标记稍长,但远短于并发标记

第四步:并发清除(Concurrent Sweep)
  • 动作:清理垃圾对象,回收内存空间

  • 特点 :与用户线程并发执行

✅ CMS的优点和缺点

优点:

  1. 低停顿时间:大部分标记工作与用户线程并发执行

  2. 响应速度快:适合交互式应用

  3. 并发收集:减少Stop-The-World时间

缺点:

1.影响用户线程的执行效率

2.无法处理浮动垃圾

3.产生大量空间碎片

3.7.G1收集器(老年代)

3.7.1. 简述

大多数垃圾收集器的共性是在整个垃圾收集过程中,一定会发生 Stop The World (简称:STW),并且 STW 的时间是根据垃圾标记所需要的时间来确定,可能依然会存在某次垃圾收集时,STW 的时间过长的问题,导致这个问题的原因在大多数垃圾收集器都是对整个新生代或老年代进行垃圾回收,要扫描的对象太多了。但 STW 又是每个垃圾收集器都不可避免的,所以,现代垃圾收集器的发展就是为了能够尽量缩短 STW 的时间。

3.7.2. 什么是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 模式。

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

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

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

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

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

3.7.4.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) 的垃圾收集器的特征了。

🗑️ 垃圾收集器对比总表

收集器名称 适用代区 算法 线程 特点 优点 缺点 适用场景
Serial 新生代 复制算法 单线程 简单高效 实现简单,无线程开销 停顿时间长 客户端模式、单核处理器
ParNew 新生代 复制算法 多线程 Serial的多线程版 多核下效率高 单核下不如Serial 与CMS配合使用
Parallel Scavenge 新生代 复制算法 多线程 吞吐量优先 高吞吐量 停顿时间较长 后台运算、批处理
Serial Old 老年代 标记-整理 单线程 Serial的老年代版 稳定可靠 停顿时间长 客户端模式
Parallel Old 老年代 标记-整理 多线程 Parallel的老年代版 高吞吐量 JDK6才开始提供 与Parallel Scavenge搭配
CMS 老年代 标记-清除 并发 低停顿优先 响应速度快 内存碎片、CPU敏感 Web服务、交互应用
G1 全堆 分区收集 并发 可预测停顿 平衡吞吐与停顿 内存占用较大 大内存服务器
相关推荐
喵手2 分钟前
Java中Stream与集合框架的差异:如何通过Stream提升效率!
java·后端·java ee
JavaArchJourney3 分钟前
PriorityQueue 源码分析
java·源码
学行库小秘3 分钟前
基于门控循环单元的数据回归预测 GRU
人工智能·深度学习·神经网络·算法·回归·gru
喵手13 分钟前
你知道,如何使用Java的多线程机制优化高并发应用吗?
java·后端·java ee
_meow_18 分钟前
数学建模 15 逻辑回归与随机森林
算法·数学建模·逻辑回归
渣哥28 分钟前
10年Java老司机告诉你:为什么永远不要相信浮点数相等
java
Faith-小浩浩1 小时前
macos 多个版本的jdk
java·macos·jdk
二向箔reverse1 小时前
机器学习算法核心总结
人工智能·算法·机器学习
喵手1 小时前
Java异常处理最佳实践:如何避免捕获到不必要的异常?
java·后端·java ee
猿java1 小时前
精通MySQL却不了解OLAP和 OLTP,正常吗?
java·后端·面试