JVM-垃圾回收

一、如何判断对象可以回收

1.1 引用计数法

如果一个对象被其他变量所引用,就让对象计数加1,如果引用了两次,计数加2,若某个变量不再引用了,则计数减1.
弊端:循环引用问题
在这里插入图片描述
两个对象的引用计数都是1,不能被垃圾回收,造成内存泄露

1.2 可达性分析算法

需要确定一系列根对象,根对象是不能被回收的对象。

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以
    回收
  • 哪些对象可以作为 GC Root ?
    在这里插入图片描述
1.3 四种引用

在这里插入图片描述

  • 所有的实线箭头代表强引用,虚线箭头表示软、弱、虚引用
  1. 强引用
    平时 new 一个对象,通过=赋值给一个变量,这个变量就强引用了 这个对象,
    只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。若被引用,则不会垃圾回收。

  2. 软引用(SoftReference)
    没有强引用引用该对象时,仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
    可以配合引用队列来释放软引用自身

  • 应用
/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_3 {

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

    public static void main(String[] args) throws IOException {
    //强引用
        /*List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }

        System.in.read();*/
        soft();


    }

    public static void soft() {
    //软引用
        // list --> SoftReference --> byte[]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

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

这里给堆内存设置为20m,在强引用下,会出现堆内存溢出,不重要资源用软引用来解决。

  • 引用队列:从队列中获取无用的 软引用对象,并移除
/**
 * 演示软引用, 配合引用队列
 */
public class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

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

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

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}

  1. 弱引用(WeakReference)
    没有强引用引用该对象时,仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    可以配合引用队列来释放弱引用自身
  • 应用:与软引用类似
/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();

        }
        System.out.println("循环结束:" + list.size());
    }
}

  1. 虚引用(PhantomReference)
    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
    由 Reference Handler 线程调用虚引用相关方法释放直接内存(Unsafe.freeMemory())
  2. 终结器引用(FinalReference)
    无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象 (不推荐使用 finalize 释放)

二、垃圾回收算法

JVM根据不同的情况使用以下三个垃圾回收算法

2.1 标记清除

把没有引用的对象先标记,再清除。

  • 速度较快
  • 会造成内存碎片

在这里插入图片描述

2.2 标记整理

把没有引用的对象先标记,清除了后向前整理

  • 速度慢
  • 没有内存碎片

在这里插入图片描述

2.3 复制

先对没有引用的对象标记,然后把引用的对象复制到另一个空间,再清空From内存空间,将From和To空间交换。

  • 不会有内存碎片
  • 需要占用双倍空间

在这里插入图片描述

三、分代垃圾回收

长时间使用的对象,放老年代中,用完就丢弃的对象放新生代中
在这里插入图片描述

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

3.1 相关 JVM 参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC-XX:+ScavengeBeforeFullGC

四、垃圾回收器

  1. 串行
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先
    • 多线程
    • 堆内存较大,多核 cpu
    • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
  3. 响应时间优先(CMS)
    • 多线程
    • 堆内存较大,多核 cpu
    • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

4.1 串行

-XX:+UseSerialGC = Serial + SerialOld

安全点: 让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器 : 在新生代,使用复制算法。单线程、简单高效。Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
SerialOld 收集器: 在老年代,标记整理算法

ParNew 收集器: ParNew 收集器其实就是 Serial 收集器的多线程版本。一样存在 Stop The World 问题。
在这里插入图片描述

4.2 吞吐量优先

并行工作

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n

在这里插入图片描述
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器
特点: 属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)

4.3 响应时间优先

UseConcMarkSweepGC :并发工作

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

在这里插入图片描述
CMS 收集器: Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器。
特点: 基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景: 适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:

  1. 初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
  2. 并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
  3. 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
  4. 并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

4.4 G1

定义:Garbage First
适用场景:

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

相关 JVM 参数

-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
4.4.1 G1 垃圾回收阶段

在这里插入图片描述

  1. Young Collection:对新生代垃圾进行收集
  • 划分为多个Region,并且有STW时间,E表示伊甸园区域
    在这里插入图片描述
  • 当伊甸园区满了,就通过复制算法,复制到幸存区
    在这里插入图片描述
  • 当幸存区对象较多,或者存活年龄超过一定时间(15),触发垃圾回收,新生代的一部分对象会存到老年代,不够年龄的会拷贝到另一个幸存区。
    在这里插入图片描述
  1. Young Collection + Concurrent Mark:新生代的垃圾收集+一些并发标记
  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

在这里插入图片描述

  1. Mixd Collection:混合收集

会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMillis=ms

在这里插入图片描述

  • 新生代复制到幸存区,
  • 部分幸存区中未满年龄复制到另一个幸存区,
  • 幸存区复制到老年区,
  • 老年区中有用对象复制到其他老年区。
4.4.2 Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足

CMS 和 G1 当老年代空间不足时,不一定会触发 full gc,老年代占用堆空间比例达到阈值时,会触发并发标记和混合收集,回收速度 > 用户新线程数,不触发;当回收速度 < 新线程产生数(垃圾数),退回串行,进行 full gc。

4.4.3 Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题(根对象在老年代)
    在这里插入图片描述
  • 卡表与 Remembered Set
  • 在引用变更时通过 post-write barrier + dirty card queue
  • concurrent refinement threads 更新 Remembered Set
    在这里插入图片描述
4.4.4 Remark
  • pre-write barrier + satb_mark_queue
    在这里插入图片描述
    黑色表示处理完了,有引用在引用,灰色表示正在处理中,白色表示未处理。单独的白色会垃圾回收。

在这里插入图片描述

在这里插入图片描述
若 C 的引用由 B 变到 A ,写屏障指令会执行,将 C 加入队列,将 C 变成灰色,当整个并发标记结束,会进入 Remark 阶段,会STW,将 C 变成黑色。

4.4.4 JDK 优化

(1)JDK 8u20 字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表

(2) JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark 默认启用

(3) JDK 8u60 回收巨型对象

  • 一个对象大于 region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生
  • 代垃圾回收时处理掉

(4) JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

五、垃圾回收调优

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io
5.2 确定目标
  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC
  • ParallelGC
  • Zing
5.3 最快的 GC

答案是不发生 GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表 limit n”)
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小 16 Integer 24 int 4
    • 是否存在内存泄漏?
      • static Map map =
      • 软引用
      • 弱引用
      • 第三方缓存实现
5.4 新生代调优
  • 新生代的特点

    • 所有的 new 操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价是零
    • 大部分对象用过即死
    • Minor GC 的时间远远低于 Full GC
  • 越大越好吗?
    -Xmn
    Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.

  • 新生代能容纳所有【并发量 * (请求-响应)】的数据

  • 幸存区大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

-XX:MaxTenuringThreshold=threshold //调整阈值
-XX:+PrintTenuringDistribution
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
...
5.5 老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经很OK了,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
    - -XX:CMSInitiatingOccupancyFraction=percent
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值