3、JVM 垃圾回收(超全面)

本栏目讲叙JVM简介、JVM内存结构、垃圾回收机制和类加载与字节码技术


五种引用

五种引用

1、强引用

  • 概述由 new 关键字创建的对象,只有所有 GC Roots 对象都不引用该对象,该对象才能被垃圾回收
// new Object()对象被obj强引用
Object obj = new Object();
// 只有当obj = null或线程运行结束,不引用该对象才会被回收,否则即使内存不足,JVM宁愿抛出OutOfMemoryError也不会回收
obj = null;

2、软引用

  • 概述仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。可以配合引用队列来释放软引用自身
public class SoftReferenceDemo {

    private static final int _4MB = 4 * 1024 * 1024;

    // 将堆内存设置为20MB -Xmx20MB -XX:+PrintGCDetails -verbose:gc
    // list -> softReference -> byte[]
    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<SoftReference<byte[]>>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();

        for (int i = 0; i < 5; i++) {
			// SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[_4MB]);
            // 关联了引用队列,当软引用所关联的byte[]被回收时,软引用自己会加入到queue中去
            SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[_4MB], queue);
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中去除没软引用
        Reference<? extends byte[]> reference = queue.poll();
        while (reference != null) {
            list.remove(reference);
            reference = queue.poll();
        }

        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(Arrays.toString(ref.get()));
        }
    }

}

3、弱引用

  • 概述仅有弱引用引用该对象。在垃圾回收时,无论内存是否充足,都会回收弱引用对象。可以配合引用队列来释放弱引用自身
public class WeakReferenceDemo {

    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {

        List<WeakReference<byte[]>> list = new ArrayList<WeakReference<byte[]>>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> reference = new WeakReference<byte[]>(new byte[_4MB]);
            list.add(reference);
            for (WeakReference<byte[]> ref : list) {
                System.out.println(Arrays.toString(ref.get()));
            }
            System.out.println();
        }
        System.out.println("循环结束");
    }

}

4、虚引用

  • 概述:必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 ReferenceHandler 线程调用虚引用相关方法释放直接内存

5、终结器引用

  • 概述:无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

可回收算法

1、引用计数法

  • 概述:一个对象被变量引用时计数加 1,当该对象不被变量引用则减 1,当计数为 0 时,可以被回收
  • 缺点:会出现循环引用的情况,这样导致两个对象都不能被垃圾回收
    引用计数法

2、可达性分析算法

  • 概述:在垃圾回收之前,对堆内存中的对象进行扫码,检测对象是否被根对象直接或间接的引用,如果被引用则不能被回收

垃圾回收算法

1、标记清除

  • 步骤
    • 标记没有引用的对象
    • 将标记为垃圾的对象进行释放,即将对象所占用内存的起始、结束地址记录在空闲地址列表中,等再次分配时再从列表中获取
  • 优点:垃圾回收速度快
  • 缺点:容易产生内存碎片
    标记清除

2、标记整理

  • 步骤
    • 标记没有引用的对象
    • 将标记为垃圾的对象进行释放,并把没被标记回收的对象进行整理
  • 优点:不产生内存碎片
  • 缺点:效率较慢
    标记整理

3、复制

  • 步骤
    • 标记没有引用的对象
    • 将没有标记的对象从 FROM 区复制到 TO 区
    • 清除 FROM 区,并且将 TO 区与 FROM 区交换
  • 优点:不产生内存碎片
  • 缺点:占用双倍的内存空间
    复制

分代回收

分代回收

  • 对象首先分配在伊甸园
  • 当新生代空间不足时,触发 Minor GC ,伊甸园和 From 幸存区存活的对象复制到 To 幸存区中,存活的对象年龄加 1 并且交换两个幸存区
  • Minor GC 会引发 STW ,暂停其它用户线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升到老年代,该阈值默认为 15
  • 当老年代空间不足时,会先尝试触发 Minor GC,如果空间仍不足,那么触发 Full GC (进行全部的清理),STW 时间更长

垃圾回收器

1、SerialGC

  • 概述:串行垃圾回收器,适用于堆内存较小,单核 CPU 的情况
  • 启动:-XX:UseSerialGC = Serial + SerialOld(新生代使用复制算法,老年代使用标记整理算法)
  • 工作过程:多个线程执行 Java 代码,当堆内存不足时,会在安全点暂停其他线程,进行垃圾回收,此时只有一个垃圾回收线程运行,其他线程处于阻塞中。当垃圾线程回收完毕,再唤醒其他线程
    SerialGC

2、ParallelGC

  • 概述:并发垃圾回收器是以吞吐量优化的垃圾回收器。即单位时间内,多个线程进行垃圾回收,使多次 STW 平均时间变短,从而提高吞吐量
  • 场景:适用于堆内存较多,多核 CPU 的场景
    ParallelGC
  • VM参数
    • -XX:+UseParallelGC ~ XX:+UseParallelOldGC:新生代使用复制算法,老年代使用标记整理算法
    • -XX:+UseAdaptiveSizePolicy:设置新生代的内存为自适应策略
    • -XX:GCTimeRatio = ratio:调整吞吐量,一般设置为19。公式为1 / (1 + ratio),如1 / (1 + 99) = 0.01,即垃圾回收时间不能超过总时间的 0.01,如果达不到这个目标,则 GC 会自动调整堆大小来达到目标
    • -XX:MaxGCPauseMillis = ms:垃圾回收时用户线程暂停时间,默认 200 ms,与 GCTimeRatio 参数是对立的
    • -XX:ParallelGCThreads = n:设置垃圾回收线程数

3、响应时间优化 GC

  • 概述:多个线程进行垃圾回收,尽可能让单次 STW 的时间最短,从而降低响应时间
  • 场景:适用于堆内存较多,多核 CPU 的场景
    响应时间优化 GC
  • 工作流程
    • 当老年代内存不足时,线程在安全点暂停,CMS进行初始标记(只标记根对象)
    • 初始标记后,唤醒其他用户线程,同时CMS进行并发标记(标记剩余垃圾)
    • 然后暂停所有工作线程,进行重新标记
    • 然后唤醒所有工作线程,并且进行并发清理
  • VM 参数
    • -XX:+UseConcMarkSweepGC ~ -XX:+UseParkNewGC ~ SerialOld:CMS 是工作在老年代基于标记清除算法的 GC,如果并发失败时,CMS 会退化成 SerialGC,PN是工作在新生代基于复制算法的 GC
    • -XX:ParallelGCThreads = n:并发垃圾回收线程数量,默认 CPU 核心数量
    • -XX:ConcGCThreads = n:并行垃圾回收线程数量,一般为并行线程数量的 1/4
    • -XX:CMSInitiatingQccupancyFaction = percent:设置老年代内存占比多少时进行垃圾回收,默认 65
    • -XX:CMSScavingeBeforeRemark:重新标记前对新生代先进行垃圾回收,减少重新标记时新生代引用了老年代对象

4、G1

  • 概述:将堆划分成多个大小相等的区域,注重吞吐量和低延迟的垃圾回收器,整体使用标记整理算法,两个区域之间是复制算法。适用于超大堆内存的环境
    G1
  • 工作流程
    • Young Collection 阶段:新生代 GC 进行初始标记,并进行复制(E->S->O) ,如果老年代的对象引用新生代对象,则标记为脏卡,当进行GC Root遍历时只需要找脏卡,提高效率
    • Concurrent Mark 阶段:当老年代占用堆空间比例达到阈值时,当垃圾回收的速度高于产生的速度,进行并发标记,否则,退化成串行回收,进行 Full GC
    • Mixed Collection 阶段:进行最终标记和拷贝存活,这两个过程都会发生 STW
  • VM参数
    • -XX:+UseG1GC:启用 G1
    • -XX:InitiatingHeapQccupancyPercent = percent:设置老年代内存占比多少时进行垃圾回收,默认 45
    • -XX:+G1HeapRegionSize = size:设置区域大小,每个区域都可以做为独立的新生代和老年代
    • -XX:+MaxGCPauseMillis = time:垃圾回收时用户线程暂停时间

回收调优

1、相关 VM 参数

  • -Xms:堆初始化大小
  • -Xmx:堆最大大小
  • -Xmn:新生代大小
  • -XX:InitialSurvivorRatio = ratio 和 -XX+UseAdaptiveSizePolicy:幸存区比例(动态)
  • -XX:SurvivorRatio = ratio:幸存区比例
  • -XX:MaxTenuringThreshold = threshold:晋升阈值
  • -XX:+PrintTenuringDistribution:晋升详情
  • -XX:+PrintGCDetail -verbose:gc:GC 详情
  • -XX:+ScavengeBeforeFullGC:FullGC 前 MinorGC
  • -XX:+PrintFlagsFinal -version | findstr “GC”:查看虚拟机运行参数

2、新生代调优

  • 特点:创建对象时会在伊甸园中分配内存,速度非常快,死亡对象回收成本是零;伊甸园和幸存区 From 使用复制算法将数据复制到幸存区 To,清除数据的速度远远低于 Full GC
  • 新生代调优:并发量 * (请求数据大小 - 响应数据大小),如并发量 1000,请求数据 - 响应数据:512 K,那么新生代应设置为 521 M
  • 幸存区调优
    • 容量要尽量大,如果内存少,会提前把对象存入老年代,那么只有触发了 Full GC 时才会清除对象
    • 晋升阈值要适当,让长时间存放对象尽快存入老年代
# 堆初始化大小
-Xms = size

# 设置新生代大小
-Xmn = size

# 幸存区比例
-XX:SurvivorRatio = ratio

# 设置晋升阈值
-XX:MaxTenuringThreshold = threshold

# 打印晋升阈值
-XX:PrintTenuringDistribution

2、老年代调优

  • 避免 GC
    • 数据是不是太多,查看是否查询了大表
    • 数据表示是否太臃肿
    • 是否内存泄漏,可以使用软、弱和第三方缓存解决
  • 调优策略:以 CMS 为例
    • CMS 老年代内存越大越好
    • 在尝试新生代调优,再考虑老年代调优
    • 观察发生 Full GC 时老年代内存占用,使用 -XX:CMSInitiatingOccupancyFaction = percent,将老年代内存上调 1/4 - 1/3

3、案例

# 案例1:Full GC 和 Minor GC频繁
解决:增大新生代空间

# 案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
解决:根据分析,CMS进行垃圾回收时,时间都消耗在重新标记,可以设置-XX:CMSScavingeBeforeRemark:
重新标记前对新生代先进行垃圾回收,减少重新标记时新生代引用了老年代对象

# 案例3:老年代充实情况下,发生 Full GC(1.7版本)
解决:根据分析,并不是空间不足或空间碎片过多导致并发失败后进行Full GC,而是因为1.7版本使用的是永久代,
永久代空间不足时也会发生Full GC,所以可以增加永久代大小,或使用1.8以上版本
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值