首页 > 编程语言 >【Java】JVM垃圾收集器深入解析:原理与实践

【Java】JVM垃圾收集器深入解析:原理与实践

时间:2024-09-25 21:18:54浏览次数:9  
标签:Java 收集器 对象 标记 算法 引用 JVM 垃圾

目录

一、判断对象是否存活

1. 引用计数算法

2. 可达性计数算法

3. Java中的四种引用 

3.1 强引用(Strong Reference)

3.2 软引用(Soft Reference)

3.3 弱引用(Weak Reference)

3.4 虚引用(Phantom Reference)

3.5 小结

二、垃圾收集算法

1. 分代收集理论

1.1 分代存储

1.2 分代收集

2. 垃圾收集算法

2.1 标记-清除算法

2.2 标记-复制算法

2.3 标记-整理算法

3. 综上所述

三、垃圾收集器

1. Serial收集器(新生代)

2. Serial Old收集器(老年代)

3. ParNew收集器(新生代)

4. Parallel Scavenge收集器(新生代)

5. Parallel Old收集器(老年代)

6. CMS收集器(老年代)

7. G1收集器(老年代)

7.1 什么是G1 垃圾收集器

7.2 G1 垃圾收集器的结构

7.3 G1垃圾收集器工作流程

7.4 G1垃圾收集器与CMS垃圾收集器的区别        


一、判断对象是否存活

1. 引用计数算法

        引用计数算法( Reference counting )基本思路:

  • 在对象中添加一个引用计数器
  • 每当有一个地方引用它的时候,计数器就加 +1
  • 每当有一个引用失效的时候,计数器就减-1
  • 计数器的值为 0 的时候,那么该对象就是可被GC回收的垃圾对象

        引用计数算法存在的问题对象循环引用
        a对象引用了 b对象,b 对象也引用了 a对象,a、b 对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是0,所以引用计数算法就无法回收它们。

2. 可达性计数算法

        可达性分析算法( Reachability Analysis )基本思路:通过定义了一系列称为“GC Roots”的根对象作为起始节点集,从 GC Roots 开始,根据引用关系往下进行搜索,查找的路径我们把它称为“引用链"。当一个对象到 GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象,可达性分析算法也是JVM 默认使用的寻找垃圾算法。

        例如:Object 6、Object7、Object 8彼此之前有引用关系,但是没有与"GC Roots"相连,那么就会被当做垃圾所回收。

3. Java中的四种引用 

3.1 强引用(Strong Reference)

        强引用是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

强引用:

Object strongReference = new Object();

        如果强引用对象不使用时,需要弱化从而使 GC 能够回收。

         弱化方式1:
        显式地设置 strongReference 对象为 null,则GC 认为该对象不存在引用,这时就可以回收这个对象。但是,具体什么时候收集这要取决于GC算法。例如, strongReference 是全局变量时,就需要在不用这个对象时赋值为 null,因为强引用不会被垃圾回收。

强引用弱化方式1:

strongReference = null;

        应用场景:在 ArrayList 集合类中定义 elementData 数组,在调用 clear()方法清空集合元素时,将每个数组元素被赋值为 null 。目的是为了将内存数组中存放的引用类型进行内存释放,可以及时释放内存。不选择将elementData=null ,是为了避免在后续调用 add()等方法添加新元素时,需要进行内存的重新分配。

        弱化方式2:让对象超出作用范围

        应用场景:应用场景:在一个方法的内部有一个强引用,这个引用保存在 stack 栈中(GC Root),而真正的引用对象( Object )保存在堆中。当这个方法运行完成后,就会退出方法栈,则这个对象会被回收。

强引用弱化方式2:

public void test(){

        Object strongReference = new Object();

        // 省略其他操作

}

3.2 软引用(Soft Reference)

        如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。所以,软引用可用来实现内存敏感的高速缓存。

        创建软引用,可以使用 SoftReference

        // 强引用
        String strongReference = new String("abc");

        // 软引用
        String str = new String("abc");
        SoftReference<String> softReference = new SoftReference<>(str);

        // 访问软引用
        softReference.get();

         软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc()方法只是起通知作用,最终何时回收,由JVM 决定。

        所以,当内存不足时, JVM 首先将软引用中的对象引用置为 null,然后通知垃圾回收器进行回收。

    public static void main(String[] args) {
        // 软引用
        String str = new String("abc");
        SoftReference<String> softReference =new SoftReference<>(str);

        str = null;

        // Notify GC
        System.gc();

        try {
            byte[] buff1 = new byte[900000000]; // 内存充沛
//            byte[] buff2 = new byte[900000000]; // 内存不足
//            byte[] buff3 = new byte[900000000];
//            byte[] buff4 = new byte[900000000];
//            byte[] buff5 = new byte[900000000];
//            byte[] buff6 = new byte[900000000];
        }catch(Error e){
            e.printStackTrace();
        }

        System.out.println(softReference.get());  // abc或null
    }

        应用场景:短视频APP中的视频缓存,后退时,显示的短视频内容是重新进行请求还是从缓存中取出呢?

        (1)如果一个短视频在播放结束时,就进行内容的回收,则后退查看前面播放的短
视频时,需要重新请求。

        (2)如果将播放过的短视频存储到内存中,会造成内存的开销,甚至会造成内存溢
出。
        
        此时,可以使用软引用解决这个实际问题。

3.3 弱引用(Weak Reference)

        只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

        创建弱引用,使用 WeakReference

        String str = new String("abc");

        WeakReference<String> weakReference = new WeakReference<>(str);
        str = null;

        System.gc();

        // 一旦发生GC,弱引用一定会被回收
        System.out.println(weakReference.get());
3.4 虚引用(Phantom Reference)

        虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收。

        虚引用,主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知。

        在 JDK1.2之后,用PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get()方法,而且它的 get()方法仅仅是返回一个 null ,也就是说将永远无法通过虚引用来获取对象

3.5 小结

二、垃圾收集算法

1. 分代收集理论

        目前主流 JVM 虚拟机中的垃圾收集器,都遵循分代收集理论:

  • 弱分代:绝大多数对象都是朝生夕灭
  • 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡

        按照分代收集理论设计的“分代垃圾收集器”,所采用的设计原则:收集器应该将 Java 堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储。

1.1 分代存储

        如果一个区域中大多数对象都是朝生夕灭(新生代),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。

        如果一个区域中大多数对象都是难以回收(老年代),那么把它们集中放在一起,JVM 虚拟机就可以使用较低的频率,来对这个区域进行回收。这样设计的好处是,兼顾垃圾收集的时间开销和内存空间的有效利用。

1.2 分代收集

   1.2.1 堆区按照分代存储的好处:

        在 Java 堆区划分成不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些区域,所以才有MinorGc、MajorGC、FullGC 等垃圾收集类型划分。
        
        在 Java 堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-复制算法标记-清除算法标记-整理算法等。

   1.2.2 垃圾收集类型划分:

 (1)部分收集( Partial Gc ):没有完整收集整个 Java 堆的垃圾收集,其中又分为:     

  • 新生代收集(Minor Gc/Young Gc)
  • 老年代收集(Major Gc /Old Gc)
  • 混合收集(Mixed Gc ):收集整个新生代和部分老年代的垃圾收集。

(2)整堆收集( Full GC ):收集整个 Java 堆的垃圾收集。

2. 垃圾收集算法

2.1 标记-清除算法

        “标记-清除”算法实现思路:

         该算法分为“标记”和“清除”阶段:从根集合(GC Roots)开始扫描,标记出所有存活对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。


       

2.2 标记-复制算法

        “标记-复制”算法实现思路:

        “标记-复制”收集算法简称复制算法”,为了解决“标记-清除”面对大量可回收对象时执行效率低下的问题。
        
        该算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把已使用的空间一次清理掉。

2.3 标记-整理算法

        “标记-整理”算法实现思路:

        标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向内存空间一端移动,然后直接清理边界以外的内存,这样清理的机制,不会像标记-整理那样留下大量的内存碎片。
        

3. 综上所述

        当前虚拟机的垃圾收集都基于分代收集思想,根据对象存活周期的不同,将内存分为几个不同的区域,在不同的区域使用不同的垃圾收集算法。

        例如: Heap 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

        在新生代中,每次收集都会有大量垃圾对象被回收,所以可以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
        
        在老年代中,对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以选择“标记-清除”或“标记-整理”算法进行垃圾收集。

三、垃圾收集器

1. Serial收集器(新生代)

        Serial (串行)收集器是最基本、历史最悠久的垃圾收集器,采用“标记复制”算法负责新生代的垃圾收集。它是 Hotspot 虚拟机运行在客户端模式下的默认新生代收集器。

        它是一个单线程收集器。它会使用一条垃圾收集线程去完成垃圾收集工作并且它在进行垃圾收集工作的时候,必须暂停其他所有的工作线程("Stop The World"),直到收集结束。

2. Serial Old收集器(老年代)

        Serial Old 收集器同样是一个单线程收集器,采用“标记-整理”算法负责老年代的垃圾收集,主要用于客户端模式下的Hotspot虚拟机使用。

        如果在服务器端使用,它主要有两种用途:       

  • 在 JDK5 及以前版本,与 Parallel Scavenge 收集器搭配使用;
  • 作为 CMS 收集器发生失败时的后备预案。

3. ParNew收集器(新生代)

        ParNew 收集器是一个多线程的垃圾收集器。它是运行在 Server 模式下的虚拟机的首要选择,可以与 Serial Old,CMS 垃圾收集器一起搭配工作,采用“标记-复制”算法。

4. Parallel Scavenge收集器(新生代)

        Parallel Scavenge收集器是也是一款新生代收集器,使用“标记-复制”算法实现的多线程收集器。

        Parallel scavenge 收集器预其它收集器的目标不同, CMS 等其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间。但是 Parallel Scaveng收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

           

5. Parallel Old收集器(老年代)

        Parallel Old 收集器是一个多线程的垃圾收集器,使用“标记-整理”算法,是 Parallel scavenge 收集器的老年代版本。

        在注重吞吐量或者处理器资源较为稀缺的应用场景,都可以优先考虑 Parallel Scavenge 收集器+ Parallel Old 收集器这个收集器组合。      

6. CMS收集器(老年代)

        CMS(Concurrent Mark sweep )收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,是 Hotspot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

        工作流程
        整个过程包括四个步骤:
        1. 初始标记(cMS initial mark):标记-下 GC Roots能直接关联到的对象,速度很快;
        2. 并发标记(CMS concurrent mark):从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
        3. 重新标记(CMS remark):重新标记阶段,是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间长,远远比并发标记阶段时间短
        4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

7. G1收集器(老年代)

7.1 什么是G1 垃圾收集器

        G1( Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器、大容量内存的机器。它不再严格按照分代思想进行垃圾回收。G1 采用局部性收集的设计思路和基于 Region 的内存布局形式。

7.2 G1 垃圾收集器的结构

        G1采用局部性收集的思想,对于堆空间的划分,采用 Region 为单位的内存划分方式:
        
        G1 垃圾回收器把堆划分成 2048 个大小相同的独立区域( Region ),每个 Region 的大小取值范围是 1MB-32MB,且应为 2的 N次幂,即 1MB,2MB,4MB,8MB,16MB,32MB。

        每个 Region 都会代表某一种角色,H、S、E、O。E代表 Eden区,S代表 Survivor 区,H代表的是 Humongous(G1 用来分配大对象的区域,对于 Humongous 也分配不下的超大对象,会分配在连续的N个Humongous 中),剩余的深蓝色代表的是 Old 区,灰色的代表的是空闲的 region 。

        这种思想上的转变和设计,使得G1可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的 Mixed GC模式,即混合GC模式

7.3 G1垃圾收集器工作流程

        初始标记( Initial Marking ):这个阶段仅仅只是标记GC Roots能直接关联到的对象,这阶段需要停顿线程,但是耗时很短。
        并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。
        最终标记( Final Marking ):对用户线程做另一个短暂的暂停,用于处理
并发阶段结束后遗留记录。
        筛选回收( Live Data counting and Evacuation ):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个 Region 来构成会收集,然后把回收的那一部分 Region 中的存活对象==>复制==>到空的
Region中,最后对那些 Region 进行清空。

7.4 G1垃圾收集器与CMS垃圾收集器的区别        

        算法不同:CMS 采用“标记-清除”容易产生内存碎片,执行若干次 GC后进行 1 次碎片整理。 G1 从整体来看是基于“标记-整理”算法实现的收集器从局部(两个 Region 之间)上来看是基于“标记-复制”算法实现。意味着G1垃圾收集器不会产生内存空间碎片,垃圾收集完成后,能提供规整的可用内存不会导致因为大对象分配内存时无法找到连续内存空间而提前触发垃圾收集。

        场景不同:小内存应用上CMS 的表现大概率优于G1,而在大内存应用G1 则能发挥优势。大小内存的参考值分水岭大概在 6GB-8GB中。

标签:Java,收集器,对象,标记,算法,引用,JVM,垃圾
From: https://blog.csdn.net/weixin_71491685/article/details/142530747

相关文章

  • JavaScript中if嵌套assert的方法
    在JavaScript中,通常我们不会直接使用assert这个词,因为JavaScript标准库中并没有直接提供assert函数(尽管在一些测试框架如Jest、Mocha中经常看到)。但是,我们可以模拟一个assert函数的行为,即当某个条件不满足时抛出一个错误。结合if语句进行嵌套判断时,可以在每个需要断言的地方调用这......
  • java项目发布后到Tomcat时,总是带一层路径解决方案
    java项目发布后到Tomcat时,总是带一层路径参考文章:java线上项目访问项目会多一层项目根路径根据参考文章写的这篇文章,部分文章细节有完善和改动在JavaWeb应用中,当你把应用发布到Tomcat时,如果应用的web.xml配置文件中的<context-root>元素被设置成了非根路径,或者你......
  • Java毕业设计:基于Springboo律师事务所预约网站毕业设计源代码作品和开题报告怎么写
     博主介绍:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育和辅导。所有项目都配有从入门到精通的基础知识视频课程,学习后应对毕业设计答辩。项目配有对应开发文档、开题报告、任务书、P......
  • Java毕业设计:基于Springboot网球场地预约网站管理系统毕业设计源代码作品和开题报告怎
     博主介绍:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育和辅导。所有项目都配有从入门到精通的基础知识视频课程,学习后应对毕业设计答辩。项目配有对应开发文档、开题报告、任务书、P......
  • java中的接口
    接口表示一个类额外功能的实现,其作用是为了降低耦合。接口的使用注意事项1.接口中只能存在抽象方法,jvm默认会在方法前使用publicabstract进行修饰,刚学java推荐加上2.类和接口是实现关系可以通过关键字implements实现接口3.当一个具体的类实现一个接口的时候,......
  • JAVA基础:lock锁底层机制
    目录lock锁底层机制乐观锁lock锁底层机制lock锁底层使用的是CAS+AQS,在lock底层有一个计数器,记录锁被获取的状态,起初为0,当被抢占的时候变为1当我们调用lock.lock()方法,就是将状态从0改为1的过程。当我们调用lock,unlock()方法时,就是将状态从1改为0的过程当我们调用......
  • java读取寄存器数据
    一:概述在嵌入式系统或硬件编程中,Java通常不是首选语言,因为它运行在虚拟机上,与硬件层面的交互不够直接。然而,随着Java技术的发展,以及JNA(JavaNativeAccess)等库的出现,使得Java也能进行一些底层操作,包括读取寄存器数据。本文将探讨几种在Java中读取寄存器数据的方法,并提供实际案例。......
  • java读取寄存器数据
    一:概述在嵌入式系统或硬件编程中,Java通常不是首选语言,因为它运行在虚拟机上,与硬件层面的交互不够直接。然而,随着Java技术的发展,以及JNA(JavaNativeAccess)等库的出现,使得Java也能进行一些底层操作,包括读取寄存器数据。本文将探讨几种在Java中读取寄存器数据的方法,并提供实际案例。......
  • 基于Java对数据库的增加和查询操作
     在开始编码前,我们需要先给IDEA配置下面两个jar文件:第一步:我们先进行数据库的连接publicclassDBUtils{/***打开数据库*///优化:让加载器等操作,只做一次publicstaticStringdriver;//驱动地址publicstaticStringurl;//数据库的地......
  • Java中集合泛型的学习
    集合遍历目录集合遍历泛型的基本概念泛型的好处泛型的使用1.泛型类2.泛型接口3.泛型方法Java集合框架中的泛型泛型通配符Java集合泛型是JavaSE1.5(Java5)中引入的一个重要特性,它允许在定义类、接口和方法时指定一个或多个类型参数。这些类型参数在实例化或调用时会被具体的......