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:");}@Overrideprotected 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优先级较低,所以先暂定5sThread.sleep(500);if (SAVE_HOKE!=null){SAVE_HOKE.alive();}else {System.out.println("no, i am dead:");}//这段代码和上面一样,但是却拯救失败SAVE_HOKE=null;System.gc();//由于finalize优先级较低,所以先暂定5sThread.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.mzph.cn/web/6388.shtml

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

相关文章

组队竞赛和删除公共字符

这里附上两个题目的链接 题目一:删除公共字符_牛客题霸_牛客网 (nowcoder.com) 题目二:组队竞赛_牛客笔试题_牛客网 (nowcoder.com) 第一题 分析: 当我们看见这个题目的时候,可以使用传统的暴力查找方式,如判断第一个…

VsCode | 修改首页启动页 Logo

VsCode | 修改首页启动页 Logo 最终效果: 插件的安装 先安装插件 Custom CSS and JS Loader 插件配置 Ctrl Shift P 输入 打开用户设置,在末尾添加 "vscode_custom_css.imports": [""]下载 Logo 下载 Logo 点我下载 引入…

json转excel

前面有介绍过excel文件转换成json文件的方法,那json文件转excel文件呢?如果json文件里数据格式都是统一的话,那么也比较容易就转。 我们假设json文件中是一个json数组,每条json数据的属性字段都一样,手写一段node.js例…

若依分离版-前端使用echarts组件

1 npm list:显示已安装的模块 该命令用于列出当前项目的所有依赖关系,包括直接依赖和间接依赖。执行 npm list 时,npm 将从当前目录开始,递归地列出所有已安装的模块及其版本信息 npm list 2 npm outdated:用于检查当前项目中的npm包是否有…

亚马逊云科技AWS免费证书-EC2服务器设计(含题库)

亚马逊云AWS官方程序员专属免费证书又来了!这次证书是关于AWS EC2实例的设计和搭建,EC2作为AWS服务的核心,是学好AWS的第一步。强推没有任何AWS背景和转码的小伙伴去学!学完也能变成AWS开发大神! 证书名字叫Getting St…

使用 TensorFlow 和 Keras 构建 U-Net

原文地址:building-a-u-net-with-tensorflow-and-keras 2024 年 4 月 11 日 计算机视觉有几个子学科,图像分割就是其中之一。如果您要分割图像,则需要在像素级别决定图像中可见的内容(执行分类时),或者从像…

第Ⅰ章-IV npm yarn pnpm 包管理器

第Ⅰ章-Ⅰ 了解Vue3 创建一个Vue3项目 第Ⅰ章-Ⅱ Vue3自定义创建项目 项目文件详解 第Ⅰ章-III Vite 创建vue3 项目 第Ⅰ章-IV npm yarn pnpm 包管理器 第Ⅰ章-IV npm yarn pnpm 包管理器 简介npm工作原理 yarn工作原理 pnpm工作原理 功能脚本添加依赖移除依赖安装所有依赖查看…

Oracle23ai来了,23爱,全能、超级巨兽...

📢📢📢📣📣📣 作者:IT邦德 中国DBA联盟(ACDU)成员,10余年DBA工作经验, Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主,全网粉丝10万 擅长主流Oracle、My…

[图解]关于SysML v2(1)大刀阔斧 对比 伪创新圈子

1 00:00:03,960 --> 00:00:08,270 OMG在2月份,这里写了4月 2 00:00:08,440 --> 00:00:13,530 应该是2月,发布了 3 00:00:13,870 --> 00:00:17,700 SysML v2的 beta 2版本 4 00:00:17,870 --> 00:00:19,780 也是当前最新的版本 5 00:00:2…

关于灰度发布

目录 一 来源 二 运行过程 三 适用范围 一 来源 灰度发布,也叫金丝雀发布,起源是,矿井工人发现,金丝雀对瓦斯气体很敏感,矿工会在下井之前,先放一只金丝雀到井中,如果金丝雀不叫了&#xff…

【DevOps】掌控云端:Google Cloud SDK 快速上手

一、Google Cloud SDK Google Cloud SDK (Software Development Kit) 是一组工具,包括 gcloud、gsutil 和 bq,用于通过命令行或自动化脚本访问和管理 Google Cloud 资源和服务。以下是 Cloud SDK 的详细介绍: 1、gcloud 命令行工具 gcloud 是 Cloud SDK 的核心组件,用于管理…

ES的脑裂现象

目录 0 集群结点的职责1 什么是脑裂现象2 造成脑裂现象的原因2.1 网络问题(最常见)2.2 主节点负载过大,资源耗尽,别的结点ping不到主节点2.3 主节点JVM内存回收时间过长导致 3 脑裂现象的解决方案3.1 局域网部署3.2 角色分离&…

主成分分析(PCA)学习

概述 主成分分析(Principal Component Analysis,PCA)是一种常用的数据降维方法,它通过线性变换将原始数据变换为一组各维度线性无关的表示,通常用于提取数据的主要特征分量。PCA 的目标是从原始数据中提取出最重要的特…

python实验一 简单的递归应用

实验一 实验题目 1、兔子繁殖问题(Fibonacci’s Rabbits)。一对兔子从出生后第三个月开始,每月生一对小兔子。小兔子到第三个月又开始生下一代小兔子。假若兔子只生不死,一月份抱来一对刚出生的小兔子,问一年中每个月各有多少只兔子。 &…

[每日AI·0501]GitHub 版 Devin,Transformer的强力挑战者 Mamba,Sora 制作细节与踩坑,OpenAI 记忆功能

AI 资讯 国资委:加快人工智能等新技术与制造全过程、全要素深度融合GitHub版 Devin 上线,会打字就能开发应用,微软 CEO:重新定义 IDE在12个视频理解任务中,Mamba 先打败了 TransformerSora 会颠覆电影制作吗&#xff…

(delphi11最新学习资料) Object Pascal 学习笔记---第11章 ( 接口)

第11章 接口 ​ 与C及其他语言不同,Object Pascal不支持多重继承,这意味着每个类只能有一个单一的基类。 ​ 多重继承的实用性是面向对象编程专家争论的议题之一。Object Pascal中缺少多重继承可以被看做一种劣势,因为您没有C的功能强大&am…

Go实现 - 树莓派自己烧录自己 之 多读卡器同时烧录

简介 Go实现 监控读卡器设备存储空间变化, 自动烧写SD Card, 烧写完成之后自动弹出, 显示执行状态, 还支持热插拔。 步骤 代码 lsblkParser.go imageWriter.go package actionimport ("fmt""os/exec" )ty…

Oracle 23c? No Oracle 23ai

昨天 Oracle 发布了最新的Oracle版本。出乎意料的是这个版本从Oracle 23c 更名为 Oracle 23ai ,似乎预示着Oracle的掌舵人Larry也要全面拥抱AI技术浪潮了。 23ai版本主要功能介绍: Oracle Database 23ai 是 Oracle 数据库的下一个长期支持版本。它包括 300 多项新功…

【LeetCode刷题】410. 分割数组的最大值

1. 题目链接2. 题目描述3. 解题方法4. 代码 1. 题目链接 410. 分割数组的最大值 2. 题目描述 3. 解题方法 题目中提到的是某个和的最大值是最小的,这种题目是可以用二分来解决的。 确定区间,根据题目的数据范围,可以确定区间就是[0, 1e9]…

LEETCODE LCR 041. 数据流中的移动平均值

class MovingAverage:def __init__(self, size: int):"""Initialize your data structure here."""self.sizesize1self.front0self.rear0self.queue[None for _ in range(size1)]self.sum0def next(self, val: int) -> float:# 满了if (self.…