JVM垃圾回收

在Java堆内存创建的对象都是占用内存资源的,而且内存资源有限,当对象实例不需要使用时,JVM通过垃圾回收机制回收实例对象。GC回收前需要判断对象是否死去,通过引用计数法或可达性分析法。
  1. 引用计数法:每个对象都存在一个引用计数器,被引用就加1,引用失效就减1,任何时候为0代表对象不可能再被使用。此算法很难解决对象之间的循环引用问题。
  2. 可达性分析:从GC Roots开始向下搜索,走过的路径称为引用链,当一个对象到GC Roots不可达时,证明此对象不可用。GC Root就是一组必须活跃的引用,包括:活跃的栈帧里指向GC堆的对象的引用,常量类/静态变量的引用、常量池里的引用、所有当前被加载的java类及成员变量的引用。
对象的自救:一个对象正真确认死亡需要经过两次标记,对象进行可达性分析后,发现没有相应引用链,对象会被第一次标记并进行一次筛选。筛选条件就是此对象是否有必要执行finalize()方法。如果对象没有覆盖该方法或对象已执行过该方法,则不需要执行finalize()方法,直接回收。如果需要执行,则将对象放入一个队列,并被虚拟机创建的线程去触发。虚拟机直接去执行并等待它运行结束,为了避免一个对象在该方法中执行缓慢或死循环导致队列中其他对象永久等待,造成内存回收系统崩溃。
finalize()是对象逃脱死亡的最后机会,如果对象在该方法中重新与引用链上任何一个对象建立关联就成功自救。但任何对象只能执行一次该方法,执行过得下一次会被回收。
方法区的回收:永久代主要回收废弃常量和无用的类。
  • 废弃常量:没有任何String对象引用常量池中的某变量,也没有其他地方引用了这个字面量
  • 无用的类型,同时满足:堆中不存在该类的任何实例,加载该类的ClassLoader已回收,该类的Class对象没有在任何地方被引用,无法通过反射访问该类。

内存分配和回收策略

垃圾收集算法:
  • 标记清除:先标记需要回收的对象,标记完后统一回收。标记和回收效率不高,会产生大量不连续的碎片,后续程序需要分配较大对象时,无法找到足够连续内存不得不提前触发下一次GC。
  • 复制算法:将内存分为等量两块,每次用其一,用完了将还存活的对象复制到另一块,再清除之前的,对半区进行回收。无内存碎片,按顺序分配内存,更高效。
  • 标记整理:在对象存活率较高时进行复制算法,多次复制效率降低,老年代对象存活率高一般不直接用复制算法,而是标记整理。标记需要被回收的对象,存活的对象向一端移动,然后清理掉边界以外的内存。
  • 分代算法:不同年代内存采用不同的收集算法。新生代用复制算法,老年代用标记清除或标记整理

分配担保机制:

  1. 新生代采用复制算法,对象存活率低,分为了一块大点的伊甸区和两块小点的幸存者区。每次只用伊甸区和一块幸存者区。回收时,将两块区域中存活的对象复制到另一块幸存者区,然后清理两块空间,交换幸存者分区。如果S区空间不够,就要分配给老年代,老年代起兜底的作用。但是,老年代也是可能空间不足的。
  2. 所以,在这个过程中就需要做一次空间分配担保(CMS):在每一次执行YounaGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么说明本次Young GC是安全的。如果小于,那么虚拟机会査看 HandlePromotionFailure 参数设置的值判断是否允许担保失败。如果值为true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小(一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考)。如果大于,则尝试进行一次YoungGC,但这次YoungGC依然是有风险的;如果小于,或者HandlePromotionFailure=false,则会直接触发一次FuIIGC;
  3. 这个参数,在JDK7中就不再支持,后续的版本中,只要检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于则认为担保成功。但是需要注意的是,担保的结果可能成功,也可能失败。所以,在YGC的复制阶段执行之后,会发生以下种情况:剩余的存活对象大小,小于S区,直接进入S区;存活对象大小,大于S区,小于老年代可用内存,那就直接去老年代;剩余的存活对象大小,大于S并且大于老年代,触发FGC。
YGC与FGC触发时机:E区分配满就会触发YGC。FGC:老年代空间不足:创建了一个大对象、超过指定阈值就会存到老年代,空间不足时直接FGC。YGC后要移到老年代的对象,老年代存不下触发FGC;空间分配担保失败、永久代空间不足、代码中执行System.gc,不一定立即触发
STW:执行垃圾收集算法时,应用程序的其他所有线程都被挂起的全局暂停现象,所有Java停止,native代码可以执行,但不能与JVM交互。如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和多标。多标:当一个对象本来是垃圾对象(不可达对象),但是错误的标记成非垃圾对象(可达对象)时,会导致浮动垃圾。漏标:一个对象本来应该是存活对象,但是没有被正确的标记上,导致被错误的垃圾回收掉了
可达性和引用技术的问题:循环引用问题,如果两个对象互相引用,就形成了一个环形结构,如果采用引用计数法的话,那么这两个对象自将永远无法被回收。STW时间长,可达性分析的整个过程都需要STW,以避免对象的状态发生改变,这就导致GC停顿时长很长大大影响应用的整体性能。
三色标记:解决上述问题,将对象分为三种状态:
  • 白色:该对象没有被标记过。
  • 灰色:该对象已经被标记过了,但该对象的引用对象还没标记完,
  • 黑色:该对象已经被标记过了,并且他的全部引用对象也都标记完了。
    • 初始标记:遍历所有的根对象,将根对象和直接引用的对象标记为灰色。在这个阶段中,垃圾回收器只会扫描被直接或者间接引用的对象,而不会扫描整个堆。因此,初始标记阶段的时间比较短。(Stop The World)
    • 并发标记:在这个过程中,垃圾回收器会从灰色对象开始遍历整个对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色。并发标记过程中,应用程序线程可能会修改对象图,因此垃圾回收器需要使用写屏障(Write Barrier)技术来保证并发标记的正确性。(不需要STW)当应用程序线程修改了一个对象的引用时,写屏障会记录该对象的新标记状态。如果该对象未被标记过,那么它会被标记为灰色,以便在垃圾回收器的下一次遍历中进行标记。如果该对象已经被标记为可达对象,那么写屏障不会对该对象进行任何操作。
    • 重新标记:重新标记的主要作用是标记在并发标记阶段中被修改的对象以及未被遍历到的对象。这个过程中,垃圾回收器会从灰色对象重新开始遍历对象图,将被引用的对象标记为灰色,并将已经遍历过的对象标记为黑色。(Stop The World)
    • 在重新标记阶段结束之后,垃圾回收器会执行清除操作,将未被标记为可达对象的对象进行回收,从而释放内存空间。这个过程中,垃圾回收器会将所有未被标记的对象标记为白色(White)。
    • 以上三个标记阶段中,初始标记和重新标记是需要STW的,而并发标记是不需要STW的,其中最耗时的其实就是并发标记的这个阶段,因为这个阶段需要遍历整个对象树,而三色标记把这个阶段做到了和应用线程并发执行,大大降低了GC的停顿时长,
跨代引用:堆中不同代之间存在引用关系,通过Remembered Set:跟踪老年代对象与年轻代对象之间的引用关系,识别老年代中存活对象。减少全堆扫描的开销。它记录了老年代对象指向年轻代对象的引用关系,此后当发生GC时,垃圾回收器不需要扫描整个老年代来确定哪些对象存活。它只需扫描Remembered Set中的条目,从而减少了扫描的开销。在RememberSet中的对象也会被加入到GC Roots进行扫描:
不同的回收器:
CMS:老年代垃圾收集器。以获取最短回收停顿时间为目的的收集器,工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间。初始/重新标记两个步骤需要服务暂停,并发标记和并发清除耗时最长,仅用于老年代收集。
基于标记清除算法。运作过程:
  1. 初始标记:标记GC Roots直接引用的对象
  2. 并发标记:与用户线程并发交替运行,从初始标记阶段的对象开始找出所有活着的对象。
  3. 预清理:修正并发标记期间,程序继续运作导致没有标记到的存活的对象。(为了减少重新标记阶段的停顿时间,采用预标记)
  4. 重新标记:标记整个老年代存活对象
  5. 并发清除:清除没有标记的对象并且回收空间
优点:并发收集,低停顿。缺点:占用CPU资源,标记清除空间碎片,无法处理浮动垃圾,出现Concurrent Mode Failure;CMS能做到并发根本原因是标记清除算法过程进行了细粒度的分解。
G1:将java堆分划为多个大小相等的独立区域,2048个Region,这样在收集时不必在全堆范围内进行。但还是保留新生代和老年代的概念,只是没有了物理隔离。
  • 采用标记整理不会产生内存空间碎片,不会在分配大对象时因无法找到连续空间而提前触发下一次GC。
  • 可预测的停顿时间模型,可设置一个垃圾回收的预期停顿时间
  • 能充分利用多CPU,多核环境。使用多个CPU来缩短Stop-the-world的时间。通过并发方式让java程序继续运行

 

G1收集运作过程:

 

  1. 初始标记:标记GC Roots能直接关联到写的对象,修改TAMS值,让下一阶段用户程序并发运行时,在正确可用的Region中创建新对象,需时间要停顿,耗时很短
  2. 并发标记:进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行
  3. 最终标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
  4. 筛选回收:对各个Region的回收价值和成本进行排序。根据用户期望的停顿时间来制定回收计划,可与用户程序并发执行。只回收一部分Region,时间用户可控。
  全局变量和栈中引用的对象是可以列入根集合的,这样在寻找垃圾时,就可以从根集合出发扫描堆空间。在G1中,引入了一种新的能加入根集合的类型,就是记忆集(Remembered Set)。它通常约占Heap大小的20%或更高。并且,我们进行对象复制的时候,因为需要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。
ZGC(Z Garbage Collector)​ :是 Java 引入的一款低延迟垃圾回收器,主打 ​毫秒级停顿时间​(通常 <10ms),适用于大内存应用(TB 级堆内存)。
核心特点:
  1. ​并发回收:几乎所有阶段(标记、转移、压缩)均与应用线程并发执行,大幅减少 STW(Stop-The-World)停顿。
  2. ​内存压缩:动态整理碎片,避免长时间 Full GC,适合长期运行的服务。
  3. ​分代支持(JDK 21+)​:新增分代 ZGC(Generational ZGC),区分年轻代和老年代,进一步降低垃圾回收开销。
  4. ​可扩展性:堆内存从几百 MB 到数 TB 均可高效管理。
适用场景:高吞吐、低延迟要求的应用(如金融交易、实时系统),替代传统的 G1 或 CMS 回收器。
缺点:相比 G1 可能略高 CPU 占用,但权衡低延迟后通常是可接受的。
​ZGC vs. G1 vs. CMS 的区别
​1. 设计目标
  • ​CMS(Concurrent Mark-Sweep)​:以 ​低延迟 为目标,减少老年代回收的停顿时间,但不压缩内存,容易产生碎片。
  • ​G1(Garbage-First)​:平衡 ​吞吐量 和 ​延迟,采用分代+分区回收,适合大堆内存,但 STW(Stop-The-World)时间可能较长(几十到几百毫秒)。
  • ​ZGC:专注 ​极低延迟​(<10ms),所有阶段几乎并发,适合 TB 级堆内存,几乎没有碎片问题。
​2. 回收方式
  • ​CMS:并发标记+清除(不压缩),老年代回收时可能触发 Full GC(压缩)。
  • ​G1:分代+分区回收,年轻代和老年代混合回收,STW 时间可控但较长。
  • ​ZGC:​全并发​(标记、转移、压缩均不 STW),几乎没有 Full GC。
​3. 内存管理
  • ​CMS:不压缩内存,长期运行后可能因碎片导致 Full GC。
  • ​G1:增量压缩,减少碎片,但仍有 STW 停顿。
  • ​ZGC:动态压缩,无碎片问题,适合超大堆。
​4. 适用场景
  • ​CMS​(已废弃):适合中小堆、低延迟但容忍偶尔 Full GC 的应用(JDK 14 移除)。
  • ​G1​(默认):通用场景,平衡吞吐和延迟(JDK 9+ 默认)。
  • ​ZGC:超低延迟、大内存(如金融、实时系统),JDK 15+ 生产可用,JDK 21+ 支持分代优化。
​5. 缺点
  • ​CMS:碎片问题,可能触发 Full GC。
  • ​G1:STW 时间比 ZGC 长,吞吐略低。
  • ​ZGC:CPU 占用稍高(换取低延迟),JDK 11+ 才正式支持。
 
posted @ 2025-04-16 17:08  难得  阅读(30)  评论(0)    收藏  举报