jvm垃圾回收策略

垃圾判定

垃圾判定是指在编程中确定哪些内存中的对象是“垃圾”,即不再被应用程序使用的对象,因此可以被垃圾回收器回收的过程。

在Java中,垃圾回收(Garbage Collection, GC)主要采用两种基本方法:引用计数法和可达性分析。下面分别对这两种方法进行说明:

  1. 引用计数算法是一种最直观的垃圾收集技术。其基本思想是给每个对象分配一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1。任何时刻计数器为0的对象就是不可能再被使用的,因此可以回收其占用的内存。

    不过,Java并不采用引用计数法来进行垃圾回收,因为它存在循环引用的问题。在循环引用中,两个或多个对象相互引用,但它们可能都已经不再被其他活动部分的应用程序所引用。由于它们相云引用,因此它们的引用计数永远不会达到0,导致内存泄漏。

    public class ReferenceCounting {
    Object instance = null;

    public static void main(String[] args) {
    ReferenceCounting objA = new ReferenceCounting();
    ReferenceCounting objB = new ReferenceCounting();

    // 创建循环引用
    objA.instance = objB;
    objB.instance = objA;

    // 尝试手动置空以断开引用
    objA = null;
    objB = null;

    // 希望GC能回收objA和objB,但如果是采用引用计数法,则无法回收
    System.gc();
    }
    }

  2. Java采用的是可达性分析算法来进行垃圾回收。在这种方法中,通过一系列的称为“GC Roots”的对象作为起点,然后向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

    在Java中,可作为GC Roots的常见对象包括:

    虚拟机栈(栈帧中的局部变量表)中引用的对象:例如,正在执行的方法中的局部变量或参数。
    方法区中类静态属性引用的对象:这些静态变量所引用的对象也被称为 GC Roots。
    方法区中常量引用的对象:例如,字符串常量池(String Table)的引用
    本地方法栈中JNI(Java Native Interface)引用的对象:在使用 JNI 调用本地方法的过程中,会涉及到本地方法栈,其中引用的对象也是 GC Roots。
    举一个简单的例子来描述可达性分析:

    public class ReachabilityAnalysis {
    public static void main(String[] args) {
    ReachabilityAnalysis obj = new ReachabilityAnalysis(); // 对象obj是可达的,因为它被栈上的引用变量所引用

    // 现在让我们断开这个引用
    obj = null; // 此时对象不再可达

    // 垃圾回收可以执行了,它将使用可达性分析来确定obj的内存是否可以被释放
    System.gc();
    }
    }

    在JVM模型中,垃圾回收主要发生在堆内存(Heap)中,因为这里是存放对象实例的地方。当前主流的JVM使用分代垃圾收集算法,将堆内存分为年轻代(Young Generation),老年代(Old Generation),以及永久代(Permanent Generation,但在Java 8及之后被MetaSpace所替代)。不同代的对象会根据其生命周期的不同被相应的垃圾回收器回收,以提高回收效率。

    垃圾回收算法、垃圾回收器的选择以及垃圾回收的时机,通常是由JVM自动管理的,但是开发者可以通过JVM参数来对其进行调优。


垃圾回收算法

  1. 标记-清除算法

    标记阶段:从根对象(如活动线程的堆栈指针、静态对象等)开始,递归遍历所有可达的对象,并将它们标记为活动的。
    清除阶段:遍历堆内存中所有对象,对于没有被标记为活动的对象,释放其占用的内存空间。
    缺点:

    整个过程中需要停止应用程序,导致停顿时间(STW,Stop-The-World)。
    会产生内存碎片。

  2. 标记-整理

    标记-整理算法是标记-清除的改进版。在标记活动对象之后,它会将所有存活的对象移到内存的一端,然后清理掉端边界外的内存空间。

    优点:

    解决了内存碎片问题,不需要复制活动对象。
    缺点:

    需要移动存活对象,可能会造成较大的内存迁移开销。
    需要较多的停顿时间,不适合对响应时间要求较高的应用

  3. 复制

    复制算法将堆内存分为两半:一半用于分配内存,另一半处于空闲状态。在垃圾收集期间,它将所有活动对象从当前的内存区域复制到另一半,接着清除原有的内存区域中的所有对象。

    优点:

    解决了内存碎片问题,适合存活对象较少场景。
    缺点:

    不适用于处理存活较多对象的场景
    会占用双倍内存空间


Minor GC和FULL GC的区别

Minor GC

Minor GC又称为Young Generation GC,是在年轻代(Young Generation)中发生的垃圾回收。

年轻代通常包含三个部分:

Eden空间:几乎所有新生成的对象首先都是在这里分配的。
两个Survivor空间(通常称为From和To或者S0和S1):用来存放从Eden和自己区域中经过第一次Minor
GC后仍然存活的对象。
Minor GC的过程通常是这样的:

新对象被分配到Eden区,当Eden区满了之后,触发Minor GC。
存活的对象从Eden区复制到一个Survivor区(From)。
如果存在之前已经经过一次Minor GC的对象,它们会被从一个Survivor区复制到另一个(从From到To),同时对象的年龄会增加。当对象年龄达到一定阈值之后,它们会被晋升到老年代(Old Generation)。
经过垃圾回收后,Eden区和一个Survivor区(从哪里复制的)会被清空。
Minor GC的特点是频繁且速度快。它只是针对年轻代的垃圾回收,而且通常情况下只会回收一小部分内存空间。

FULL  GC

Full GC,又称为Major GC或Old Generation GC,涉及整个Java堆(Heap)的回收,即包括年轻代、老年代(Old Generation),以及方法区(也称为永久代在JDK 7或之前的版本,或者元空间在JDK 8以后的版本)。

Full GC的过程相对来说比Minor GC要复杂且耗时:

对年轻代执行Minor GC。
然后老年代的对象也会被检查,不再存活的对象会被清除。
方法区/元空间中不再使用的类和静态内容将被回收。
识别老年代中可以移动的对象,并整理内存空间,从而在老年代中也能高效地分配内存。
Full GC发生的时候,通常会触发STW(Stop-The-World),即所有的应用线程都会被暂停,直到GC完成。

触发条件

Minor GC和Full GC的触发条件各不相同:

Minor GC通常是当年轻代的Eden区满了时触发。

Full GC可能由以下几个条件触发:

老年代空间不足。
方法区/元空间内存不足。
System.gc()方法的调用。
JVM的内存回收策略。


空间担保策略

定义

空间担保策略(Promotion Guarantee)是JVM中的一种机制,确保在Minor GC时,存活的对象能够成功晋升到老年代。如果老年代没有足够的空间来接收新晋升的对象,JVM可能会提前触发一次Full GC来释放空间,或者调整自己的内存分配策略以避免此类情况的发生。

工作原理

  • jdk6 以前 要根据是否允许担保失败来采取不同的出发FULL GC的条件,如果不允许,我们就要判断新生代的全部对象空间是否小于老年代剩余连续内存空间

    在进行Minor GC前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件不能满足,虚拟机会查看 -XX:HandlePromotionFailure 设置是否允许担保失败。如果不允许(false),那么会提前进行一次Full GC来清理老年代并为新生代晋升的对象腾出空间。如果允许担保失败(true),那么只要老年代剩余空间大于历次晋升到老年代对象的平均大小即可进行Minor GC,否则也要提前进行Full GC。

  • 从JDK 7开始,HotSpot虚拟机的垃圾收集器在做Minor GC之前的空间分配担保策略上进行了调整,取消了之前版本中的 -XX:HandlePromotionFailure 选项。每次都会判断老年代剩余最大连续空间大于历次Minor GC晋升的平均大小 或者 大于新生代所有对象的大小总和 , 大于任意一个,就允许触发MinorGC,反之触发 Full GC
    jdk6之后 就没有是否允许担保失败的选项了

     

如何调优空间担保策略

空间担保策略的调优通常涉及几个关键的JVM参数:

-XX:SurvivorRatio: 设置新生代中Eden区与Survivor区的比例。
-XX:NewRatio: 设置新生代与老年代的比例。
-XX:MaxTenuringThreshold: 设置对象在新生代的存活次数,超过这个次数的对象会被晋升到老年代。
-XX:PretenureSizeThreshold: 设置大小阈值,超过这个大小的对象不会在新生代分配,而直接在老年代分配。
调优通常需要根据应用程序的具体情况来进行。监控工具(如jstat, VisualVM或其他商业监控工具)可以帮助你理解内存使用情况,并据此做出调整。

总结

如果没有空间担保,Minor GC会进行尝试,很可能在晋升过程中失败,因为老年代没有足够的空间。这时JVM可能会抛出 OutOfMemoryError,或者尝试一次昂贵的Full GC来强制回收空间。

而开启空间担保策略,JVM在开始Minor GC之前会检查老年代是否有足够的空间。在这个情况下,JVM会认识到老年代空间不足,因此可能直接触发Full GC,来确保不会在Minor GC过程中出现内存分配失败。

总之,空间担保策略是一种预防措施,保障JVM在进行Minor GC时的内存分配安全性,尽量减少Full GC的发生,以提高系统的性能和稳定性。

注:每次的垃圾回收都是对系统资源的一次消耗,因此适当的调优可以减少GC的次数和影响,从而为应用程序提供更平滑的性能体验。


垃圾回收器

Serial GC

image

Parallel GC

image

 

CMS GC

image

G1 GC

image

Z GC

ZGC是一种低延迟的垃圾回收器,目前仍在积极开发中。ZGC的设计初衷是为了在大堆内存上工作,并且几乎不产生延迟。这使得它非常适合需要快速响应但是内存占用大的应用程序。

使用参数:

-XX:+UseZGC

Shenandoah GC

Shenandoah GC与ZGC有类似的目标:减少停顿时间,即便是在大堆或者多核心处理器的情况下。Shenandoah通过在GC的许多阶段与应用线程并发执行来实现这一目标。

使用参数:

-XX:+UseShenandoahGC

如何选择合适的垃圾处理器

在选择垃圾回收器时,需要考虑应用程序的需求:

对于需要最大吞吐量的应用程序,Parallel GC可能是最好的选择。
如果应用需要较短的响应时间或者较少的停顿时间,可以考虑CMS或G1。
对于需要极低停顿时间并且具有大内存容量的应用,ZGC或者Shenandoah可能是合适的选择。
在实际中,最好的方法是通过实际的性能测试来确定最适合你的应用的垃圾回收器。

JDK8默认回收处理器

JDK 8(Java Development Kit 8)中默认的垃圾回收器组合为Parallel Scavenge(用于Young Generation)加上Parallel Old(用于Old Generation)。Parallel GC 在 JDK 5 中开始成为默认的垃圾回收器,特点:并行+STW。

Parallel Scavenge收集器是一个新生代垃圾收集器,它使用复制算法,而且是并行的多线程收集器。它主要关注于达到一个可控制的吞吐量(Throughput)。所谓吞吐量,就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即:吞吐量 = 用户代码时间 /(用户代码时间 + 垃圾收集时间)。服务器环境中,这种收集器能够最大化的利用CPU时间,尽快地完成程序的运算任务,特别适用于多核服务器。

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和”标记-整理”算法。这个收集器是在JDK 6u14版之后引入的。从JDK 9 开始默认垃圾回收器是 G1 (Garbage-First)

CMS不作为默认处理器的原因

CMS(Concurrent Mark-Sweep)收集器是一个以获取最短回收停顿时间为目标的收集器,使用”标记-清除”算法,并且是并发的。但是在JDK版本的迭代中没有任何一个版本使用CMS为一个默认的垃圾回收器。

选择Parallel Scavenge和Parallel Old的原因而不用CMS,通常基于以下考虑:

吞吐量优先: 如果应用不是非常注重服务响应时间,而是更希望在单位时间内完成更多的工作,即追求较高的吞吐量,那么Parallel Scavenge加上Parallel Old垃圾收集器会是一个更合适的选择。

碎片化问题:CMS使用的”标记-清除”算法容易导致内存碎片化,当碎片化严重到一定程度,CMS需要进行一次完全停顿的垃圾收集以整理内存碎片(Full GC),这可能导致较长时间的停顿。而Parallel Old使用的”标记-整理”算法可以有效避免内存碎片化问题。

更简单稳定: 相较于CMS,Parallel Scavenge和Parallel Old通常来说维护起来比较简单,参数配置也更容易。而CMS有几百个参数需要配置调整,对于维护的成本是非常高的。

随着Java 9的发布,G1(Garbage-First)垃圾收集器成为了默认的垃圾收集器,它旨在替代CMS,提供更好的性能表现和更简单的调优选项。而在Java 11之后,还引入了ZGC和Shenandoah等实验性的低停顿时间垃圾收集器。

CMS和G1的比较

CMS(Concurrent Mark Sweep)和G1(Garbage-First)是Java虚拟机中的两种不同的垃圾回收器,它们各自有不同的特点和适用场景。

1.CMS垃圾回收流程
初始标记:GC为单线程,暂停所有用户线程,只标记GC Root 直接引用的对发标记:多个GC线程和用户线程同时工作,从GC Roots开始遍历整个对象图,找出所有可达对象。
使用三色标记法,对象被分为三种颜色:白色(未被扫描)、灰色(已扫描但其引用尚未全部扫描)、黑色(已扫描且其引用也已全部扫描)。初始时,所有对象被视为白色。随着扫描的进行,对象从白色变为灰色,然后变为黑色。所有可达对象标记为黑色。
重新标记:由于并发标记阶段用户程序仍在运行,可能会导致部分对象的标记状态发生改变,重新标记阶段就是为了修正这些变动。
多个GC线程STW进行。
并发清理:多个GC线程并发清理那些在标记阶段被标记为垃圾(即未被标记为黑色的对象)的对象,释放内存空间。
并发重置:多个GC线程并发将存活对象上的标记给移除掉,避免影等下次gc
2. G1垃圾回收流程
初始标记:GC单线程进行标记,暂停所有用户线程,记录GC ROOT 直接引用的对象
并发标记:多个GC线程和用户线程同时工作,从GC Roots开始遍历整个对象图,找出所有可达对象。
使用三色标记法,对象被分为三种颜色:白色(未被扫描)、灰色(已扫描但其引用尚未全部扫描)、黑色(已扫描且其引用也已全部扫描)。初始时,所有对象被视为白色。随着扫描的进行,对象从白色变为灰色,然后变为黑色。所有可达对象标记为黑色。
最终标记:因为上个过程是并发进行的,所以有些对象是在标记过程中新产生的,导致没有标记上,所以这个过程进行修正,将这些漏网之鱼都标上,这个过程是多线程 STW 进行。可以通过增量标记或使用写屏障技术来减少需要重新标记的对象数量。
筛选回收:计算每个region的回收成本,并按照回收成本进行排序,然后按照用户期望的停顿时间进行计算,决定应该回收哪些region。可以通过更智能的region选择算法来优化,例如优先回收垃圾对象最多的region或回收成本最低的region。
G1收集器是为了替代CMS收集器而设计的,提供更可预测的垃圾回收暂停时间,特别是在处理大内存容量的多处理器机器时。G1能够更好地控制停顿时间,通过将堆分割成多个区域(Region)来避免整个堆的垃圾回收,逐步地清理内存,特别适合大堆内存的应用。
3. 两者比较
停顿时间控制:G1具有更好的停顿时间控制能力,可以为应用程序设置停顿时间目标。
内存占用:G1通常比CMS需要更多的内存空间,因为它需要维护更多的内存结构。
预测性能:G1可以更准确地预测垃圾收集的停顿时间,因为它是基于区域的回收。
适用场景:CMS适用于对响应时间比较敏感的应用。而G1适用于堆内存较大的场景,特别是超过4GB的堆内存。
随着Java虚拟机的发展,G1由于其更好的可预测性和适应性,越来越多地成为默认的垃圾收集器,尤其是在Java 9及以后的版本中。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇