GC垃圾回收相关算法(宋红康JVM学习笔记)

news2025/7/22 0:51:24

什么是垃圾?

在这里插入图片描述

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战。

什么是垃圾呢?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能会导致内存溢出。

想要学习GC,首先需要理解为什么需要GC?

对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被耗完,因为不断的分配内存控件而不进行回收,就好像不停滴生产生活垃圾而从来不打扫一样。

除了释放没用的对象,垃圾回收也可以清理内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。

随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

早期的垃圾收集行为

在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。

MibBridge * pBridge = new cmBaseGroupBridge();
//如果注册失败,使用Delete释放该对象所占内存区域
if(pBridge->Register(kDestroy) != NO_ERROR)
delete PbRIDGE;

这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。

Java的垃圾回收机制

自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出风险

没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄漏问题让你头疼不已。

自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心与业务开发

垃圾回收相关算法

标记阶段:引用计数算法

对象存活判断

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。

那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡了。

判断对象存活一般有两种方式:引用计数算法和可达性分析算法

引用计数算法(Reference Counting)比较简单,对每一个保存一个整型的引用计数器属性。用于记录对象被引用的情况。

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1:当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可以进行回收。

优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性。

缺点:

它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。

每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。

引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,

导致在Java的垃圾回收器中没有这类算法。

标记阶段:可达性分析算法

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环应用的问题,防止内存泄漏的发生。

相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫做追踪性垃圾收集

所谓GC Roots 根集合就是一组必须活跃的引用。

基本思路:

可达性分析算法是以跟对象集合(GC Roots)为起始点,安装从上而下的方式搜索被跟对象集合所连接着,搜索所走过的路径称为引用链

使用可达性分析算法后,内存中的存活对象都会被跟对象集合直接或间接连接着,搜索所走过的路径称为引用链

如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。

在可达性分析算法中,只有能够被跟对象集合直接或者间接连接的对象才是存活对象。

在这里插入图片描述

对象的finalization机制

Java语言提供了对象终止机制来允许开发人员提供对象销毁之前的自定义处理逻辑

当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。

finalize()方法允许在子类中被重写,用于在对象被回收时进行资源的释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
在finalize()时可能会导致对象复活

在finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()会严重影响GC的性能。

由于finalze()方法的存在,虚拟机中的对象一般处于三种可能的状态。

如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候他们暂时处于"缓刑’"阶段。一个无法触及的对象有可能在某一个条件下复活自己 如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:

可触及的:从根节点开始,可以到达这个对象。

可复活的:对象的所有引用都被释放,但是对象可能在finalize()中复活。

不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。

MAT与JRrofiler的GC Roots溯源

MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。

用于查找内存泄漏以及查看内存消耗情况。

清除阶段:标记-清除算法

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,

释放掉无用对象所占用的内存空间,以便有有足够的可用内存空间为新对象分配内存。

目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法(Mark-Sweep)、

复制算法(Copying)、标记-压缩算法(Mark-Compact)。

标记-清除算法是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应用于Lisp语言。

执行过程

当堆中的有效内存空间被耗尽的时候,就会停止整个程序(也被称为stop the world)

然后进行两项工作,第一项则是标记,第二项则是清除。

标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。

清除:Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

复制算法(Copying)

为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器”(Lisp garbage Collector Algorithm Using Serial Secondary Storage)"。M.L.Minsky在论文中描述的算法被人们称为复制算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。

核心思想:

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清楚正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

在这里插入图片描述

优点:

没有标记和清除过程,实现简单,运行高效

复制过去以后保证空间的连续性,不会出现"碎片"问题

缺点

此算法的缺点也是很明显的,就是需要两倍的内存空间

对于G1这种分拆成为大量的region的GC,复制而不是移动,意味着GC需要维护

region之间对象引用关系,不管是内存占用或者时间开销也不小。

特别的:

如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,

或者说非常低才行。

标记压缩(Mark-Compact)算法

复制算法的高效性是建立在存活对象少,垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也比较高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

标记-清除算法的确可以说应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此产生。

1970年前后,G.L.Steele、C.J.Chene和D.S.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。

在这里插入图片描述

执行过程:

第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象

第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。

之后,清理边界外所有的空间。

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

优点:

消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。

消除了复制算法当中,内存减半的高额代价。

缺点:

从效率上来说,标记-整理算法要低于复制算法。

移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。

移动过程中,需要全程暂停用户应用程序。即:STW

分代收集算法

前面所有这些算法中,并没有一种算法可以完全替代其他算法,他们都具有自己独特的优势和特点。分代收集算法应运而生。

分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是业务信息相关,比如Http请求中对的Session对象,线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

目前几乎所有的GC都是采用分代收集算法执行垃圾回收的。

在HotSpot中,基于分代的概念,GC所使用的内存回收算法比必须结合年轻代和老年代各自的特点。

年轻代

年轻代特点:区域相当老年代较小,对象生命周期短、存活率低,回收频繁。

这种情况复制算法的回收整理,速度是最快的,复制算法的效率只和当前存活对象大小有关,因此很适合用于年轻代对的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代

老年代特点:区域较大,对象生命周期长、存活率高,回收不急年轻代频繁。

这种情况存在大量存活率高的对象,复制算法明显变得不合适,一般是由标记-清除或者是标记-整理的混和实现。

Mark阶段的开销与存活对象的数量成正比。

Sweep阶段的开销与所管理区域对的大小成正相关。

Compat阶段的开销与存活对象的数据成正比。

增量收集算法

上述现有的算法,在垃圾回收过程中,用于软件处于一种Stop the World的状态。

在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接到账了增量收集算法的诞生。

基本思想:

如果一次性将所有的垃圾进行处理,需要造成系统长时间的挺短,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,知道垃圾收集完成。

总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。

缺点:

使用这种方式,由于垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要对的时间就越长,有关GC参数的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

分代算法将按照对象的生命周期长短划分为两个部分,分区算法将整个堆空间划分成连续的不同小区间。

每一个小区间都独立使用,独立回收。这种算法的好处就是可以控制异常回收多少个小区间。

在这里插入图片描述

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

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

相关文章

[附源码]java毕业设计辽宁科技大学疫苗接种管理系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

果蔬同城配送小程序有什么作用_分享果蔬同城配送小程序的作用

1、蔬菜生鲜产品展示:用户打开买菜必备软件,就能查看琳琅满目的新鲜水果、蔬菜、肉类、零食等产品,为用户展示更多信息,提升用户下单率。经常更新商品的照片、视频,让客户可以在线浏览和挑选,足不出户就能买…

二叉树的最大深度(C++两种思路递归和层序)超详解小白入

原题链接–>戳这里直达 二叉树的最大深度深度搜索(递归)递归思想和详解C代码代码效率广度搜索(层序查找)层序查找的思路C代码代码效率总结深度搜索(递归) 最近新学习了树形结构,上课的时候听…

MATLAB算法实战应用案例精讲-【数模应用】随机梯度下降法(SGD)

前言 随机梯度下降算法(Stochastic gradient descent, SGD)源于1951年Robbins和Monro[6]提出的随机逼近, 最初应用于模式识别和神经网络. 这种方法在迭代过程中随机选择一个或几个样本的梯度来替代总体梯度, 从而大大降低了计算复杂度. 1958年Rosenblatt等研制出的感知机采用了…

JAVA开发(Redis使用缺陷场景)

常见的redis使用缺陷场景主要有3个,分别是缓存穿透,缓存击穿,缓存雪崩。 穿透,(关键词,缓存中没有的,数据也没有) 击穿(大量同时请求过期的key) 雪崩&…

多目标优化问题入门理论

0 前言 多目标优化在推荐系统、物流配送、路径规划等中有广泛的应用 一些多目标优化算法主要就是求解问题的 Pareto 前沿或者近似前沿。从目标空间来看,就是他的边界了。 1. 优化问题 1.1 无约束的单目标优化问题 minxf(x),x∈RN(1)min_x \quad f(x), x \in R^N…

解决jupyter TOC勾选了但不显示的问题

解决jupyter TOC勾选了但不显示的问题 有时候TOC(Table of content2)反应很慢,或者勾选了根本就不显示。或者隔三岔五nbextension消失,按以下步骤解决问题: #mermaid-svg-rbxou4Xusp7FoS9q {font-family:"trebuc…

ArcGIS pro底图大全

ArcGIS pro底图大全 Mid-Century Street World Topographic Map Navigation Map Street Night Terrain with label Oceans National graphic style map Modern antique 668753925730)] Modern antique

CAD中如何绑定外部参照和revit中链接CAD功能

一、CAD中如何绑定外部参照 首先我们要了解什么叫外部参照外部参照是指将一副图以参照的形式引用到另外一个或多个图形文件中,外部参照的每次改动后的结果都会及时的反映在最后一次被参照的图形中,另外使用外部参照还可以有效的减少图形的容量&#xff0…

Mockito的@Mock与@MockBean

在上文的 https://blog.csdn.net/dlf123321/article/details/127930378 里 大家初步会用mockito了 但是马上出现了一个问题。 package com.example.demo.controller;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.demo.entity.Per…

【算法】数组之二分查找移除元素

目录 1、数组理论基础 2、二分查找 2.1 区间左闭右闭写法 2.2 区间左闭右开写法 3、移除元素 3.1 暴力解法 3.2 双指针(快慢指针)法 1、数组理论基础 参考以前的博客:http://t.csdn.cn/HAVSF 2、二分查找 力扣https://leetcode.cn/p…

Denodo通过重要任命新增执行团队成员:Daniel Lender担任首席财务官,Stephen Welles担任首席法务官

韩国Pulmuone选择Aera Decision Cloud™来提升服务,降低成本,并支持可持续性 决策智能化公司Aera Technology今日宣布与韩国领先的生鲜食品公司、全球排名居首的豆腐生产商Pulmuone Co. Ltd.合作,帮助这家全球领军企业实现供应链决策智能化。…

数据结构之栈和队列

目录 1.栈的概念2、栈的实现 1、队列的概念2、队列的实现 今天介绍的是栈和队列。 先说栈吧。 1.栈的概念 栈也是线性表的一种,不过他较为特殊。他只能在一边进行数据的出入。也就是说晚进的数据先出去。进行数据进出的一端叫做栈顶,另一端叫做栈底…

从ReentrantReadWriteLock开始的独占锁与共享锁的源码分析

FBI WARNING(bushi) 当涉及sync调用时,并不会分析尝试获取和释放之后的后继逻辑,因为这个逻辑是由AQS类实现的。请看姊妹篇之并发入门组件AQS源码解析。 开始的开始是一个demo 以下的代码,会将独占锁持有5分钟&…

【LSTM实战】股票走势预测全流程实战(stock predict)

任务:利用LSTM模型预测2017年的股票中High的走势,并与真实的数据进行比对。数据:https://www.kaggle.com/datasets/princeteng/stock-predict 一、import packages|导入第三方库 import pandas as pd import matplotlib.pyplot as plt impo…

利用ESP32实现蓝牙通信的方法

​大家好,我是ST! 上次给大家分享了如何使用ESP32实现UDP通信,今天跟大家聊聊如何使用ESP32实现蓝牙通信。 目录 一、蓝牙简介 二、miropython有关蓝牙的实现方法 三、我的实验代码 四、手机调试APP 一、蓝牙简介 蓝牙是一种无线通讯技术&#xff…

Linux篇【5】:Linux 进程概念(三)

目录 四、进程状态 4.1、各个操作系统下的进程状态: 4.1.1、进程的运行态: 4.1.2、进程的终止态(退出态): 4.1.3、进程的阻塞态: 4.1.4、进程的挂起态: 4.2、Linux 操作系统下的进程状态: 四、进…

30、Java高级特性——Java API、枚举、包装类、装箱和拆箱

目录 课前先导: 一、Java API 1、API 2、Java API 3、Java API常用包 二、枚举类型 1、枚举 2、枚举类 3、代码演示 3.1 创建枚举类 3.2 创建测试类 4、MyEclipse创建枚举类的快捷方式 三、包装类 1、八大基本数据类型包装类 2、包装类中的构造方…

Java并发编程之可见性分析 volatile

可见性 对于什么是可见性,比较官方的解释就是:一个线程对共享变量的修改,另一个线程能够立刻看到。 说的直白些,就是两个线程共享一个变量,无论哪一个线程修改了这个变量,则另外的一个线程都能够看到上一…

电脑可以通过蓝牙发送文件吗?电脑蓝牙怎么发送文件

蓝牙(bluetooth)是一种支持设备短距离通信的无线电技术。能在包括移动电话、PDA、无线耳机、笔记本电脑、相关外设等众多设备之间进行无线信息交换。蓝牙技术让数据传输变得更加迅速高效,为无线通信拓宽道路。随着蓝牙技术的发展,…