Java 垃圾回收

目录

Java 垃圾回收基础知识

判断一个对象是否可被回收

引用计数算法

可达性分析算法

引用类型

强引用

软引用

弱引用

虚引用

垃圾回收算法

标记 - 清除

标记 - 整理 

复制 

分代收集

内存分配与回收策略

Minor GC、Major GC、Full GC

内存分配策略

1. 对象优先在 Eden 分配

2. 大对象直接进入老年代

3. 长期存活的对象进入老年代

4. 动态对象年龄判定

5. 空间分配担保

Full GC 的触发条件

1. 调用 System.gc()

2. 老年代空间不足

3. 空间分配担保失败

4. JDK 1.7 及以前的永久代空间不足

5. Concurrent Mode Failure

CMS 垃圾回收器

并发标记和最终标记的区别

CMS 收集器的优缺点 

CMS 收集器的“碎片化”问题


Java 垃圾回收基础知识

判断一个对象是否可被回收

引用计数算法

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
    }
}

可达性分析算法

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

引用类型

无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。

Java 具有四种强度不同的引用类型。

强引用

被强引用关联的对象不会被回收。

使用 new 一个新对象的方式来创建强引用。

Object obj = new Object();

软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

使用 SoftReference 类来创建软引用。

弱引用

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

使用 WeakReference 类来实现弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

虚引用

又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来实现虚引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

垃圾回收算法

标记 - 清除

将存活的对象进行标记,然后清理掉未被标记的对象。

不足:

  • 标记和清除过程效率都不高;
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

标记 - 整理 

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

复制 

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

主要不足是只使用了内存的一半。

现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

  • 新生代使用: 复制算法
  • 老年代使用: 标记 - 清除 或者 标记 - 整理 算法

内存分配与回收策略

Minor GC、Major GC、Full GC

JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)

  • 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
      • 目前,只有 CMS GC 会有单独收集老年代的行为
      • 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
    • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
      • 目前只有 G1 GC 会有这种行为
  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾

内存分配策略

1. 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

2. 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

3. 长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

4. 动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5. 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。

4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

CMS 垃圾回收器

CMS全称Concurrent Mark Sweep,是一种并发标记清除算法。它并发执行与用户程序,减少垃圾收集时程序暂停的时间。

CMS 的核心目标就是达到并发收集、减少停顿。它的工作流程如下,

初始标记:只标记GC Roots能直接关联的对象,速度快,与用户线程共享运行,不需要Stop The World。

// 初始标记代码示例
private void initialMark() {
    for (Object obj : strongRefs) {
        CMSCollector.mark(obj);
    }
}

并发标记:从GC Roots开始递归的标记对象图,与用户线程一起工作,需要部分STW阶段。

最终标记:修正在并发标记期间并发修改导致的错误标记,需要STW。

并发清除:与用户线程一起工作,清除被标记的对象,不需要STW。

并发重置:与用户线程一起工作,为下一次GC做准备,不需要STW。

并发标记和最终标记的区别

  • 并发标记阶段与用户线程一起运行,在标记过程中对象图可能被修改,会产生”脏标记“。需要最终标记修正。
  • 最终标记阶段需要Stop The World,去修正并发标记期间的”脏标记“,确保正确的标记对象。
  • 如果没有最终标记阶段,并发标记的”脏标记“会导致非垃圾对象被清除,或者垃圾对象未被清除。
  • 所以,最终标记阶段是CMS算法关键,它通过STW去修正并发阶段的错误”脏标记“,使得CMS并发标记清除算法成为可能。

CMS 收集器的优缺点 

  1. 会产生大量空间碎片,空间利用率低。
  2. 标记和清除过程需要占用CPU资源,并发时会对程序性能产生一定影响。
  3. 并发执行时,需要处理一致性问题,会加大开发难度。
  4. 只适用于老年代回收,新生代仍需其他收集器配合。
  5. 浮动垃圾可能带来更长的GC停顿时间,不适用于需要极低停顿的场景。

所以,CMS 是一款比较经典的低停顿收集器,但也存在一定的缺陷, v在JDK9后G1成为了更好的选择。但CMS算法本身的思想仍然值得我们学习。

CMS 收集器的“碎片化”问题

CMS 是一种标记-清除算法,它会产生大量空间碎片,空间利用率低是一个缺点。这是因为:

  1. 在并发标记阶段,需与用户程序同时运行,无法移动对象。
  2. 在并发清除阶段,直接清除标记的垃圾对象,留下空隙。
  3. 这些空隙会产生空间碎片,并不连续,但空间可能不足以分配较大对象。
  4. 空间利用率降低,达不到理想状态,需要更频繁的Major GC来整理空间。 我们可以从JVM源码中看到CMSCollector在清除对象后直接造成碎片:
//CMSCollector.java
private void sweep phase() {
    for (Object obj : markedObjects) { 
        freeMemory(obj); // 直接清除,产生碎片
    }
}

 

为解决这个问题,CMS做了一些努力:

  1. 采用Free-list维护小块空闲空间, satisfiy小对象分配。
  2. 对象晋升到老年代时预留一定空间减少碎片。
  3. 空间报警触发Minor GC和Full GC,做空间整合。
  4. 可配置启动Incremental CMS模式,进入并发清理阶段做连续空间整理。

但由于算法的限制,CMS仍无法完全解决碎片空间问题,这也是它被G1等收集器替代的原因之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值