JVM笔记2--垃圾收集算法

1、如何确认哪些对象“已死”

在上一篇文章中介绍到Java内存运行时的各个区域。其中程序计数器虚拟机栈本地方法栈3个区域随着线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作。每个栈帧中分配多少内存基本上在类确定下来时就是已知的。由于这几个区域有随线程而生,随线程而灭的特性,所以不需要考虑这三个区域的内存回收问题。所以需要回收的内存区域就只有堆和方法区两个区域。
那么如何确定哪些对象“已死”了呢?
有两种方式确定哪些对象对于Java虚拟机来说是可以进行回收的。一个是引用计数法算法,另一个是可达性分析算法

1.1、引用计数算法

引用计数算法顾名思义就是在对象中添加一个引用计数器,当有一个地方引用到这个对象时,计数器的值就加一;当引用失效时计数器的值就减一任何时刻,当计数器的值为零时,表示没有地方引用到此对象,说明此对象时可以被JVM回收的
从客观上来说,引用计数器算法虽然占用了一些额外的内存空间进行计数,但是它的原理简单,判定效率高,在大多数情况下它都是一个不错的算法。但是引用计数算法无法清理掉循环引用的对象

1.2、可达性分析算法

在当前主流的商用程序语言的内存管理子系统都是通过可达性分析算法来判定对象是否存活。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这个节点开始,根据引用关系向下搜索,搜索过程中所走的路径称为**“引用链”,如果一个对象到GC Roots没有引用链**,或者说是从GC Roots到对象不可达时,则证明此对象是可回收对象。
在Java体系里固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如,各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象。比如Java类的引用类型静态变量
  • 在方法区中常量引用的对象。比如字符串常量池里的引用。
  • 在本地方法栈中JIN(即Native方法)引用的对象
  • Java虚拟机内部引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExecption)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调。本地代码缓存等

2、引用

无论通过引用计数算法还是可达性分析算法判断对象是否存活都和“引用”离不开关系。在java中对引用做了一下几种定义:

  • 强引用:是指在代码中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。任何情况下只要强引用关系还存在垃圾收集器都不会回收掉被引用的对象
  • 软引用:是用来描述一些还有用,但非必须的对象。被软引用关联着的对象,在系统将要发生内存溢出异常前会把这些对象列入可回收范围之中进行第二次回收如果这次回收还没有足够的内存,则会抛出异常。在JDK中提供SoftReference来实现软引用。
  • 弱引用:也用来描述那些非必须的对象。但是它的强度比软引用弱一些,被弱引用关联的对象,只能存活到下一次进行垃圾收集为止。当垃圾收集器开始工作,无论当前内存是否足够都会回收掉被弱引用关联的对象。JDK中提供WeakReference来实现弱引用。
  • 虚引用:也称为**“幽灵引用”或者“幻影引用”,它是最弱****的一种引用关系**。一个对象是否存在虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的目的就是在这个对象内收集器回收时能收到一个通知。在JDK中通过实现PhantomReference来显示虚引用。

3、生存还是死亡

即使在可达性分析算法中判定对象时不可达的,对象也不是“非死不可”的,这时候他们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那他将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。加入对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用,那么虚拟机将这两种情况视为**“没有必要执行”
如果对象被判定为
需要执行finalize()方法**,那么对象将被放置在一个名为F-Queue的队列中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程执行他们的finalize()方法。这里的“执行”是指虚拟机会触发这个方法开始运行,并不保证一定等待它运行结束。这样做得原因是避免一个对象的finalize()方法执行缓慢,或者产生死循环,不会导致F-Queue对象中的其他对象一直等待或者内存回收子系统的崩溃
finalize()方法是对象逃脱死亡的最后一次机会,稍后收集器会对F-Queue队列上的对象进行第二次小规模标记,如果对象想在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可。比如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移出“即将回收”的集合;如果对象这时候还没有逃离,那么基本上就真的被回收。
值得注意的是,任何一个对象的finalize()方法只会被系统****自动调用一次如果对象面临下一次回收,它的finalize()方法不会被再次执行。示例代码如下:

public class FinalizeEscapeGC {

    private static FinalizeEscapeGC SAVE_HOKE=null;

    private void alive(){
        System.out.println("yes, i am still alice:");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOKE=this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOKE=new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOKE=null;
        System.gc();
        //由于finalize优先级较低,所以先暂定5s
        Thread.sleep(500);
        if (SAVE_HOKE!=null){
            SAVE_HOKE.alive();
        }else {
            System.out.println("no, i am dead:");
        }

        //这段代码和上面一样,但是却拯救失败
        SAVE_HOKE=null;
        System.gc();
        //由于finalize优先级较低,所以先暂定5s
        Thread.sleep(500);
        if (SAVE_HOKE!=null){
            SAVE_HOKE.alive();
        }else {
            System.out.println("no, i am dead:");
        }

    }
}

image.png
可以看到两段相同的代码,一次对象自救成功,而一次失败。这也就能得出对象的finalize方法,只会被虚拟机自动执行一次。

4、方法区的垃圾回收

方法区中并不是一定存在垃圾收集行为,《Java虚拟机规范》中提到过可以不再方法区中实现垃圾收集。事实上也存在方法区没有垃圾收集的垃圾收集器,比如JDK11中的ZGC收集器。方法区中是否存在垃圾收集行为,取决于垃圾收集器的实现。
方法区的垃圾收集主要是两部分内容:废弃的常量不再使用的类型。回收废弃的常量和回收Java堆中的对象非常类似。也是通过判断常量是否存在其他对象引用此常量来进行是否清除操作。
判断一个常量是否“废弃”比较简单,但是判断一个类型是否属于“不在被使用的类”的条件就比较苛刻。需要同时满足一下三个条件:

  • 该类所有实例都被回收,也就是java堆中不存在该类于任何派生子类的实例。
  • 加载该类的类加载器已被回收
  • 该类对应得java.lang.Class对象没有在任何地方被引用,即无法在任何地方通过反射访问该类的方法

Java虚拟机允许满足上述三个条件的类型被回收,这里只是说的“允许”,而不是和对象一样没有了对象引用就一定被回收。关于是否被回收HotSpot虚拟机提供了**-Xnoclassgc**参数进行控制还可以通过-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类的加载和卸载信息。

5、垃圾收集算法

当前商业垃圾收集器大都遵循“分代收集”的理论进行设计,分代收集名为理论实际上是一套大多数程序运行实际情况的经验法则,它建立在以下两个分代假说之上:

  • 弱分代假说绝大多数对象都是朝生夕灭的
  • 强分代假说熬过越多次垃圾收集过程的对象就越难消灭

以上两个分代假说奠定了多款常用垃圾收集器的一致设计原则:收集器应该讲Java堆分为不同的区域,然后将回收对象根据年龄(年龄即是对象熬过垃圾收集过程的次数)分配到不同的内存区域中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭的,那么每次垃圾收集过程只需关注少量对象的保留问题即可。而不用关心那些大量将被回收的对象。
由此才有了“Minor GC”,“Major GC”,“Full GC”这样的回收类型划分。还有不同的垃圾回收算法:“标记-复制算法”,“标记-整理算法”,“标记-清除算法”。
java堆一般被分为“新生代”,“老年代”两个部分,顾名思义,在“新生代”中每次发生垃圾收集都会有大批对象死去。而每次回收存活的少量对象,将会逐步放到“老年代”中。
PS:分代收集存在一个明显的问题:即使对象并不是孤立的,对象之间会存在****跨代引用
因此为了解决上述问题,对分代收集理论增加了第三条经验法则:

  • 跨代引用假说跨代引用相对于同代引用仅占少数

这其实是可根据前两条假说推理出的隐含推论:存在相互引用关系的两个对象,是应该倾向于同时存在或者同时消亡的。比如,如果某个新生代的对象存在跨代引用,由于老年代对象难以消亡,代引用会使得新生代对象在垃圾收集过程中得以存活,进而在年龄增长之后晋升到“老年代”中,这样跨代引用就不存在了。
Java堆中垃圾收集类型可以分为两大类:

  • 部分收集(Partial GC)指目标不是完整的收集整个java堆的垃圾收集器。其中又分为:
    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集器
    • 老年代收集(Major GC/Old GC):指目标只是老年代的收集器。目前只有CMS收集器会有单独收集“老年代”的行为。
    • 混合收集(Mixed GC):指目标是收集整个新生代和部分老年代的垃圾收集器。目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集器。

5.1、标记-清除算法

最早出现也是最基础的垃圾收集算法就是标记-清除算法。其分**“标记”“清除”两个阶段“:首先标记处哪些对象是要回收的,在标记完成后,统一回收掉所有被标记的对象。
虽然标记-清除算法是最基础的,但是其有两个
缺点**:

  • 执行效率不稳定:如果Java堆中有大量的对象需要回收,就需要进行大量的标记和清除操作,导致标记和清除的效率随着对象的增多而降低
  • 内存空间碎片化:标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致在分配较大对象时无法找到连续的内存空间而不得不提前执行另一次垃圾收集行为

5.2、标记-复制算法

为了解决标记-清除算法中面对大量可回收对象执行效率低问题,有了标记-复制算法:它将可用内存分为大小相等的两块每次只使用其中一块。这一块用完了,就将还存活的对象复制到另一块上,然后在将用过的内存空间一次清理掉如果内存中有大量的对象存活,那么这种算法将产生大量的内存间复制开销。如果是少量对象存回的情况,算法需要复制的就是极少数的存活对象。而且每次都是针对整个半区的内存进行回收,分配内存也不用考虑空间碎片问题。只需要移动堆顶指针按需分配内存即可。
这样实现简单,运行高效,但是缺陷也明显:可用内存缩小为原来的一半。
IBM公司对新生代“朝生夕灭”的特点作了更量化的诠释:新生代中的对象有****98%**熬不过一轮垃圾收集。因此不需要按照1:1的比例来换分新生代的内存空间
所以基于上述特点对标记-复制算法进行了改进:将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存
只是用Eden和其中一块Survivor**。此类方式被称为**“Apple式回收”发生垃圾收集时将Eden和Survivor中任然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉用过的Eden和Survivor空间**。
HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,也即每次新生代可用内存空间为整个新生代的90%(Eden的80%加上一个10%的Survivor),只有一个Survivor空间,10%的新生代会被浪费掉。
由于98%的对象被回收是“普通场景”下测得的数据。因此就会存在特殊情况下有超过10%的对象存活。因此“Apple 式回收”有一个“逃生门”的安全设计,就是当Survivor空间不足以容纳一次Minor GC之后存活的对象,就需要依赖其他内存区域(实际上大多都是老年代)进行分配担保将对象直接存放到老年代

5.3、标记-整理算法

标记-复制算法在对象存活率较高时就要进行比较多的复制操作效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保。因此老年代不直接选用此种算法
针对老年代对象的死亡特征,提出了一种针对性的标记-整理算法,其中标记和标记-清除算法中的标记操作一样,只是后续不是直接进行对象的清除,而是将存活的对象移动到一端,然后直接清理掉边界以外的内存
标记-清除算法和标记-整理算法的区别就在于前者是非移动式的回收算法后者是移动式的回收算法。是否移动存活对象是一项优缺点并存的风险决策:
1、如果移动存活对象,尤其是老年代对象每次回收都有大量对象存活的区域。有以下缺点:

  • 移动存活对象并更新所有引用这些对象的地方将是一种极为负重的操作
  • 而且这种对象移动操作必须停止所有的用户应用程序才行。即**“Stop The World”**

2、如果和标记-清除算法那样完全不考虑移动和整理存活对象的话。那么为了解决空间碎片化问题就只能依赖更为复杂的内存分配和内存访问器来解决。比如**“分区空闲分配链表”。但是这样的话对系统的吞吐量有较大的影响。
3、还有一种“和稀泥”的做法就是
让虚拟机平时多数时间都采用标记-清除算法**,暂时容忍内存碎片的的存在直到内存空间的碎片化达到影响对象分配时在采用标记-整理算法收集一次以获得规整的内存空间。前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/593204.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VMvare如何更改虚拟机内共享文件夹的挂载点

更改虚拟机内共享文件夹的路径 进入目录 /etc/init.d ,并找到vmware-tools文件 里面有配置项 vmhgfs_mnt"/mnt/hgfs" 将引号内的内容更改为你需要挂载的路径,重启即可 注意挂载的路径不能是 “/”,必须根目录下的某个文件夹,或者其子文件夹 …

定时器编程前配置和控制LED隔一秒亮灭

1.配置定时器 0 工作模式16位计时 2.给初值,定一个10ms出来 3.开始计时

环形链表的判断方法与原理证明

(题目来源:力扣) 一.判读一个链表是否是环形链表 题目: 解答: 方法:快慢指针法 内容:分别定义快慢指针(fast和slow),快指针一次走两步,慢指…

物体检测:如何检测小物体?

原文地址:https://medium.com/voxel51/how-to-detect-small-objects-cfa569b4d5bd 2024 年 4 月 22 日 物体检测是计算机视觉的基本任务之一。在高层次上,它涉及预测图像中物体的位置和类别。最先进的(SOTA)深度学习模型&#x…

3031087 -“无数据”:物料不显示在 MRP 应用中

症状 使用其中一个 MRP 应用(监控物料覆盖范围、管理物料覆盖范围、监控外部需求等)时无法找到物料。 用户在搜索过滤器时会收到错误消息“无数据”。 “本 KBA 中的图像/数据来自 SAP 内部系统、示例数据或演示系统。任何与真实数据相似的都是完全巧…

Apache反代理Tomcat项目,分离应用服务器和WEB服务器

项目的原理是使用单独的机器做应用服务器,再用单独的机器做WEB服务器,从网络需要访问我们的应用的话,就会先经过我们的WEB服务器,再到达应用程序,这样子的好处是我们可以保护应用程序的机器位置,同时还可以…

R语言中,查看经安装的包,查看已经加载的包,查看特定包是否已经安装,安装包,更新包,卸载包

创建于:2024.5.4 R语言中,查看经安装的包,查看已经加载的包,查看特定包是否已经安装,安装包,更新包,卸载包 文章目录 1. 查看经安装的包2. 查看已经加载的包3. 查看特定包是否已经安装4. 安装包…

java发送请求-http和https

http和https区别 1、http是网络传输超文本协议,client---- http------ server 2、httpshttpssl证书,让网络传输更安全 ,client---- httpssl------ server 3、ssl证书是需要客户端认可的,注意官方证书和jdk生成的证书的用户来使…

实现批量自动文本标注(输出标签)代码复现

一:项目地址: IDEA-Research/Grounded-Segment-Anything: Grounded-SAM: Marrying Grounding-DINO with Segment Anything & Stable Diffusion & Recognize Anything - Automatically Detect , Segment and Generate Anything (github.com) 二…

3.SpringSecurity基本原理

SpringSecurity本质是一个过滤器链。十多个过滤器构成一个过滤器链。 这些过滤器在项目启动就会进行加载。每个过滤器执行放行操作才会执行下一个过滤器。 常见过滤器 FilterSecurityInterceptor 是一个方法级的权限过滤器,基本位于过滤器链的最底部。 Excepti…

内核workqueue框架

workqueue驱动的底半部实现方式之一就是工作队列,作为内核的标准模块,它的使用接口也非常简单,schedule_work或者指定派生到哪个cpu的schedule_work_on。 还有部分场景会使用自定义的workqueue,这种情况会直接调用queue_work和qu…

sql 中having和where区别

where 是用于筛选表中满足条件的行,不可以和聚类函数一起使用 having 是用于筛选满足条件的组 ,可与聚合函数一起使用 所以having语句中不能使用select中定义的名字

QT:QT与操作系统

文章目录 信号槽与事件 信号槽与事件 在之前的信号槽中,已经有了一个基本的认识,那么对于QT中事件的理解其实就非常的类似,当用户进行某种操作的时候,就会触发事件,去执行一些对应的方法 QT对于事件又进行了封装&…

Lucene从入门到精通

**************************************************************************************************************************************************************************** 1、概述 【1】入门:作用、有点与缺点 【2】应用:索引、搜索、fie…

【软件开发规范篇】JAVA后端开发编程规范

作者介绍:本人笔名姑苏老陈,从事JAVA开发工作十多年了,带过大学刚毕业的实习生,也带过技术团队。最近有个朋友的表弟,马上要大学毕业了,想从事JAVA开发工作,但不知道从何处入手。于是&#xff0…

Python数据分析案例43——Fama-French回归模型资产定价(三因子/五因子)

案例背景 最近看到要做三因子模型的同学还挺多的,就是所谓的Fama-French回归模型,也就是CAMP资本资产定价模型的升级版,然后后面还升级为了五因子模型。 看起来眼花缭乱,其实抛开金融资产定价的背景,从机器学习角度来…

04_jvm性能调优_并行收集器介绍

并行收集器(此处也称为吞吐量收集器)是类似于串行收集器的分代收集器。串行和并行收集器之间的主要区别在于并行收集器具有多个线程,用于加速垃圾回收过程。 通过命令行选项-XX:UseParallelGC 可启用并行收集器。默认情况下,使用…

消息队列与信号量(基本概念及操作接口介绍)

一、消息队列 基本概念 System V消息队列是Unix系统中一种进程间通信(IPC)机制,它允许进程互相发送和接收数据块(消息) 操作系统可以在内部申请一个消息队列,可以让不同的进程向消息队列中发送数据块&…

Linux Systemd基础教程

一、什么是systemd? systemd是Linux系统的一套基本构建模块。它提供了一个系统和服务管理器,作为PID 1运行并启动系统的其余部分。 systemd提供积极的并行化功能,使用套接字和D-Bus激活来启动服务,提供按需启动守护进程&#xf…

《自动机理论、语言和计算导论》阅读笔记:p352-P401

《自动机理论、语言和计算导论》学习第 12 天,p352-P401总结,总计 50 页。 一、技术总结 1.Turing Machine ™ 2.undecidability ​ a.Ld(the diagonalization language) 3.reduction p392, In general, if we have an algorithm to convert insta…