Day2_JVM-垃圾回收

本文详细解析了垃圾回收的基本原理、引用计数和可达性分析,介绍了四种引用的区别,涵盖分代垃圾回收、各种垃圾回收器(如Serial、ParNew、CMS、G1)的工作原理、参数调整及新特性,如字符串去重和类卸载。最后提供了GC调优的策略和案例分析。

一、垃圾回收

1.如何判断对象可以回收

1)引用计数法
当一个对象被引用时,就让引用对象的值加一;当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。
这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时(A引用B,B也引用了A),两个对象的计数都为1,导致两个对象都无法被释放。

这里是引用

2)可达性分析算法

  • JVM 中的垃圾回收器是通过可达性分析来探索所有存活的对象的。

可达性分析算法:扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收


哪些可以作为 GC Root 的对象?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的Native方法)引用的对象

2.四种引用

1. 五种引用

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.四种引用的举例

1.软引用案例

学习视频:P54 07_软引用_应用

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

    public static int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        method2();
    }

    // 设置 -Xmx20m , 演示堆内存不足,
    public static void method1() throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();

        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();
    }

    // 演示 软引用
    public static void method2() throws IOException {
        ArrayList<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());
        }
    }
}


2.软引用的清理_引用队列
学习视频:P55 08_软引用_引用队列

// 演示 软引用 搭配引用队列
    public static void method3() throws IOException {
        ArrayList<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[]> ref : list) {
            System.out.println(ref.get());
        }
    }

3.弱引用_实例
学习视频:P56 09_弱引用

public class Code_09_WeakReferenceTest {

    public static void main(String[] args) {
//        method1();
        method2();
    }

    public static int _4MB = 4 * 1024 *1024;

    // 演示 弱引用
    public static void method1() {
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 10; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
            list.add(weakReference);

            for(WeakReference<byte[]> wake : list) {
                System.out.print(wake.get() + ",");
            }
            System.out.println();
        }
    }

    // 演示 弱引用搭配 引用队列
    public static void method2() {
        List<WeakReference<byte[]>> list = new ArrayList<>();
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for(int i = 0; i < 9; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
            list.add(weakReference);
            for(WeakReference<byte[]> wake : list) {
                System.out.print(wake.get() + ",");
            }
            System.out.println();
        }
        System.out.println("===========================================");
        Reference<? extends byte[]> poll = queue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }
        for(WeakReference<byte[]> wake : list) {
            System.out.print(wake.get() + ",");
        }
    }

}

二、垃圾回收算法

1.标记—清除

这里是引用

2.标记—整理

这里是引用

3.复制

这里是引用
这里是引用
将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

三、分代垃圾回收

上一次学习JVM是2021年07月06日左右,经过了一个多月的暑期实习,2021年08月14日,今天再次捡起JVM,也许做笔记的风格都已经大大改变了。

长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):

在这里插入图片描述

1)、回收流程

新创建的对象都被放在了新生代的伊甸园中:

在这里插入图片描述

在这里插入图片描述

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO

在这里插入图片描述

伊甸园中不需要存活的对象清除:

在这里插入图片描述

交换FROM和TO

在这里插入图片描述

同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:

流程相同,仍需要存活的对象寿命+1:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区FROM中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了)

在这里插入图片描述

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:

在这里插入图片描述

如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:

在这里插入图片描述

小结:

  • 新创建的对象首先会被分配在伊甸园区域。
  • 新生代空间不足时,触发Minor GC,伊甸园和 FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命+1,并且交换FROM和TO。
  • Minor GC会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
  • 当对象寿命超过阈值15时,会晋升至老年代。
  • 如果新生代、老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。

2)、相关VM参数

参考文章:JVM常用内存参数配置

在这里插入图片描述

3)、GC 分析

大对象处理策略:

当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

线程内存溢出:

某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。

这是因为当一个线程抛出OOM异常后它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。


四、垃圾回收器

相关概念

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间)),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

下面来了解一下垃圾回收器的分类:

在这里插入图片描述

1)、串行

  • 单线程
  • 适用场景:内存较小,个人电脑(CPU核数较少)。

串行垃圾回收器开启语句:-XX:+UseSerialGC = Serial + SerialOld

Serial:表示新生代,采用复制算法SerialOld:表示老年代,采用的是标记整理算法

在这里插入图片描述

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。

因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。


① Serial 收集器

Serial(新生代)收集器是最基本的、发展历史最悠久的收集器:

特点:*单线程、简单高效(与其他收集器的单线程相比),采用*复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程**,直到它结束(Stop The World)。


② ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本:

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题


③ Serial Old 收集器

Serial Old是Serial收集器的老年代版本:

特点:同样是单线程收集器,采用标记-整理算法


2)、 吞吐量优先

吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间)。
100分钟里面,我尽量让它所有执行垃圾回收的总时间不超过1分钟,保证吞吐量最大,这就是吞吐量优先

  • 多线程
  • 适用场景:堆内存较大,多核CPU
  • 单位时间内,让STW(stop the world,停掉其他所有工作线程)时间最短
  • JDK1.8默认使用的垃圾回收器
// 1.吞吐量优先垃圾回收器开关:(默认开启)
-XX:+UseParallelGC~-XX:+UseParallelOldGC
// 2.采用自适应的大小调整策略:调整新生代(伊甸园 + 幸存区FROM、TO)内存的大小
-XX:+UseAdaptiveSizePolicy  
// 3.调整吞吐量的目标:吞吐量 = 垃圾回收时间/程序运行总时间
-XX:GCTimeRatio=ratio
// 4.垃圾收集最大停顿毫秒数:默认值是200ms
-XX:MaxGCPaiseMillis=ms
// 5.控制ParallelGC运行时的线程数
-XX:ParallelGCThreads=n
12345678910

在这里插入图片描述

① Parallel Scavenge 收集器

与吞吐量关系密切,故也称为吞吐量优先收集器:

特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)。

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)。

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间。
  • XX:GCRatio 直接设置吞吐量的大小。

② Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本:

特点:多线程,采用标记-整理算法(老年代没有幸存区)。


3)、 响应时间优先

  • 多线程
  • 适用场景:堆内存较大,多核CPU
  • 尽可能单次STW时间变短(尽量不影响其他线程运行)

虚拟机参数:

// 开关:
-XX:+UseConMarkSweepGC~-XX:+UseParNewGC~SerialOld  
// ParallelGCThreads=n并发线程数 
// ConcGCThreads=threads并行线程数
-XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
// 执行CMS垃圾回收的内存占比:预留一些空间保存浮动垃圾
-XX:CMSInitiatingOccupancyFraction=percent
// 重新标记之前,对新生代进行垃圾回收
-XX:+CMSScavengeBeforeRemark
123456789

这里是引用
在这里插入图片描述

在这里插入图片描述

① CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器:

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片。

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。

CMS收集器的运行过程分为下列4步:

  • 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
  • 并发清除:对标记的对象进行清除回收。

CMS收集器的内存回收过程是与用户线程一起并发执行的。


4)、 G1回收器

定义

Garbage First,JDK 9以后默认使用,而且替代了CMS 收集器:

img

适用场景

  • 同时注重吞吐量和低延迟(响应时间)。
  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域。
  • 整体上是标记-整理算法,两个区域之间是复制算法。

相关参数:JDK8 并不是默认开启的,需要参数开启:

// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time
123456
① G1垃圾回收阶段

在这里插入图片描述

新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)。

② Young Collection 新生代垃圾回收

分区算法region

分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间。

E:伊甸园 S:幸存区 O:老年代

  • 会触发STW:

在这里插入图片描述

垃圾回收时,会把伊甸园(E)的幸存对象复制到幸存区(S):

在这里插入图片描述

当幸存区(s)中的对象也比较多触发垃圾回收,且幸存对象寿命超过阈值时,幸存区(S)中的一部分对象(寿命达到阈值)会晋升到老年代(O),寿命未达到阈值的会被再次复制到另一个幸存区(S):

在这里插入图片描述

③ Young Collection + CM 新生代垃圾回收和并发标记

CM:并发标记!

  • 在 Young GC 时会对 GC Root 进行初始标记。
  • 老年代占用堆内存的比例达到阈值时,进行并发标记(不会STW),阈值可以根据用户来进行设定:
-XX:InitiatingHeapOccupancyPercent=percent // 默认值45%
1

在这里插入图片描述

这里是引用

④ Mixed Collection 混合收集

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

  • 最终标记
  • 拷贝存活
//  用于指定GC最长的停顿时间
-XX:MaxGCPauseMillis=ms
12

:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的最大停顿时间,会根据最大停顿时间,有选择的回收最有价值的老年代(回收后,能够得到更多内存)。

在这里插入图片描述

G1在老年代内存不足时(老年代所占内存超过阈值):

  • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理。
  • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC。

5)、对细节的进一步解释

上面中涉及到一些细节,需要我们进一步解释

① Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集,需要分2种情况,看红体字
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集,需要分2种情况,看红体字

② 新生代垃圾回收时的跨代引用问题:

回顾新生代垃圾回收的过程:要先找到根对象,然后根对象可达性分析,再找到存活对象,存活对象进行复制,复制到幸存者区。问题就来了,我们要找新生代的根对象时有一部分根对象是来自老年代的,老年代里的对象一般很多的,难度我们要一个一个遍历去找那个根对象吗?

  • 新生代回收的跨代引用(老年代引用新生代)问题:

在这里插入图片描述

  • 卡表与Remembered Set
    • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡:
      • 脏卡:O(老年代)被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡。
  • 我们要找新生代的根对象时有一部分根对象是来自老年代的,有了这样的标记就不用遍历整个老年代了,直接看E里面Remembered Set的数据。

在这里插入图片描述

③ Remark重新标记阶段

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
但是在并发标记过程中,处理A和B时发现它们被强引用着,所以需要保留。黑色的A和B表示已经被处理完且需要保留的对象。在刚刚的处理A时A并没有引用C,但是在处理完A后A才引用了C,导致处理C时并不知道它被引用就垃圾回收掉了。这时就会用到remark重新标记。

重新标记过程如下:

  • 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态。
  • 并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,处理它时就会对它进行保留。

在这里插入图片描述

6)、新特性

① JDK 8u20 字符串去重
String s1 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
String s2 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}

s1和s2本来一样,这种重复的字符串也是很占用内存的,所以 在JDK 8u20中有了字符串去重

字符串去重开启指令 -XX:+UseStringDeduplication

案例分析:

  • 将所有新分配的字符串(底层是char[])放入一个队列。

  • 当新生代回收时,G1并发检查是否有重复的字符串。

  • 如果字符串的值一样,就让他们引用同一个字符串对象

在这里插入图片描述

② JDK 8u40 并发标记类卸载

在所有对象经过并发标记阶段以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用时,则卸载它所加载的所有类。

并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark 默认启用。

③ JDK 8u60 回收巨型对象
  • H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
  • G1不会对巨型对象进行拷贝(拷贝代价太大了,浪费时间)。
  • 回收时被优先考虑(巨型对象回收了那不是释放了很多内存)。
  • G1会跟踪老年代所有引用,如果老年代引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

在这里插入图片描述

巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~

④ JDK 9 并发标记起始时间的动态调整

还记得下面红色字体内容吗?
在这里插入图片描述

  • 并发标记必须在堆空间占满前完成,否则就退化为 Full GC(Full GC代价很大的)。
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 设置阈值,默认是 45%
  • JDK 9 可以动态调整:(目的是尽可能的避免并发标记退化成 Full GC)
    • -XX:InitiatingHeapOccupancyPercent:用来设置初始阈值。
    • 在进行垃圾回收时,会进行数据采样并动态调整阈值
    • 总会添加一个安全的空挡空间,用来容纳那些浮动的垃圾。
⑤ JDK 9 更高效的回收
  • JDK 9 对垃圾回收进行了 250+ 项的增强,180+ 项的bug修复。具体的不说了,有缘再见把4吧

五、GC 调优

略(高阶部分专门会讲)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BlackTurn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值