JVM系列 | 垃圾收集算法

news2024/9/23 5:33:16

JVM系列 | 垃圾收集算法

文章目录

  • 前言
  • 如何判断对象已"死"?
    • 引用计数法
    • 可达性分析算法
    • 可达性分析2.0版 | 引用的增强
    • 对象的消亡过程
    • 回收方法区
      • 主要回收目标:
      • 回收操作
  • 垃圾收集算法
    • 分代收集理论 与 跨代引用假说
      • 分代收集理论
      • 跨带引用假说
    • 垃圾收集算法 | 标记清除算法
    • 垃圾收集算法 | 标记复制算法
      • 传统标记复制算法
      • 优化标记复制算法
      • 逃生门 | 提前养老
    • 垃圾收集算法 | 标记整理算法

前言

在上一篇文章《【JVM】对象的生命周期一 | 对象的创建与存储》中,我们已经介绍了对象的完整创建过程,既然有出生,那么必然有死亡。本文将会详细介绍对象的消亡-JVM的垃圾回收机制。

这两篇文章详细介绍了对象的生命周期,推荐联合观看。


如何判断对象已"死"?


引用计数法

JVM 并不是使用引用计数器来判断对象是否存活的,这是由于引用计数器有着非常大的缺点。

引用计数法非常的简单:在对象中添加一个引用计数器,每当有一个地方引用它,计数器的值就+1,当引用失效时,计数器-1。当计数器为0时,就代表没有地方引用它,它就可以被垃圾回收器清除掉。

想法很完美,但是,如果两个对象互相引用,那么即使这两个对象已经不存在其它的调用关系,也不会被垃圾收集算法清理掉,请看以下代码:

class Node {
    Node next;
}

public class ReferenceCountingExample {
    public static void main(String[] args) {
        Node node1 = new Node();
        Node node2 = new Node();
        
        // 互相引用
        node1.next = node2;
        node2.next = node1;

        // 取消外部引用
        node1 = null;
        node2 = null;
        
        // 在引用计数垃圾回收器中,node1 和 node2 由于互相引用,
        // 引用计数永远不会变为0,它们不会被回收。
    }
}

以上代码中:

  1. 创建两个node对象,每个node对象的引用计数器为1
  2. 让他们的nextNode指向对方,现在互相引用,每个node的引用计数器是2
  3. 清除外部引用,也就是让node1/node2变为null,此时每个node的引用计数器是1
  4. 没有地方再引用node1/2了,但是node1/2还是无法正常退出

可达性分析算法

可达性分析算法从根对象(GC Roots)出发(注意根对象可以不止有一个,下面会有介绍),一级一级向下扫描,能被根对象直接或间接引用的对象就是存活对象(从根对象可达),与根对象没有任何关系的对象则为消亡的对象(根对象不可达)。

在这里插入图片描述

上图中:

  1. O2/O3/O4与根对象O1(间接)可达,那么在本次扫描中,该三个对象全部为存活
  2. O6与O7对象虽然与O5对象关联,但是O5对象并没有与根节点关联,因此O5/O6/O7对象全部消亡
  3. O8/O9对象虽然互相引用,但是也不例外,消亡

可达性分析2.0版 | 引用的增强

在Java 1.2之前,引用只有传统的实现方式:可达即存活、不可达即消亡。

但是在一些场景下,有一些对象存在能创造一定的价值,但是消亡了意义也不大,典型的例子就是缓存。缓存中存在的内容可能是一些大的对象,我们通过缓存可以加快程序的运行速度。但是如果缓存的内容太多,那么会严重影响JVM的运行速度,此时Java都跑不动了,还关心数据库读取什么的嘛?这个时候就可以释放掉这些引用内容。

为了解决这一问题,JDK引入了另外三种引用方式,分别如下:

  1. 强引用(经典引用 Strongly Reference)
  2. 软引用(Soft Reference)
  3. 弱引用(Weak Reference)
  4. 虚引用(Phantom Reference)
  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。


对象的消亡过程

上文中我们已经确定了判断对象的消亡的方法,但是并不是一旦发现消亡对象之后就立刻进行清除,要清除一个对象至少要经过两次标记阶段。

阶段一:判定对象为不可达

阶段二:判断对象是否重写了finalize()方法(终结器方法),如有没有重写该方法,则直接进行垃圾回收/如果重写了该方法那么将会把该对象放在名为F-Queue队列中,并在稍后由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束(以防止执行缓慢或死循环等)。

阶段三:对F-Queue队列中的对象进行二次标记。此时会查看这些对象是否仍然不可达,如果不可达那么这些对象将会被垃圾回收器进行回收。

如过finalize方法中,有代码把该对象的引用重新赋值给某个静态变量或其他存活的对象(该对象又可达了),那么该对象将不会被垃圾回收掉。

可见,对象的消亡像是一个判刑的过程,如果对象一开始犯了错(不可达),那么就先要判断该对象有没有必要缓刑(重写finalize方法),然后JVM将不缓刑的对象直接死刑立即执行,将需要缓刑的对象关到一个单独的监牢里面,随后给缓刑的对象们一个"托关系"的机会,找到机会了就能活,没有机会就得消亡。


回收方法区

简单复习:方法区是用于存储已被虚拟机加载的类信息(类名、访问修饰符、父类、接口、字段、方法等)、常量、静态变量、即时编译器编译后的代码(字段的名称与描述符、方法的字节码、访问修饰符)等数据。方法区在JVM规范中是堆的一部分,但在实现上可以有不同的划分和管理方式。

对方法区的回收性价比很低,在Java堆中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,方法区则远低于此。因此也有一些垃圾收集器没有实现对方法区的回收。


主要回收目标:

  1. 废弃的常量
  2. 不再使用的类型

回收操作

回收常量:比较简单,如果一个字符串"Jim.kk"进入到常量池但是又没有任何一个字符串对象值是它,那么他就可以被清理出去。

回收不再使用的类型:回收不再使用的类型比较麻烦,它要同时满足以下条件才能够允许被回收。而且并不一定会被回收,需要程序员使用参数控制。

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

启用条件:关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版[1]的虚拟机支持。

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。



垃圾收集算法


分代收集理论 与 跨代引用假说

分代收集理论

当我们使用引用可达算法扫描堆内存的时候,会发现大部分的对象都是朝生夕死的,存活时间可能根本不会超过一次垃圾收集。还有一些对象是长命百岁的,能一直活,很能活。

对于这种情况,垃圾收集器提出了新生代与老年代的概念:所有新创建的对象放在新生代中并定时进行扫描与垃圾回收,能挺过多次垃圾回收的对象将被放入老年代中,并在老年代中以更低的频率来回收该区域。这就同事兼顾了垃圾收集的时间开销和内存的空间利用。


跨带引用假说

对象与对象之间可能存在跨代引用,比如老年代引用新生代的对象,但是新生代的大部分都是朝生夕死的,所以跨代引用必定是小数(如果广泛存在的话则大部分新生代对象肯定都能存活很久),没有必要为了这一小部分跨带引用去扫描整个老年代,因此JVM建立了一个称为"记忆集"的数据结构(存储在新生代中),用来记录老年代的哪一块老年代的内存存在跨带引用,这样的话在扫描新生代时,只需要扫描一下这些被记录的老年代即可。

上图中,在第二个与第五个老年代的分片上存在跨代引用(分别是o7引用Y11/o20引用Y25),将这两个区域记录在记忆集中,随后在对新生代进行垃圾收集的时候,从记忆集中拿到两个老年代的内存区域并进行扫描,所以最终扫描对象除了所有新生代的对象以外,还包含(o5、o6、o7、o8、o17、o18、o19、o20)。

事实上并不只是跨老年代与新生代之间才存在跨代引用与记忆集,许多的分代或者分区的垃圾收集器中都存在跨代引用,比如近些年很火的G1收集器,没有明确的新生代与老年代,整个堆内存就是无数的小分区。


垃圾收集算法 | 标记清除算法

标记清除算法是最早出现的算法,在1960年有Lisp之父John McCarthy提出(当时还没有Java语言,不止是只有Java才有虚拟机与垃圾回收)。

标记清除算法分为两个步骤:1. 标记 2. 清除。可以对所有需要回收的对象做标记,随后统一清除掉;也可以对不需要回收的对象做标记,随后统一清除掉没有标记的对象。

标记回收算法有两个缺点:

  1. 是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  2. 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

在之前一篇文章《【JVM】对象的生命周期一 | 对象的创建与存储》中提到过使用空闲列表来记录堆内存中的空闲内存,随后在空闲内存中插入新对象的方式。这种方式就适用于标记清除算法,在一段时间使用后堆内存中会存在大量的空隙,造成很严重的内存浪费。


垃圾收集算法 | 标记复制算法

传统标记复制算法

标记复制算法又称为标记移动算法,它解决了标志清除算法中大量内存空间碎片的问题。

简单来说,标记复制算法就是将需要进行垃圾回收的区域分为两个部分(可以是新生代也可以是老年代),在创建新的对象的时候只使用其中的一半,在需要进行垃圾回收的时候,先对对象进行标记,然后将能存活的对象复制到另一半,当前区域的对象全部消亡(注意当前区域与目标区域不是新生代与老年代的区别)。

标记复制算法也有一个非常大的缺点:内存空间浪费,标记复制算法总有一半的内存空间是未被使用的。


优化标记复制算法

为了解决标记复制算法带来的巨大空间浪费问题,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。

Appel(不是Apple哦)式回收采用一个大的Eden空间两个小的Survivor空间(Eden:Survivor通常为8:1),新建对象时将对象存放至Eden空间,进行垃圾回收时,扫描Eden空间与其中一块Survivor空间,并将存活的对象全部移动至另一块Survivor空间。

上一次垃圾回收存活的对象放在Survivor1中,本次垃圾回收扫描Eden空间与Survivor1空间,并将所有存活对象放入另一个Survivor2空间中,以此循环。


逃生门 | 提前养老

虽然朝生夕死大部分情况下可以消灭98%的对象,但是毕竟也会有特殊情况,万一那10%的Survivor无法存储本次GC可以存活下来的对象怎么办呢?Appel式垃圾回收提出了逃生门机制:

在通常情况下,对象进入老年代存在一个阈值,比如一个对象连续存活超过20次可以进入老年代。但是当触发逃生门机制的时候(Survivor无法存放全部存活对象),就会让一部分存活了一段时间但是还未达到阈值的对象提前进入老年代,这样可以保证Survivor空间的正常。

垃圾收集算法 | 标记整理算法

标记整理算法在需要GC时,会先标记所有对象,然后将不需要存活的对象从内容空间中剔除,给存活的对象整齐的复制到内存的开端。

标记整理算法相比于标记移动算法,节省了内存空间,但是移动所有的存活对象并更新所有引用是一件及其负重的操作,而且在这一阶段之内用户线程无法继续执行(否则可能会造成空引用等问题),因此这样的停顿被最初的虚拟机设计者描述为“Stop The World”(有点类似于时停的意思)。

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

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

相关文章

Oracle数据库加密与安全

Wallet简介: Oracle Wallet(即内部加密技术TDE( Transparent DataEncryption) TDE是 Oracle10gR2中推出的一个新功能,使用时要保证Oracle版本是在10gR2或者以上 Wallet配置: 1.创建一个新目录,并指定为Wallet目录 /home/oracle…

论文翻译:Large Language Models for Education: A Survey and Outlook

https://arxiv.org/abs/2403.18105 目录 教育领域的大型语言模型:一项调查和展望摘要1. 引言2. 教育应用中的LLM2.1 概述2.2 学习辅助2.2.1 问题解决(QS) 2.2.2 错误纠正(EC)2.2.3 困惑助手(CH)…

ExcelToDB2:批量导入Excel到IBM DB2数据库的自动化工具

ExcelToDB2:批量导入Excel到IBM DB2数据库的自动化工具 简介 ExcelToDB2是一个可以批量导入Excel到IBM DB2数据库的自动化工具。支持将xls/xlsx/xlsm/xlsb/csv/txt/xml格式的Excel文件导入到IBM DB2等多种原生及国产数据库。自动化是其最大的特点,因为它…

Python爬虫教程第5篇-使用BeautifulSoup查找html元素几种常用方法

文章目录 简介find()和find_all()字符串通过id查找通过属性查找通过.方式查找通过CSS选择器查找通过xpath查找正则表达自定义方法总结 简介 上一篇详细的介绍了如何使用Beautiful Soup的使用方法,但是最常用的还是如何解析html元素,这里再汇总介绍下查询…

数据分析——Python网络爬虫(四){正则表达式}

爬虫库的使用 爬虫的步骤正则表达式正则表达式的流程正则表达式的使用括号的使用管道匹配问号匹配星号匹配加号匹配花括号匹配用点-星匹配所有字符跨行匹配findall方法其他常用字符匹配 例子正则表达式在线测试 爬虫的步骤 #mermaid-svg-zSQSbTxUEex051NQ {font-family:"t…

Web开发 —— 放大镜效果(HTML、CSS、JavaScript)

目录 一、需求描述 二、实现效果 三、完整代码 四、实现过程 1、HTML 页面结构 2、CSS 元素样式 3、JavaScript动态控制 (1)获取元素 (2)控制大图和遮罩层的显隐性 (3)遮罩层跟随鼠标移动 &…

【电脑应用技巧】如何寻找电脑应用的安装包华为电脑、平板和手机资源交换共享

电脑的初学者可能会直接用【百度】搜索电脑应用程序的安装包,但是这样找到的电脑应用程序安装包经常会被加入木马或者强制捆绑一些不需要的应用装入电脑。 今天告诉大家一个得到干净电脑应用程序安装包的方法,就是用【联想的应用商店】。联想电脑我是一点…

使用Lego进行证书的申请和更新

姊妹篇: 使用Let’s Encrypt 申请通配符证书 关于acme 协议 ACME是自动证书管理环境(Automatic Certificate Management Environment)的缩写,是一个由IETF(Internet Engineering Task Force)制定的协议标准&#xff0c…

gd32F470串口重定义

c代码: /** Author: Bleaach008* Date: 2024-07-10 17:31:01* LastEditTime: 2024-07-11 09:42:06* FilePath: \MDK-ARMd:\Code\GD32\GD01_UART\MyApplication\Public.c* Description:** Copyright (c) 2024 by 008, All Rights Reserved.*/ /* Includes ----------…

QFileDialog的简单了解

ps:写了点垃圾(哈哈哈) 现在感觉Qt库应该是调用了Windows提供的这块的接口了。 它继承自QDialog 这是Windows自己的文件夹 这是两者的对比图: 通过看QFileDialog的源码,来分析它是怎么实现这样的效果的。 源码组成…

面试篇-Java-5+设计模式

文章目录 前言一、你知道工厂方法模式吗1.1 你有使用过简单工厂模式吗1.2 你有使用过简单工厂方法模式吗1.3 你有使用过抽象工厂方法模式吗1.4 你有使用过策略模式吗 二、你们项目中是怎么使用设计模式的呢2.1 策略模式 工厂模式 实现不同的方式的登录2.1.1 定义一个登录的接口…

SCI一区级 | Matlab实现NGO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测

SCI一区级 | Matlab实现NGO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测 目录 SCI一区级 | Matlab实现NGO-CNN-LSTM-Mutilhead-Attention多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现NGO-CNN-LSTM-Mutilhead-Attention北方苍鹰算…

怎么用PPT录制微课?详细步骤解析!

随着信息技术的不断发展,微课作为一种新型的教学形式,因其短小精悍、针对性强等特点,在教育领域得到了广泛的应用。而PPT作为一款常用的演示工具,不仅可以用来制作课件,还可以利用其内置的录屏功能或结合专业的录屏软件…

【机器学习】Exam4

实现线性不可分logistic逻辑回归 我们目前所学的都是线性回归,例如 y w 1 x 1 w 2 x 2 b y w_1x_1w_2x_2b yw1​x1​w2​x2​b 用肉眼来看数据集的话不难发现,线性回归没有用了,那么根据课程所学,我们是不是可以增加 x 3 x…

有必要把共享服务器升级到VPS吗?

根据自己的需求来选择是否升级,虚拟专用服务器 (VPS) 是一种托管解决方案,它以低得多的成本提供专用服务器的大部分功能。使用 VPS,您的虚拟服务器将与在其上运行的其他虚拟服务器共享硬件服务器的资源。但是,与传统的共享托管&am…

# Redis 入门到精通(一)数据类型(4)

Redis 入门到精通(一)数据类型(4) 一、redis 数据类型–sorted_set实现时效性任务管理 1、sorted_set 类型数据操作的注意事项 score 保存的数据存储空间是64位,如果是整数范围是-9007199254740992~9007199254740992…

内网对抗-基石框架篇域树林域森林架构信任关系多域成员层级信息收集环境搭建

知识点: 1、基石框架篇-域树&域林架构-权限控制-用户和网络 2、基石框架篇-域树&域林架构-环境搭建-准备和加入 3、基石框架篇-域树&域林架构-信息收集-手工和工具1、工作组(局域网) 将不同的计算机按照功能分别列入不同的工作组。想要访问某个部门的…

PostgreSQL 怎样处理数据仓库中维度表和事实表的关联性能?

文章目录 PostgreSQL 中维度表和事实表关联性能的处理 PostgreSQL 中维度表和事实表关联性能的处理 在数据仓库的领域中,PostgreSQL 作为一款强大的关系型数据库管理系统,对于处理维度表和事实表的关联性能是一个关键的问题。维度表和事实表的关联是数据…

基于B站视频评论的文本分析,采用包括文本聚类分析、LDA主题分析、网络语义分析

研究主题 本研究旨在通过对B站视频评论数据进行文本分析,揭示用户评论的主题、情感倾向和语义结构,助力商业决策。主要技术手段包括Python爬虫、LDA主题分析、聚类分析和语义网络分析。首先,利用Python爬虫采集大量评论数据并进行预处理。运…

Hadoop3:动态扩容之新增一台机器的初始化工作

一、需求描述 给Hadoop集群动态扩容一个节点 那么,这个节点是全新的,我们需要做哪些准备工作,才能将它融入集群了? 二、初始化配置 1、修改IP和hostname vim /etc/sysconfig/network-scripts/ifcfg-ens33 vim /etc/hostname2、…