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

news2024/11/16 13:37:33

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收集器面临空间碎片过多时采用的就是这种处理办法

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV多张图片堆叠显示

OpenCV实现多张图片堆叠显示 程序思路效果代码 程序思路 读取两张或多张图片;获取图片尺寸;选择多张图片中较大的宽度和高度建立画布;合并图片到画布; 效果 代码 import cv2 import numpy as np# 读取两张图片 img1 cv2.imrea…

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

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

Endnote X9 20 21如何把中文引用的et al 换(变)成 等

描述 随着毕业的临近,我在写论文时可能会遇到在引用的中文参考文献中出现“et al”字样。有的学校事比较多,非让改成等等,这就麻烦了。 本身人家endnote都是老美的软件,人家本身就是针对英文文献,你现在让改成等等&a…

Fetch的概述和基本使用

03 【Fetch的概述和基本使用】 1.XMLHttpRequest缺点 浏览器提供了原生的AJAX实现类XMLHttpRequest,基于该类实例,我们可以实现在网页上发送AJAX请求到服务端。 但是XMLHttpRequest的设计并不完美,主要体现在以下几个方面: HT…

贪吃蛇(上)Win32API

感谢大佬的光临各位,希望和大家一起进步,望得到你的三连,互三支持,一起进步 个人主页:LaNzikinh-CSDN博客 文章目录 前言一、Win32 API二、地图的绘制和初始化总结 前言 贪吃蛇(也叫做贪食蛇)游…

Noir Dark Mode for Safari:夜间浏览的舒适伴侣

Noir Dark Mode for Safari是一款实用的浏览器插件,它使夜间浏览网页变得更加轻松和舒适。通过自动为访问的每个网站添加暗色模式,Noir减少了用户在暗光环境下浏览网页时可能产生的眼睛疲劳。 Noir的自定义功能允许用户根据自己的喜好调整暗色模式的设置…

配电室智能巡检机器人

近年来,生产过程高度自动化,各工矿企业关键场所需定期巡检维护。但目前巡检主要靠人工,既耗时费力效率又低,且受环境等因素影响,巡检难以全面规范,隐患或问题易被忽视。在此情况下,如何利用现有…

IoTDB 入门教程 基础篇③——基于Linux系统快速安装启动和上手

文章目录 一、前文二、下载三、解压四、上传五、启动六、执行七、停止八、参考 一、前文 IoTDB入门教程——导读 二、下载 下载二进制可运行程序:https://dlcdn.apache.org/iotdb/1.3.1/apache-iotdb-1.3.1-all-bin.zip 历史版本下载:https://archive.…

Linux Systemd基础教程

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

金属表面粗糙度对信号的影响

在进行PCB的传输线设计时,如果希望仿真结果更加贴合于实际的效果,就需要考虑很多的附加因素,比如,真实的叠构参数、介电常数、损耗角正切值、蚀刻因子、金属表面粗糙度、玻纤效应等,在常规的信号仿真中,前三…

Git系列:如何为不同的Git仓库设置不同的配置项?

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

如何配置X86应用程序启用大地址模式(将用户态虚拟内存从2GB扩充到3GB),以解决用户态虚拟内存不够用问题?(项目实战案例解析)

目录 1、概述 2、为什么不直接将程序做成64位的? 3、进程内存不足导致程序发生闪退的案例分析 3.1、问题说明 3.2、将Windbg附加到程序进程上进行动态调试 3.3、动态调试的Windbg感知到了中断,中断在DebugBreak函数调用上 3.4、malloc或new失败的…

新手去做抖音小店,这七点千万别忽视!建议收藏!

大家好,我是电商小V 今天咱们就来详细的说一下新手操作抖音小店的几个通病,想要去做抖音小店的小伙伴千万要注意,一定要避免,不要踩坑, 第一点:新手刚去做抖音小店不赚钱的主要原因不是因为你选择了大类目&…

从零开始Hadoop安装和配置,图文手把手教你,定位错误(已部署成功)

文章目录 时间急的可以看速成,虚拟机和配置方法已给出,提供下载的为一台主节点一台分结点的虚拟机下载,只需进行ip地址更换即可 [现成Hadoop配置,图文手把手交你](https://blog.csdn.net/weixin_52521533/article/details/1328627…

7.string

目录 学库一定会看文档 1.Member functions string介绍 2.迭代器iterator 1.正向迭代器 2.反向迭代器 3.反向const 4.反向const 3.容量capacity 0.size(),capacity(),max_size(),length() 1.扩容机制(vs和Linux g对比) 2.clear(&a…

从电商系统认识数据与数据的存储

相信各位读者一年不知道要多少次通过电商App浏览和购买商品。既然大家对电商系统都比较熟悉,我将以电商系统作为研究对象,进一步聊聊数据与数据存储的相关内容。 比如我们在某平台搜索“文件系统”这个关键字,想看看这方面的书籍。当我们输入…

环形链表面试题详解

A. 环形链表1 给你一个链表的头节点 head ,判断链表中是否有环. 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置…

Typescript语法二

继承 继承是⾯向对象编程中的重要机制,允许⼀个类(⼦类或派⽣类)继承另⼀个类(⽗类或基类)的属性和⽅法。⼦类可以直接使⽤⽗类的特性,并根据需要添加新的特性或覆盖现有的特性。这种机制赋予⾯向对象程序良…

OpenCV的周期性噪声去除滤波器(70)

返回:OpenCV系列文章目录(持续更新中......) 上一篇:OpenCV如何通过梯度结构张量进行各向异性图像分割(69) 下一篇 :OpenCV如何为我们的应用程序添加跟踪栏(71) 目录 目标 理论 如何消除傅里叶域中的周期性噪声? 源代码 解释 结果 目…

C语言——指针的奥秘(1.0)

指针 一.内存和地址1.内存2.编址 二.指针变量和指针1.取地址操作符(&)2.指针变量和解引用操作符(*)1.指针变量2.拆解指针类型3.解引用操作符4.指针变量的大小 三.指针变量的类型和意义1.指针的解引用2.指针 - 整数3.void* 指针…