《消息队列高手课》内存管理:如何避免内存溢出和频繁的垃圾回收?

2023-11-04

不知道你有没有发现,在高并发、高吞吐量的极限情况下,简单的事情就会变得没有那么简单了。一个业务逻辑非常简单的微服务,日常情况下都能稳定运行,为什么一到大促就卡死甚至进程挂掉?再比如,一个做数据汇总的应用,按照小时、天这样的粒度进行数据汇总都没问题,到年底需要汇总全年数据的时候,没等数据汇总出来,程序就死掉了。

之所以出现这些情况,大部分的原因是,程序在设计的时候,没有针对高并发高吞吐量的情况做好内存管理。要想解决这类问题,首先你要了解内存管理机制。

现代的编程语言,像 Java、Go 语言等,采用的都是自动内存管理机制。我们在编写代码的时候,不需要显式去申请和释放内存。当我们创建一个新对象的时候,系统会自动分配一块内存用于存放新创建的对象,对象使用完毕后,系统会自动择机收回这块内存,完全不需要开发者干预。

对于开发者来说,这种自动内存管理的机制,显然是非常方便的,不仅极大降低了开发难度,提升了开发效率,更重要的是,它完美地解决了内存泄漏的问题。是不是很厉害?当年,Java 语言能够迅速普及和流行,超越 C 和 C++,自动内存管理机制是非常重要的一个因素。但是它也会带来一些问题,什么问题呢?这就要从它的实现原理中来分析。

自动内存管理机制的实现原理

做内存管理,主要需要考虑申请内存和内存回收这两个部分。

申请内存的逻辑非常简单:

  1. 计算要创建对象所需要占用的内存大小;
  2. 在内存中找一块儿连续并且是空闲的内存空间,标记为已占用;
  3. 把申请的内存地址绑定到对象的引用上,这时候对象就可以使用了。

内存回收的过程就非常复杂了,总体上,内存回收需要做这样两件事儿:先是要找出所有可以回收的对象,将对应的内存标记为空闲,然后,还需要整理内存碎片。

如何找出可以回收的对象呢?现代的 GC 算法大多采用的是“标记 - 清除”算法或是它的变种算法,这种算法分为标记和清除两个阶段:

  • 标记阶段:从 GC Root 开始,你可以简单地把 GC Root 理解为程序入口的那个对象,标记所有可达的对象,因为程序中所有在用的对象一定都会被这个 GC Root 对象直接或者间接引用。
  • 清除阶段:遍历所有对象,找出所有没有标记的对象。这些没有标记的对象都是可以被回收的,清除这些对象,释放对应的内存即可。

这个算法有一个最大问题就是,在执行标记和清除过程中,必须把进程暂停,否则计算的结果就是不准确的。这也就是为什么发生垃圾回收的时候,我们的程序会卡死的原因。后续产生了许多变种的算法,这些算法更加复杂,可以减少一些进程暂停的时间,但都不能完全避免暂停进程。

完成对象回收后,还需要整理内存碎片。什么是内存碎片呢?我举个例子你就明白了。

假设,我们的内存只有 10 个字节,一开始这 10 个字节都是空闲的。我们初始化了 5 个 Short 类型的对象,每个 Short 占 2 个字节,正好占满 10 个字节的内存空间。程序运行一段时间后,其中的 2 个 Short 对象用完并被回收了。这时候,如果我需要创建一个占 4 个字节的 Int 对象,是否可以创建成功呢?

答案是,不一定。我们刚刚回收了 2 个 Short,正好是 4 个字节,但是,创建一个 Int 对象需要连续 4 个字节的内存空间,2 段 2 个字节的内存,并不一定就等于一段连续的 4 字节内存。如果这两段 2 字节的空闲内存不连续,我们就无法创建 Int 对象,这就是内存碎片问题。

所以,**垃圾回收完成后,还需要进行内存碎片整理,将不连续的空闲内存移动到一起,以便空出足够的连续内存空间供后续使用。**和垃圾回收算法一样,内存碎片整理也有很多非常复杂的实现方法,但由于整理过程中需要移动内存中的数据,也都不可避免地需要暂停进程。

虽然自动内存管理机制有效地解决了内存泄漏问题,带来的代价是执行垃圾回收时会暂停进程,如果暂停的时间过长,程序看起来就像“卡死了”一样。

为什么在高并发下程序会卡死?

在理解了自动内存管理的基本原理后,我再带你分析一下,为什么在高并发场景下,这种自动内存管理的机制会更容易触发进程暂停。

一般来说,我们的微服务在收到一个请求后,执行一段业务逻辑,然后返回响应。这个过程中,会创建一些对象,比如说请求对象、响应对象和处理中间业务逻辑中需要使用的一些对象等等。随着这个请求响应的处理流程结束,我们创建的这些对象也就都没有用了,它们将会在下一次垃圾回收过程中被释放。

你需要注意的是,直到下一次垃圾回收之前,这些已经没有用的对象会一直占用内存。

那么,虚拟机是如何决定什么时候来执行垃圾回收呢?这里面的策略非常复杂,也有很多不同的实现,我们不展开来讲,但是无论是什么策略,如果内存不够用了,那肯定要执行一次垃圾回收的,否则程序就没法继续运行了。

在低并发情况下,单位时间内需要处理的请求不多,创建的对象数量不会很多,自动垃圾回收机制可以很好地发挥作用,它可以选择在系统不太忙的时候来执行垃圾回收,每次垃圾回收的对象数量也不多,相应的,程序暂停的时间非常短,短到我们都无法感知到这个暂停。这是一个良性的循环。

在高并发的情况下,一切都变得不一样了。

我们的程序会非常繁忙,短时间内就会创建大量的对象,这些对象将会迅速占满内存,这时候,由于没有内存可以使用了,垃圾回收被迫开始启动,并且,这次被迫执行的垃圾回收面临的是占满整个内存的海量对象,它执行的时间也会比较长,相应的,这个回收过程会导致进程长时间暂停。

进程长时间暂停,又会导致大量的请求积压等待处理,垃圾回收刚刚结束,更多的请求立刻涌进来,迅速占满内存,再次被迫执行垃圾回收,进入了一个恶性循环。如果垃圾回收的速度跟不上创建对象的速度,还可能会产生内存溢出的现象。

于是,就出现了我在这节课开始提到的那个情况:一到大促,大量请求过来,我们的服务就卡死了。

高并发下的内存管理技巧

对于开发者来说,垃圾回收是不可控的,而且是无法避免的。但是,我们还是可以通过一些方法来降低垃圾回收的频率,减少进程暂停的时长。

我们知道,只有使用过被丢弃的对象才是垃圾回收的目标,所以,我们需要想办法在处理大量请求的同时,尽量少的产生这种一次性对象。

最有效的方法就是,优化你的代码中处理请求的业务逻辑,尽量少的创建一次性对象,特别是占用内存较大的对象。比如说,我们可以把收到请求的 Request 对象在业务流程中一直传递下去,而不是每执行一个步骤,就创建一个内容和 Request 对象差不多的新对象。这里面没有多少通用的优化方法,你需要根据我告诉你的这个原则,针对你的业务逻辑来想办法进行优化。

对于需要频繁使用,占用内存较大的一次性对象,我们可以考虑自行回收并重用这些对象。实现的方法是这样的:我们可以为这些对象建立一个对象池。收到请求后,在对象池内申请一个对象,使用完后再放回到对象池中,这样就可以反复地重用这些对象,非常有效地避免频繁触发垃圾回收。

如果可能的话,使用更大内存的服务器,也可以非常有效地缓解这个问题。

以上这些方法,都可以在一定程度上缓解由于垃圾回收导致的进程暂停,如果你优化的好,是可以达到一个还不错的效果的。

当然,要从根本上来解决这个问题,办法只有一个,那就是绕开自动垃圾回收机制,自己来实现内存管理。但是,自行管理内存将会带来非常多的问题,比如说极大增加了程序的复杂度,可能会引起内存泄漏等等。

流计算平台 Flink,就是自行实现了一套内存管理机制,一定程度上缓解了处理大量数据时垃圾回收的问题,但是也带来了一些问题和 Bug,总体看来,效果并不是特别好。因此,一般情况下我并不推荐你这样做,具体还是要根据你的应用情况,综合权衡做出一个相对最优的选择。

小结

现代的编程语言,大多采用自动内存管理机制,虚拟机会不定期执行垃圾回收,自动释放我们不再使用的内存,但是执行垃圾回收的过程会导致进程暂停。

在高并发的场景下,会产生大量的待回收的对象,需要频繁地执行垃圾回收,导致程序长时间暂停,我们的程序看起来就像卡死了一样。为了缓解这个问题,我们需要尽量少地使用一次性对象,对于需要频繁使用,占用内存较大的一次性对象,我们可以考虑自行回收并重用这些对象,来减轻垃圾回收的压力。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《消息队列高手课》内存管理:如何避免内存溢出和频繁的垃圾回收? 的相关文章

随机推荐

  • 机械臂正运动学标准DH参数建立技巧

    1 切记 i 坐标系建立在i 1关节轴上 如 0 坐标系建立在关节1轴上 依次类推 6 坐标系与 5 坐标系姿态一致 固连在法兰盘接口末端 2 坐标系原点建立 若1 2轴垂直或异面垂直 则坐标系 1 原点在1轴与2轴的交点 0 坐标系原点建
  • 2.2.1 数据通信系统的模型

    一个数据通信系统分为三大部分 1 源系统 或发送端 发送方 2 传输系统 或传输网络 3 目的系统 或接收端 接收方 数据通信系统模型如下 上图中调制解调器有2个功能 1 调制 将计算机发出的低频 数字信号 转换成传输媒介可以传输的 模拟信
  • fetch中断请求, 和再次恢复使用

    业务场景 当时用fetch 建立长连接请求 在不使用时需要将其断掉 以缓解带宽压力和浏览器运行压力 等再次需要建立长链接时 再次启用 1 外层定义controller 一旦中止 AbortController就会被消耗 每次调用都必须创建新
  • Python基础学习-简要记录

    目录 快捷键 基础 1 字符串 2 变量 3 序列 4 列表 5 元组 6 字典 7 集合 8 time 模块 9 datetime 模块 date time datetime 类 10 calendar 模块 Calendar 类 Tex
  • 在1行上输入5个数字,数字之间用英文半角逗号分隔。输出其中最小的数字。 结果保留2位小数。

    题目描述 在1行上输入5个数字 数字之间用英文半角逗号分隔 输出其中最小的数字 结果保留2位小数 输入 6 4 5 2 3 输出 2 00 样例输入 Copy 12 22 2 32 42 样例输出 Copy 2 00 a map eval
  • undo表空间故障恢复

    time 2008 04 15author skate 参考文档 http blog chinaunix net u 7667 showart 163271 html undo表空间故障恢复 ORA 00376 file 2 cannot
  • mysql查询排名前5的语句_MySQL语句实现排名

    首先我们创建一张city popularity表 CREATE TABLEcity popularity regionint 10 NOT NULL COMMENT 1 国内 2 海外 city nameVARCHAR 64 NOT NUL
  • Vue.js全家桶仿哔哩哔哩动画 (移动端APP)

    项目地址 由于项目是移动端 电脑访问时可以切换成手机端 播放页面其实没有根据B站移动端来 比较粗糙 源码地址 欢迎Star 在线预览 项目描述 前端部分 实现的Swiper Toast Indicator组件 来自Mint ui 使用了Vu
  • 【HDFS】EditLogTailer功能及原理(二)-- selectInputStreams细节详解

    HDFS EditLogTailer功能及原理 一 整体流程 HDFS EditLogTailer功能及原理 二 selectInputStreams细节详解 HDFS EditLogTailer功能及原理 三 loadEdits方法细节详
  • Javascript变量提升预解析的理解

    预解析 JavaScript代码的执行是由浏览器中的JavaScript解析器来执行的 JavaScript解析器执行JavaScript代码的时候 分为两个过程 预解析过程和代码执行过程 预解析过程 把变量的声明提升到当前作用域的最前面
  • 使用python的pandas库把.data文件转化为csv文件

    1 问题引入 在数据分析 机器学习 深度学习中 我们经常会处理各种各样格式的数据 今天 博主在做房价预测时 采用波士顿房价数据集 从网上下载的数据集格式为 data 并不是我们喜闻乐见的csv格式 所以想采用pandas库将其转为为csv格
  • 【Redis】Redis 的学习教程(十)之使用 Redis 实现消息队列

    消息队列需要满足的要求 顺序一致 要保证消息发送的顺序和消费的顺序是一致的 不一致的话可能会导致业务上的错误 消息确认机制 对于一个已经被消费的消息 已经收到ACK 不能再次被消费 消息持久化 要具有持久化的能力 避免消息丢失 这样当消费者
  • linux怎么将磁盘剩余空间给分区,利用fdisk将硬盘剩余空间进行分区

    1 首先查看分区 发现300多G的硬盘 dev sdc1只使用了200多G而已 root local103 dbbackup df h Filesystem Size Used Avail Use Mounted on dev sda2 1
  • [黑科技] 使用Word和Excel自制题库自判断答题系统

    这篇文章是LZY老师告诉我的一个方法 如果你需要做题库 并且喜欢电子答题的方法 这篇文章或许会对你有所帮助 反正李老师班级的平均成绩高出其他班级的14分 这就是它的好处 希望这篇文章对我今后的学生有所帮助吧 注意 这篇文章涉及到Word特殊
  • 详解分布式共识(一致性)算法Raft

    分布式共识及Raft简介 所谓分布式共识 consensus 与CAP理论中的一致性 consistency 其实是异曲同工 就是在分布式系统中 所有节点对同一份数据的认知能够达成一致 保证集群共识的算法就叫共识算法 它与一致性协议这个词也
  • PyTorch 更改训练好的模型 继续训练

    目录 直接加载预训练模型 加载部分预训练模型 冻结部分参数 训练另一部分参数 微改基础模型预训练 微改基础模型 简单预训练 直接加载预训练模型 如果我们使用的模型和原模型完全一样 那么我们可以直接加载别人训练好的模型 my resnet M
  • SHADER学习笔记(一):Surface Shader

    Surface Shader是Unity为了方便shader编写提供的特殊功能 它对底层的vertex fragment shader做了封装 省去了一些重复代码编写的工作量 我的理解是它同时具有vertex fragment shader
  • [CISCN 2022 初赛]login_normal

    叠甲 菜 很菜 就是懂一点但是不多 可能涉及的错误会很多 有错误欢迎指出 同时对于这个疑问有解答的也欢迎留言 总之就是很菜 QAQ 这一道题 首先要考代码审计 来猜它这个 login 的格式 然后在通过它的 login 之后 通过传入可见字
  • 【Android】ViewModel原理分析

    概述 本文主要通过分析ViewModel源码解决以下两个疑问 1 ViewModel如何保证的唯一性 2 ViewModel如何保证数据不丢失的 为了解决这些问题 从ViewModel的构造开始 一般创建ViewModel的方法如下 Vie
  • 《消息队列高手课》内存管理:如何避免内存溢出和频繁的垃圾回收?

    不知道你有没有发现 在高并发 高吞吐量的极限情况下 简单的事情就会变得没有那么简单了 一个业务逻辑非常简单的微服务 日常情况下都能稳定运行 为什么一到大促就卡死甚至进程挂掉 再比如 一个做数据汇总的应用 按照小时 天这样的粒度进行数据汇总都