JVM垃圾回收器详解

在Java编程中,垃圾回收器是自动管理内存的重要工具,它能够自动回收不再使用的对象所占用的内存,从而避免内存泄漏和内存溢出的问题。了解Java垃圾回收器的基础知识对于编写高效、稳定的Java程序至关重要。本篇博客将为你开启一段旅程,带你初步了解Java垃圾回收器的基础知识

一、介绍

垃圾回收器是自动内存管理的核心组件,其主要作用包括以下几个方面:

  1. 内存分配:当程序创建新的对象时,垃圾回收器会在堆内存中找到足够的空闲空间来存储这些对象。
  2. 内存回收:垃圾回收器会自动检测并回收那些不再被程序引用的内存区域,这些区域被称为“垃圾”。通过垃圾回收,可以确保内存资源的有效利用,防止内存泄漏。
  3. 系统稳定性:通过定期进行垃圾回收,可以避免由于长时间运行的程序持续占用内存而导致的系统资源耗尽和性能下降问题。
  4. 简化编程:垃圾回收器消除了手动管理内存的复杂性,使得我们可以更专注于业务逻辑的实现,而无需关心内存分配和释放的具体细节。

垃圾回收器通常采用各种算法和技术来实现上述功能,如标记-清除、复制、标记-压缩等。这些算法的目标是在保证程序正常运行的同时,尽可能高效地管理和回收内存资源。需要注意的是,虽然垃圾回收器大大减轻了程序员的工作负担,但在某些特定场景下,如实时系统或对内存控制有严格要求的应用中,可能仍需要进行一定程度的手动内存管理。

二、内存分配与对象生命周期

数据内存主要分配在栈和堆上,栈是线程私有的,当一个线程调用完毕后 线程中所使用到的内存区域也就随之销毁了。而堆空间是线程共有的。当我们对于某些方法或者某些对象管理不当时,就会对我们的堆或者是栈造成内存溢出、程序崩溃的影响。

栈: 之前的博客中我们提到过,栈存储的每个方法的调用以及内部的一些符号引用、基本变量等信息,每一个方法为一个栈帧;当我们一个栈帧弹出后,其中包括的基本数据类型变量及地址所对应的数据也随之销毁,和我们的函数是紧密相关的。

堆:存储我们在程序中通过new的对象或者数组,我们都会为这个对象或数组在堆中开辟一块内存空间,返回该内存区域的一个地址引用。

三、垃圾回收的算法以及基本原理

  1. 在了解基本原理之前,我们先来看下什么是"垃圾"?
    • 通俗一些就是在我们的程序中没有任何地方在对某一个变量存在引用关系的时候,这个对象就变成了无用对象,也就是我们这里说的"垃圾";
  2. 那垃圾回收器又是怎么知道我这个对象没有在其他地方被引用了呢?
    • 引用计数分析算法:
      • 当对一个对象进行引用的时候,该对象的引用计数就会进行+1,当该计数为0的时候表示该对象变成了垃圾对象。但该算法存在问题,两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
    • 可达性分析算法:
      • 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
      • GC Roots一般包含以下内容:
        • 虚拟机栈中引用的对象
        • 本地方法栈中引用的对象
        • 方法区中类静态属性引用的对象
        • 方法区中的常量引用的对象
  3. 上边#2说了怎么被认定为该对象是"垃圾",那下边我们介绍几种常见的垃圾回收算法
    • 标记-清除
    • 标记-整理
    • 复制
    • 分代收集

3.1. 标记清除

image.png
将所有可回收的对象(即垃圾对象)进行标记可以进行回收,回收后如右侧的图一样。内存空间有些是不连续的。这种算法的坏处就是会有大量不连续的内存碎片,使内存利用率低。 假如我有一个大对象进来之后,可能需要连续的一些内存区域,而清除后的剩下的这些区域因为存在不连读内存区域而导致大对象无法存放,会直接进入老年代。

3.2. 标记整理
image.png
将所有可回收的对象进行标记可回收,回收后如右图;垃圾回收算法会将剩余存活对象重新进行在内存区域中排列存放,形成有序的内存区域占用。这种算法好处就是不会有内存碎片的产生,内存区域都是连续的。但是这种算法的不足在于 剩余存活对象可能会需要移动在内存中的位置而更新地址引用,对象的移动增加了系统的复杂和开销。

3.3. 复制
image.png
会将一块内存区域划分成两个一样大小的两半内存区域,每次也就只能使用其中一块内存区域,当一块满了之后会进行回收,将剩余对象复制到另一半内存区域并进行整理。内存使用率低,而且不适合大对象存储,内存地址引用的更换和整理算法一样,增加了系统的复杂和开销。好处是不会产生碎片,更高效一些(原因是只使用了一半的内存区域),适合生命周期短的对象;

3.4. 分代收集

分代收集 是一种基于对象生命周期假设的垃圾回收策略。它将堆内存划分为不同的区域或“代”,每个代代表了对象的不同生命周期阶段。通常,堆内存被划分为新生代(新生代被划分了 Eden区、Survivor Form区和To区)和老年代。

新生代:新生代一般采用 复制算法。

  • 新生代主要存储新创建的对象和短生命周期的对象。
  • 垃圾回收器在新生代中频繁执行垃圾回收操作,如使用复制算法或标记-清除算法。
  • 当新生代中的对象经历过一定次数的垃圾回收仍然存活时,它们会被晋升到老年代。

老年代:标记 - 清除 或者 标记 - 整理 算法。jdk1.8 老年代默认使用的是 标记-整理 算法。

  • 老年代主要存储长期存活的对象和大对象。
  • 垃圾回收器在老年代中的垃圾回收操作相对较少且较为复杂,通常采用标记-整理或标记-清除算法。

四、垃圾收集器

image.png
垃圾回收器有Serial、ParNew、Parallel、CMS、Serial Old、Parallel Old、G1、ZGC等;jdk1.8默认使用的是多线程垃圾回收器(parallel-新生代、Parallel Old-老年代),jdk1.9使用的就是G1收集器了。

4.1. Serial垃圾收集器

image.png

单线程垃圾回收机制(会STW 【stop the world】) 用于年轻代的回收;可使用命令开启使用该垃圾回收器 -XX:+UseSerialGC ; 它是以串行的方式执行的。新生代采用复制算法,老年代采用标记整理算法;

优点: 优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。适合单核或小型应用;
缺点:单线程执行且需要停顿用户所有线程,对于大型应用来说会比较耗时。处理大内存来说效率会低。

总的来说,Serial垃圾收集器适用于那些对响应时间要求不高、内存较小或者处理器核心数量有限的环境。对于更复杂、高性能或者大规模的应用,可能需要选择更先进的多线程垃圾收集器,如Parallel GC、CMS GC或G1 GC等

4.2. Serial Old垃圾收集器

image.png
是Serial收集器的老年代版本,主要用于执行老年代的垃圾回收;可使用命令开启使用该垃圾回收器 -XX:+UseSerialOldGC ;该收集器使用"标记-整理"算法。
优缺点:同Serial垃圾收集器一样;

4.3. ParNew 收集器
image.png
它是 Serial 收集器的多线程版本,主要用于新生代的垃圾回收;默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数;在实际应用中,需要根据具体的工作负载和系统配置来选择合适的垃圾收集器。

优点:多线程并行执行;使用"标记-复制"算法;效率更高效,单核上可能比Serial要低一些;可以与CMS垃圾回收器一起使用;因为可以并行执行,多核情况下可以缩短用户线程的停顿时间;
缺点:该垃圾回收器不可以单独使用,需要与其他收集器配合使用;不适用于单核或小型系统;

4.4. CMS垃圾回收器
image.png
主要用于老年代,以最大程度减少STW时间来完成用户体验,它实现了基本上让垃圾回收和用户进程同时工作。可能会发生多标和漏标的现象,但CMS会使用写屏障 + 增量更新来解决此问题;采用标记清除算法;可使用指令-XX:+UseConcMarkSweepGC(old) 开启;
整个过程分为5步:
1.初始标记(会进行STW):这是一个STW阶段,垃圾收集器标记从GC Roots直接可达的老年代对象。
2.并发标记(会和用户线程同步进行,这步操作会很慢,同时会和用户线程争抢资源):垃圾收集器并发地跟踪和标记所有从第一步标记的对象可达的对象。
3.重新标记(会进行STW):用于处理在并发标记阶段期间产生的浮动垃圾。
4.并发清除(也会和用户线程同步进行):垃圾收集器并发地清理未被标记为存活的对象,并释放相应的内存空间。
5.并发重置(清除所有标记):在某些情况下,CMS会尝试进行并发压缩,以解决内存碎片问题。

多标:
会产生浮动垃圾,等下次GC的时候会进行清理。在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。

漏标:
这会是挺严重的一个bug,会将有用的对象当作垃圾对象删除。漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新和原始快照;增量更新和原始快照是通过三色标记来完成的。

三色标记:
将对象分为三种颜色,黑色、灰色、白色
黑色: 指一个对象里所有的引用都扫描完了会被标记为黑色(只是当前自己这一级)
灰色: 指一个对象所引用的对象至少还有一个没有被扫描 则会标记为灰色
白色: 最初所有对象都是白色标记。

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之 后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向 白色对象的引用之后, 它就变回灰色对象了。

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑 色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
读写屏障参考aop的概念,其实就是在赋值前或者后做一些处理,进行记录。

优点:
1.提升了用户的体验;并发收集、低停顿;适合高吞吐量应用;CMS使用多线程进行标记和扫描,这有助于加快垃圾回收的速度;
缺点:

  1. 会和用户线程争抢资源
  2. 可能会发生并发失败的问题
    • 出现原因:当在发生GC的时候,因为和用户线程同步进行,可能会有新的对象继续往老年代存放,也就是还没有回收完又触发了GC。如果出现,则会使用单线程的垃圾回收器,会非常慢。
    • 如何解决:可以通过设置老年代满足多少阈值之后就触发GC,保证老年代不会满了之后还往里存放对象)
  3. 无法处理浮动垃圾,只能等下次GC的时候处理
  4. 因为采用标记清除算法可能会产生大量的内存碎片,但可以设置参数完善这不,就是每次GC之后进行一次内存整理(-XX:+UseCMSCompactAtFullCollection:设置几次之后进行一次内存整理)

请注意,仅仅开启CMS垃圾回收器可能还不够,你可能还需要配置相关的参数来优化其性能。以下是一些常用的CMS相关参数:

java -XX:+UseConcMarkSweepGC \
    -XX:NewRatio=3 \
    -XX:SurvivorRatio=8 \
    -XX:MaxTenuringThreshold=15 \
    -XX:+UseParNewGC \
    -XX:CMSInitiatingOccupancyFraction=70 \
    -XX:+UseCMSInitiatingOccupancyOnly \
    -XX:+CMSParallelRemarkEnabled \
    -XX:CMSFullGCsBeforeCompaction=5 \
    -jar your_application.jar

这些参数的含义如下:

-XX:NewRatio=3:设置年轻代与老年代的大小比例为1:3。
-XX:SurvivorRatio=8:设置新生代中Eden区与一个Survivor区的大小比例为8:1。
-XX:MaxTenuringThreshold=15:设置对象从年轻代晋升到老年代的最大年龄阈值。
-XX:+UseParNewGC:启用ParNew垃圾收集器作为新生代的垃圾回收器,与CMS配合使用。
-XX:CMSInitiatingOccupancyFraction=70:设置当老年代占用率达到70%时触发CMS垃圾回收(默认是92)。
-XX:+UseCMSInitiatingOccupancyOnly:仅使用上述的占用率阈值来触发CMS垃圾回收(只使用设定的回收阈值)。
-XX:+CMSParallelRemarkEnabled:启用并行的remark阶段(在重新标记的时候多线程执行,缩短STW;)-XX:CMSFullGCsBeforeCompaction=5:设置执行多少次不压缩的CMS垃圾回收后,进行一次带压缩的Full GC。

4.5. Parallel & Parallel Old垃圾回收器
image.png
jdk1.8的默认垃圾收集器。与 ParNew 一样是多线程收集器,线程数会和电脑的核数一样。主要用于新生代;它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器;新生代采用复制算法,老年代采用标记整理算法;它可以使用 -XX:+UseParallelGC(年轻代), -XX:+UseParallelOldGC(老年代)来开启使用该垃圾回收器;

优点:
高吞吐量:通过并行执行,Parallel垃圾收集器能够提高垃圾回收的效率,从而实现更高的吞吐量。
适用于多核处理器:它能够充分利用多核处理器的资源,提高垃圾回收的速度。
缺点:
停顿时间可能较长:虽然并行执行可以提高整体效率,但在垃圾回收过程中仍然会导致应用程序暂停,并且在大型堆或高并发场景下,停顿时间可能会相对较长。

4.6. G1垃圾回收器
image.png

G1 是一个面向服务面向用户的一个垃圾回收器。主要针对大内存的机器。jdk9中将G1变成默认的垃圾收集器。
G1也还有分代的概念,只不过内部划分的是好多个小的内存区叫region,默认会分配5%的新生代内存区域。当然这个默认比也可以通过参数进行设置。最大区域不可以超过60%;
JVM最多可以划分2048个内存区域(Region)。假如堆有4个G的内存,则Region大小为2M,当然也可以手动指定Region的大小,不过只可以是2的n次幂。
每个小的内存区域(Region)是可以进行变化的(当当前内存区域被回收之后),这块儿区域还可能变成老年代。
大对象会进入H的内存块儿;在进行 MixedGC 的时候会进行回收大对象。

G1回收算法: 主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样 回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
image.png

G1 大致分为以下步骤:
1.初始标记
2.并发标记
3.最终标记
4 筛选回收: 因为为了满足最大停顿时间,所以不一定会吧所有的垃圾对象都回收,回收不完的时候只会回收一部分,剩下的到下次GC的时候再回收。

G1的特点:
1.并行与并发
2.分代收集
3.空间整合(G1从整体来看是基于“标记整理”算法实现的收集器;从局部 上来看是基于“复制”算法实现的。)
4.可预测的停顿。

G1提供的三种垃圾回收模式 :
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时 间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的 Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做 MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够 的空region能够承载拷贝对象就会触发一次Full GC

Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这 个过程是非常耗时的。(Shenandoah优化成多线程收集了)

什么场景适合使用G1?

  1. 50%以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化非常大
  3. 垃圾回收时间特别长,超过1秒
  4. 8GB以上的堆内存(建议值)
  5. 停顿时间是500ms以内

总结:理解垃圾回收器的工作原理是优化应用程序性能的关键步骤。不同的垃圾回收器有着各自的优点和适用场景,了解这些差异可以帮助我们根据应用程序的需求选择最合适的垃圾回收策略。例如,对于延迟敏感的应用,我们可能需要选择低暂停时间的垃圾回收器;而对于高吞吐量的需求,我们可能更关注提高垃圾回收的效率。

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值