JVM学习笔记(二) 垃圾回收

本文深入探讨Java垃圾回收机制,包括可达性分析算法确定对象是否可回收,四种引用类型(强、软、弱、虚引用)及其作用,以及常见的垃圾回收算法(标记清除、标记整理、复制算法)。此外,还介绍了新生代、老年代的概念,CMS并发标记清除收集器和G1垃圾收集器的工作原理,以及如何通过JVM参数调整垃圾回收策略,以优化系统性能。
摘要由CSDN通过智能技术生成

三、垃圾回收

1、如果判断对象可以回收

1)引用计数法

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

在这里插入图片描述

2)可达性分析算法

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找  到该对象,如果找不到,则表示可以回收
  • 可以作为 GC Root 的对象
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中 JNI(即一般说的Native方法)引用的对象
public static void main(String[] args) throws IOException {
        // list是局部变量,是一个引用,放在活动栈桢中,而new 的对象是存放在堆中。
        // 虚拟机栈(栈帧中的本地变量表)中引用的对象。
        ArrayList<Object> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add(1);
        System.out.println(1);
        System.in.read();

        list = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end");
    }

分析的 gc root,找到了 ArrayList 对象,然后将 list 置为null,再次转储,那么 ArrayList 对象就会被回收

3)四种引用

在这里插入图片描述

  1. 强引用:只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  2. 软引用(SoftReference)仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象,可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference)仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身,触发full gc才会全部释放,不是只要gc 就释放
  4. 虚引用(PhantomReference)必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
  5. 终结器引用(FinalReference)无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

 软引用

/**
 * 演示 软引用
 * -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 , 演示堆内存不足,list 强引用 byte[]
    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();
    }

    // 演示 软引用    list --> SoftReference-->byte[],不再直接强引用
    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());
        }
    }
}

method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常堆内存不足,因为mehtod1 中的 list 都是强引用,进行垃圾回收

 method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。但是软引用还存在(null...希望从集合中删除),所以,一般软引用需要搭配一个引用队列一起使用。

// 演示 软引用 搭配引用队列
    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());
        }
    }

在这里插入图片描述

 弱引用

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();
        }
    }

    // 演示 弱引用搭配 引用队列

}

2、垃圾回收算法

1)标记清除算法

将对象占用内存的开始结束地址记录下来,放到一个空闲地址列表中,下次分配新内存时到空闲地址列表中查找即可 (操作系统内存管理,会产生内存碎片)

  • 优点:速度较快
  • 缺点:会产生内存碎片

在这里插入图片描述

2)标记整理算法

与标记清楚区别在整理部分,避免内存碎片问题,将可用对象向前移动,使内存更加紧凑,连续空间增多。整理过程中需要移动对象,速度变慢。

  • 缺点:速度慢
  • 优点:没有内存碎片
  • 在这里插入图片描述

3)复制算法

将内存区化成大小相等的两个区域,TO区域始终为空,将FROM存活的对象复制到TO区域,然后交换FROM和TO的位置

  • 优点:不会有内存碎片
  • 缺点:需要占用两倍内存空间

在这里插入图片描述

 3、分代垃圾回收

在这里插入图片描述

将整个堆内存大的区域划分成了新生代(new generation)、老年代(tenured generation)两块,老年代存放更有价值,存活更久的对象,

  • 新创建的对象首先分配在 Eden 区,即伊甸园区。
  • Eden区空间不足时触发 Minor GC ,Eden 区 和 From 区存活的对象使用 - copy 复制到 To 中,存活的对象年龄加一,然后交换 From To,也就是说复制算法没有涉及到对象的物理地址的变化,只是局部变量的引用地址和物理地址的唡映射关系发生变化,这里很细节,实际变的不是两块物理地址,而是指针引用。每次都把不需要回收和幸存区留下的对象移到To中,然后交换From和To的位置,保证每次Minor GC后幸存区To都为空。总结:Minor GC时应是复制Eden区的幸存对象和幸存区的对象同时复制到To区,再+1年龄后交换
  • Minor GC 会引发 Stop the world,暂停其他线程(停顿现象),由垃圾回收线程完成垃圾回收动作。等垃圾回收结束后,恢复用户线程运行。原因是垃圾回收过程中会牵扯到对象地址的变化,根据原来的地址找会找不到
    当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是 15(4bit)
  • 当老年代空间不足时,会先触发 Minor GC,如果空间仍然不足,那么就触发 Full GC ,STW时间更长!如果此时老年代的空间仍然不足,会触发OutOfMemory内存溢出。

 1)相关 JVM 参数

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

案例一

初始时,设置虚拟机参数 -xmn10m 新生代大小为 10M,new generation 此时统计的是eden space 和 from space 的大小共 9216k(9M),刚开始时Eden区(8M)已经占了24%,这是因为Java程序刚开始会加载一些类,创建一些对象,使用Eden区域。

 发生垃圾回收,未到15次,7M大对象就被放入到老年代中

大对象直接放入到老年代中 

4、垃圾回收器

相关概念(重点)

  • 并行收集(paralleller):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
  • 并发收集(concurrent):指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
  • 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。垃圾回收时间越少,堆越大,吞吐量越大。

1)串行

  • 单线程
  • 适合堆内存较少情况,适合个人电脑
-XX:+UseSerialGC=serial + serialOld

在这里插入图片描述

  1. 安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
  2. Serial 收集器:Serial 收集器是最基本的、发展历史最悠久的收集器,单线程、简单高效(与其他收集器的单线程相比),采用复制算法(回收新生代的垃圾对象)。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停阻塞其他所有的工作线程,直到它结束(Stop The World)!
  3. ParNew 收集器:ParNew 收集器其实就是 Serial 收集器的多线程版本。特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World (STW)问题
  4. Serial Old 收集器:Serial Old 是 Serial 收集器的老年代版本,回收老年代的垃圾对象。特点:同样是单线程收集器,采用标记-整理算法

2)吞吐量优先

  • 多线程
  • 适合堆内存较大的情况,必须是多核 cpu
  • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4
  • 垃圾回收器开启多个回收线程进行垃圾回收,与CPU核数相关。垃圾回收发生时,CPU占用率会立即飙升,达到峰值。

在这里插入图片描述

-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC // 1.8下默认开启并行(暗指多线程)的垃圾回收器
-XX:+UseAdaptiveSizePolicy // 自适应,调整新生代,enden和surviver区的比例
-XX:GCTimeRatio=ratio // 1/(1+ratio),调整垃圾回收时间与总时间的占比,ratio默认值为99,垃圾回收时间不能超过总时间(工作时间)的 1 %,如果达不到目标,ParallelGC会调整堆的大小,一般是增大(可用空间增多,吞吐量增多,垃圾回收的次数就不频繁了,垃圾回收时间下降,但是每次垃圾回收的时间会增多,故而与MaxGCPauseMillis相悖)。
-XX:MaxGCPauseMillis=ms // 200ms
-XX:ParallelGCThreads=n // 设定线程数

-XX:GCTimeRatio=ratio 

        1/(1+ratio),调整垃圾回收时间与总时间的占比,ratio默认值为99(一般设置为19),垃圾回收时间不能超过总时间(工作时间)的 1 %,如果达不到目标,ParallelGC会调整堆的大小,一般是增大(可用空间增多,吞吐量增多,垃圾回收的次数就不频繁了,垃圾回收时间下降,但是每次垃圾回收的时间会增多,故而与MaxGCPauseMillis相悖)。

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

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

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

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

XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
XX:GCTimeRatio=rario 直接设置吞吐量的大小


Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)

3)响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让 STW 的单次时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC(新生代垃圾回收器) ~ SerialOld(concurrent并发)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads(3:1)
-XX:CMSInitiatingOccupancyFraction=percent(进行CMS垃圾回收的时机,老年代内存到80%是就进行垃圾回收)
-XX:+CMSScavengeBeforeRemark

在这里插入图片描述

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

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

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

4)G1 收集器(待解决,有很多疑问)

定义: Garbage First
适用场景:

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

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

-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

G1 垃圾回收阶段 

在这里插入图片描述

Young Collection:对新生代垃圾收集
Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。

1.Young Collection

新生代存在 STW分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!
E:eden,S:幸存区,O:老年代
新生代收集会产生 STW ,新生代采用复制算法复制到幸存区,在工作一段时间超过年龄,幸存区一部分对象晋升到老年代,另一部分对象复制到新的幸存区

在这里插入图片描述

2.Young Collection + CM

在 Young GC (新生代垃圾回收)时会进行 GC Root 的初始化标记
老年代占用堆空间比例达到阈值时,触发进行并发标记(类似CMS,不会STW),由下面的 JVM 参数决定 -XX:InitiatingHeapOccupancyPercent=percent (默认45%,老年代占用堆空间45%)

在这里插入图片描述

3.Mixed Collection

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

  • 最终标记会 STW
  • 拷贝存活会 STW

-XX:MaxGCPauseMills=xxms 用于指定最长的停顿(STW)时间!
问:为什么有的老年代被拷贝了,有的没拷贝?
        筛选回收,根据暂停时间,先回收一部分最不仅要的数据,下一次再回收一部分,即减少了空间,也没有过多耗费时间,因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
在这里插入图片描述

4.Full GC(G1)

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

  • 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
  • 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器          串行的收集,就会导致停顿的时候长。

5.Young Collection 跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题(集合插入新对象,集合在老年代,新对象还在新生代)

在这里插入图片描述

  • 卡表 与 Remembered Set
    • Remembered Set (记忆集,避免遍历个老年代去抄找新生代的引用,提高效率)存在于E中,用于保存新生代对象对应的脏卡
    • 脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过 post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set
    在这里插入图片描述

6.Remark(没听懂)

重新标记阶段
在垃圾回收时,收集器处理对象的过程中,下图表示并发标记时对象的处理状态。

  • 黑色:已被处理,有引用在引用,需要保留存活的
  • 灰色:正在处理中的
  • 白色:还未处理的

在这里插入图片描述

但是在并发标记过程中,B不再引用C,C被处理之后变成白色,没有对象引用,但是用户线程A又引用了C,C的引用发生了改变,C不应该被回收。这时就会用到 remark 。

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

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值