JVM是如何管理内存的?图文详解GC垃圾回收算法

前言:在C/C++中对于变量的内存空间一般都是由程序员手动进行管理的,往往会伴随着大量的 malloc 和 free 操作,常常会有很多问题困扰开发者,这个代码会不会发生内存泄漏?会不会重复释放内存?但是在Java开发中我们却很少有这样的担忧,程序员几乎很少手动管理内存,这是因为在Java虚拟机JVM中这些事情都被JVM的垃圾回收算法管理和代理操作了。


目录

一.什么是GC

二.JVM中的GC

▐ 如何找到要回收的内存

1.使用引用计数器判断某个对象是否具有引用指向(Python、PHP)

2.可达性分析(JVM采取的方案)

▐ 如何对找到的内存垃圾进行释放回收

1.标记-清除

2.复制算法

3.标记-整理

分代回收


一.什么是GC

GC是垃圾回收(Garbage Collection)的缩写,是计算机科学中一种自动化的内存管理机制。在传统的内存管理方式中,程序员需要手动分配和释放内存。而GC则可以自动跟踪和回收不再被程序使用的内存,从而减轻了程序员的负担。要注意的是,GC并不是Java独有的一种机制,现如今GC广泛应用于许多的高级语言,诸如PHP、Python、Lua、Ruby、Go... ...

GC的主要原理是通过检测程序中不再被引用的对象,将其标记为垃圾,然后自动回收这些垃圾对象所占用的内存资源。GC会定期地执行垃圾回收操作,找出不再被使用的对象并释放其内存,从而避免内存泄漏和内存溢出的问题。

垃圾回收机制给程序员带来了许多便利的同时也会产生性能问题,很简单的逻辑,既然要自动跟踪回收部分内存,那就需要分配一定的系统资源给到GC上,如果GC的效率非常差,很可能触发GC的一瞬间就会把系统的负载拉满,严重时会导致服务器无法响应其他的请求,因此,一个优秀且高效率的GC算法就必不可少。


二.JVM中的GC

对于一个Java程序来说,GC回收的是内存,其实就是不同的对象,往往都是堆区上的数据,我们对于JVM中的内存区域大致做个分析:

  1. 程序计数器:一般是不需要额外回收的,线程销毁了,内存自然就回收了
  2. 栈区:一般夜市不需要额外回收的,线程销毁了,内存自然也就回收了
  3. 元数据区:一般也不需要,我们一般进行的都是加载类的操作,很少说是卸载类
  4. 堆区:GC的主力回收区域

并且GC回收内存的时候,一定回收的是一个完整的对象,比如一个对象有10个成员,那么一定是回收这全部10个成员,不可能只回收一部分。

对于GC回收的内容有了一个了解后,就要关心GC回收的流程,总的来说垃圾回收分为俩个步骤

  • 找到要回收的垃圾(内存)
  • 释放对应的内存

下文也按照这个流程分为俩部分来讲解

▐ 如何找到要回收的内存

一个对象的创建时间往往是很明确的,但是对于该对象什么时候不再使用,时机往往是模糊不定的。

举个例子来说,就像一个一年级的小学生,做作业的时候很容易被其他事物分心,可能写半个小时作业就去玩一下,过一段时间再来写作业。但是如果我们认为他已经连续2个小时没有写作业了,就在他玩的时候将作业和本子和笔收起来,那么等到他回来准备继续写作业的时候,就会发现根本无从下手,对应到我们的代码中,后面的业务和逻辑就完全无法进行了。

因此,我们必须要保证代码中使用的每一个对象都是有效的,千万不能出现提前释放的情况,我们必须要采取很保守的态度,宁可晚一点回收内存,也不能提前回收打断了原有程序的运行。

那我们需要用什么来作为判断某个对象是否为垃圾的依据呢?JVM是如何判断某个对象是否应该被回收呢?对于小学生写作业的例子中,我们采取了 “上一次使用时间” 进行判断,很显然这是不太合理的,在GC中我们往往使用一种很保守的方法来判断某个对象是否需要释放——即是否存在引用指向该对象

就拿下面这段示例来说,我们new了一个类对象Test,这时的 t 就是指向该对象的引用,此时这个对象就是有效的,我们则不能回收他。

Test t = new Test();

但如果我们将 t 置为 null ,原先指向Test对象的 t 更改了他的指向,此时我们就说这个Test对象不存在引用指向该对象,即该对象就是我们要回收的垃圾

t = null;

在我们理解了如何判断一个对象是否为垃圾后,还有一个问题需要解决,对于我们刚才方案中提到的这个依据,我们又该如何判断这个依据是否存在呢?刚才的例子很简单,但是实际情况往往是很复杂的,不可能一概全是用 null 来改变指向,在垃圾回收机制中具体是怎么判定某个对象是否有引用指向呢?

这样的策略有很多,主要分为以下俩种

  • 使用引用计数器(Python/PHP采用的方案)
  • 可达性分析(JVM采用的方案)

1.使用引用计数器判断某个对象是否具有引用指向(Python、PHP)

这种方案为Python和PHP采用的方案,我们知道内存是一块连续的物理空间,那我们在存储对象的时候在对象旁边放置一个引用计数器来统计这个对象目前有多少个引用,每个对象都有自己的引用计数器,当这个计数器为0的时候就说明当前对象没有引用,那么就可以作为GC回收的垃圾进行内存回收了。

这样的方案优点在于简单容易实现,笔者这里还是画图说明一下

当我们new了一个对象,并且用a来指向它,此时引用计数器 +1

Test a = new Test();

 然后我们使用一个b来指向a,虽然这一步并没有新建一个对象,但是这个b还是指向的Test这个对象,因此引用计数器 +1

Test b = a;

然后我们如果再更改b的指向,让b不再指向Test这个对象,那么对应的引用计数器就要 -1

b = null;

那么如果我们再更改a的指向,此时的引用计数器则 -1 变为了 0 ,则该对象没有任何的引用,则该对象就是垃圾,需要被回收

a = null;

这样的方案优点在于简单易懂,好实现,但是同样有俩个缺点,那就是会消耗额外的空间以及会参数循环引用的问题。

消耗额外的空间:这很好理解,每个对象都有自己的引用计数器,那么如果对象很多,几百个上千个对象就需要同样数量的引用计数器,每个引用计数器的维护也都需要内存,这无疑会造成很大的资源浪费

循环引用的问题则较为复杂,笔者这里还是使用图文的方式详细解释一下。

假设我们分别new了俩个Test对象,分别用a和b来指向他们。

class Test {Test t;
}Test a = new Test();
Test b = new Test();

那么情况就应该同下图,a和b分别指向俩个地址

然后我们让每个对象的内部成员对象都指向对方,由于每个Test对象都指向了对方,那么理所应当的俩个计数器都应该 +1 

a.t = b;
b.t = a;

到这里一切都是很正常的,但是,如果我们此时把 a b 都指向 null 的话会发生什么呢?由于原本指向俩个 Test 对象的 a b 都指向 null ,那么理所应当的俩个计数器也都应该 -1

a = null;
b = null;

所以理所应当的就会变为上图的情况,大家仔细观察一下,这合理吗?明明已经没有任何引用指向俩个 Test 对象了,但是他们的引用计数器却因为之前的种种操作没有合理的清零,就导致了俩个对象永远相互指向对方,俩者的引用计数器都为 1(不为0,不是垃圾,不会被清理),但是外部代码没有任何方式访问到这俩个对象。这就是我们所说的引用循环的问题。

这样的问题能解决吗?当然也是可以解决的,前文也说了,有许多语言是使用的这个策略。为了解决这个问题我们则需要引入其他的机制。JVM并没有使用这种策略。

2.可达性分析(JVM采取的方案)

可达性分析的方案策略是JVM采用的方案,它解决了空间的问题和循环引用的问题,但是付出了时间上的代价,这意味着它需要消耗的时间更多,需要消耗的系统资源也更多。

那么这个方案具体是怎么做的呢?

JVM会把对象之间的引用关系理解为一个树形结构,通过不断的遍历这样的结构,就能把每个对象打上标记,分为“可达”和“不可达”,就像我们在学习离散数学中那样,对于图论的研究,我们会去考虑一个图的可达性问题,我们知道树其实也是一种特殊的图,我们通过研究这颗树的连通性和可达性就可以判断出他们每个节点之间的关系,节点与节点之间如果可达就说明他们有引用关系,如果不可达就说明他们没有引用关系,自然而然的我们就知道了哪些节点(对象)不存在引用关系,从而判断出哪些对象属于垃圾,需要回收。

如果其中某个对象没有任何对象指向它,那么该对象则被判定为垃圾,需要被回收

对于之前提到的循环引用的情况,由于他们与跟节点不可达,因此也会被判定为垃圾,从而进行回收。如图所示:

这样就可以解决引用计数器中出现的俩个问题,当然这需要额外消耗系统资源。

一个Java程序中往往有很多的遍历和类对象,这就意味着有很多上述这样的树结构,具体树有多复杂都取决于实际的代码结构,在这其中有一个很关键的概念——GC roots,也就是这些树的根节点,在Java代码中对于栈上的局部遍历,常量池中引用的对象、方法区中的静态成员这些都是GC roots,JVM会周期性的对这些树进行遍历,不断的标记可达和不可达,不断的回收掉不可达的对象。

由于可达性分析需要消耗一定的时间,因此Java垃圾回收没法做到“实时性”,JVM会提供一组专门复杂GC的线程,不停的进行扫描工作。

▐ 如何对找到的内存垃圾进行释放回收

解决了找到垃圾的策略,接下来要思考的就是回收垃圾的策略。

对于回收垃圾我们也有三种策略:

  • 标记-清除
  • 复制算法
  • 标记-整理

以下分为三部分讲解

1.标记-清除

这种做法简单粗暴,直接将标记为垃圾的对象对应的内存释放掉,如下图所示

但是这样的策略带来的最大的问题在于:它会存在“内存碎片”的问题,就会导致后续很难申请到一块大的连续的内存了。因为我们申请内存都是要申请连续的内存空间的,这样会使得空间利用率极低。

这就好比放假,假如一个人一个月有15天假期,尽管数量多但是都不是连续的,都是工作一天休息一天,那么这个人就算这么多假期,也还是不能出省出国的旅游,只能在家休息,毕竟隔一天就要上班。

因此,这种方案并不实用。

2.复制算法

这种方案会预先留出一段空间,当发生GC的时候,会将有用的空间全部复制到预留空间里面去,然后再将原来复制前的空间清空回收。

举例子来说,假如我们现在需要释放2、4、6三块内存空间,保留1、3、5、7共四块内存空间

首先将需要保留的空间复制到预留空间里面去

最后再将复制前的前半部分空间全部回收

这样的方案解决了空间碎片化的问题,但是需要保留的空间越多,复制的时间也就月多,因此也会有浪费系统资源的问题

3.标记-整理

这种策略类似于顺序表中删除元素的流程,它既能解决内存碎片问题,也能解决空间利用率的问题

还是这个例子,假如我们现在需要释放2、4、6三块内存空间,保留1、3、5、7共四块内存空间

就像顺序表删除元素一样,后面的元素依次向前覆盖,最终只保留前半部分内容,对后半部分进行回收

但是这样搬运覆盖对时间又有损耗

综上所述,三种方案各有各的优点,各有各的缺点,那么JVM是如何进行选择的呢?JVM表示“小孩子才做选择,我都要”。JVM综合了以上三种方案 ,试用了更复杂的策略——分代回收

分代回收

 在该方案中JVM会根据对象的年龄来进行分类,对于年龄这个概念需要做出解释:

年龄:GC中有一组线程,周期性扫描,对于某个对象,经历了一轮GC后,如果还是存在,没有成为垃圾的话,年龄就+1

对于GC在堆区的操作我们大概可以分为以下几个部分,我们将堆区分为新生代和老年代,对于新生代我们又可以细分为Eden(伊甸区)S0(生存区)S1(幸存区) 

对于新创建的对象,基本上都是放在伊甸区,在伊甸区中大部分的对象生命周期都是比较短的,第一轮GC到达的时候,大多数对象都会成为垃圾,只有少数对象能够活过第一轮GC。

对于伊甸区存活下来的对象,会通过复制算法转移到生存区,由于存活对象很少,复制开销也很低,因此生存区空间也不必很大。

每经历一轮GC,生存区都会淘汰掉一批对象,对于生存区存活下来的对象,会同样通过复制算法转移到幸存区,同样进入幸存区的还可能会有伊甸区进来的对象。

其实对于生存区和幸存区,他们二者之间没有什么特别的区别,因此,将其二者都称为生存区或者幸存区都是可以的,重点在于理解思想,二者的名称并没有那么重要。

某些对象经历了很多轮的GC都没有变为垃圾,那么他们就会从生存区/幸存区经历复制算法,转移到老年代,老年代的对象也是需要GC的,但是对于老年代的对象,他们的生命周期往往都比较长,因此可以降低GC的频率。

上述过程就是分代回收的基本逻辑。

对象在 伊甸区 --> 生存区/幸存区 --> 老年代 的过程中主要体现了复制算法的思想;对象在老年代则通过标记-整理的策略进行回收。

整个过程其实很像玩“吃鸡”游戏,一波一波的刷毒圈,一波一波的淘汰人,同样也很像找工作面试的情况。




 本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

 

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

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

相关文章

各地户外分散视频监控点位,如何实现远程集中实时监看?

公司业务涉及视频监控项目承包搭建,此前某个项目需求是为某林业公司提供视频监控解决方案,需要实现各地视频摄像头的集中实时监看,以防止国家储备林的盗砍、盗伐行为。 公司原计划采用运营商专线连接各个视频监控点位,实现远程视…

Redis的缓存雪崩,击穿,穿透的介绍

1.缓存雪崩 为保证缓存中的数据与数据库的数据一致,会给Redis里的数据设置一个过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成新的缓存,因为就会访问数据库,并将数据更新到Redis里,这样后续请求就可以直接命中缓存. 当大量缓存在同一时间过期或…

解决:WPS,在一个表格中,按多次换行,无法换到下一页

现象:在一个表格里面,多次按下回车,始终无法到下一页 解决方法:右击—>表格属性—>选择行—>勾选 允许跨页断行 效果演示 对比展示

基于Python+Flask+MySQL的新冠疫情可视化系统

基于PythonFlaskMySQL的新冠疫情可视化系统 FlaskMySQL 基于PythonFlaskMySQL的新冠疫情可视化系统 项目主要依赖前端:layui,Echart,后端主要是Flask,系统的主要支持登录注册,Ecahrt构建可视化图,可更换主…

004-基于Sklearn的机器学习入门:回归分析(下)

本节及后续章节将介绍机器学习中的几种经典回归算法,包括线性回归,多项式回归,以及正则项的岭回归等,所选方法都在Sklearn库中聚类模块有具体实现。本节为下篇,将介绍多项式回归和岭回归等。 目录 2.3 多项式回归 2…

从零开始的python学习生活2

接上封装 class Phone:__volt0.5def __keepsinglecore(self):print("让cpu以单核运行")def if5G(self):if self.__volt>1:print("5G通话已开启")else:self.__keepsinglecore()print("电量不足,无法使用5G通话,已经设置为单…

使用F1C200S从零制作掌机之debian文件系统完善NES

一、模拟器源码 源码:https://files.cnblogs.com/files/twzy/arm-NES-linux-master.zip 二、文件系统 文件系统:debian bullseye 使用builtroot2018构建的文件系统,使用InfoNES模拟器存在bug,搞不定,所以放弃&…

[Qt] Qt Creator中,新建QT文件时选择界面模版下的各选项

在Qt Creator中,新建文件时选择界面模版下的各选项具有特定的意义,这些选项主要帮助开发者根据项目需求快速生成不同类型的文件。以下是对这些选项的详细解释: 0. Qt Item Model 意义:列表模型是Qt中用于表示和操作数据的强大抽…

Spring Boot:连接MySQL错误Public Key Retrieval is not allowed

环境: MySQL版本:8.0.17 SpringBoot版本:2.5.15 解决 解决方式很简单,在数据库配置连接字符串spring.datasource.url末尾添加&allowPublicKeyRetrievaltrue即可,如下图: 重新启动,恢复正常…

了解光耦合器:实际应用和有效使用

光耦合器,也称为光隔离器,是电子电路中必不可少的组件,旨在利用光波在隔离电路之间传输电信号。该技术在增强电路安全性、降低噪音和电气隔离方面具有关键优势,在从工业控制系统到消费电子产品的各种应用中都具有不可估量的价值。…

初中生物知识点总结(人教版)

第一章 认识生物 一、 生物的特征: 1. 生物的生活需要营养 2. 生物能进行呼吸 3. 生物能排出身体内产生的废物 4. 生物能对外界的刺激做出反应 5. 生物能生长和繁殖 除病毒以外,生物都是由细胞构…

SpringBoot整合XXL_JOB示例

XXL-JOB 是一个分布式任务调度平台,主要用于管理和执行定时任务。它适用于各种场景,例如定时任务、批处理任务、分布式任务等。XXL-JOB 提供了丰富的功能,使得任务调度变得简单、高效和可靠。以下是 XXL-JOB 的一些主要功能和特点&#xff1a…

myeclipse开发ssm框架项目图书管理系统 mysql数据库web计算机毕业设计项目

摘 要 随着计算机的广泛应用,其逐步成为现代化的标志。图书馆的信息量也会越来越大,因此需要对图书信息、借书信息、还书信息等进行管理,及时了解各个环节中信息的变更,要对因此而产生的单据进行及时的处理,为了提高高…

Dify 与 Xinference 最佳组合 GPU 环境部署全流程

背景介绍 在前一篇文章 RAG 项目对比 之后,确定 Dify 目前最合适的 RAG 框架。本次就尝试在本地 GPU 设备上部署 Dify 服务。 Dify 是将模型的加载独立出去的,因此需要选择合适的模型加载框架。调研一番之后选择了 Xinference,理由如下&…

搞不清啊?伦敦金与上海金区别是?

进入黄金市场的朋友,有可能会被各式各样的黄金交易品种带得眼花缭乱,其实各品种虽然都以黄金作为投资标的物,但是也是各有不同的,下面我们就来比较一下相似的投资品种——伦敦金和上海金。 首先在比较之前,我们要搞清楚…

SpringBoot新手快速入门系列教程十一:基于Docker Compose部署一个最简单分布式服务项目

如果您还对于Docker或者Docker Compose不甚了解,可以劳烦移步到我之前的教程: SpringBoot新手快速入门系列教程九:基于docker容器,部署一个简单的项目 SpringBoot新手快速入门系列教程十:基于Docker Compose&#xf…

(Windows环境)FFMPEG编译,包含编译x264以及x265

本文使用 MSYS2 来编译 ffmpeg 一、安装MSYS2 MSYS2 是 Windows 下的一组编译套件,它可以在 Windows 系统中模拟 Linux 下的编译环境,如使用 shell 运行命令、使用 pacman 安装软件包、使用 gcc (MinGW) 编译代码等。 MSYS2 的安装也非常省心&#x…

揭秘GPT-4o:未来智能的曙光

引言 近年来,人工智能(AI)的发展突飞猛进,尤其是自然语言处理(NLP)领域的进步,更是引人注目。在这一背景下,OpenAI发布的GPT系列模型成为了焦点。本文将详细探讨最新的模型GPT-4o&a…

浅析Nginx技术:开源高性能Web服务器与反向代理

什么是Nginx? Nginx是一款轻量级、高性能的HTTP和反向代理服务器,也可以用作邮件代理服务器。它最初由俄罗斯的程序员Igor Sysoev在2004年开发,并于2004年首次公开发布。Nginx的主要优势在于其非阻塞的事件驱动架构,能够处理大量并…

【多GPU训练方法】

一、数据并行 这是最常用的方法。整个模型复制到每个GPU上。训练数据被均匀分割,每个GPU处理一部分数据。所有GPU上的梯度被收集并求平均。通常使用NCCL(NVIDIA Collective Communications Library)等通信库实现。参数更新 使用同步后的梯度…