跳到主要内容
版本:master

内存管理

本章主要内容包括内存管理的概念、常见的内存管理方式、 GS 的 GC 内存管理。适用于缺乏内存管理基础概念或缺乏 GC (Garbage Collection,垃圾回收)相关知识的同学。读完本章你应当能够理解内存管理在项目开发中的重要性,了解常见的内存管理方式、理解 GS 的 GC 内存管理机制。

1 什么是内存管理

内存管理是操作系统或应用程序运行时环境对计算机主内存(RAM)资源的分配、使用、回收和保护的策略与机制的总称。可以将其比喻为一个高效的“图书馆管理员”,它负责:

  • 分配:当程序需要内存时(如创建变量、对象),管理员从书库(内存空间)中找出空闲的“书架格”(内存地址)分配给程序。
  • 回收:当程序不再需要某块内存时(如变量超出作用域),管理员需要及时收回这些“书架格”,以便后续重新分配使用。
  • 保护:确保一个程序不能随意访问或修改其他程序的内存区域,防止数据被破坏或泄露,保证系统的稳定性和安全性。

2 为什么内存管理至关重要

内存是一种有限且关键的资源,低效或错误的内存管理会直接导致严重问题:

  • 内存泄漏: 程序未能释放不再使用的内存。如同从图书馆借书永不归还。随着程序运行,泄漏的内存不断累积,最终耗尽所有可用内存,导致程序或系统变慢、崩溃。

  • 悬空指针/野指针: 程序释放了某块内存后,未能清空指向它的指针。这个指针就像了一个指向已归还书籍的错误索书号。后续若通过该指针访问内存,行为不可预测,通常会导致程序崩溃或数据损坏。

  • 内存溢出: 程序申请的内存超过了系统能提供的最大可用内存。直接导致申请失败,程序异常终止。

因此,一套优秀的内存管理机制是保障程序健壮性、稳定性和性能的基石。

::: tip 以默认的 page-alloc 参数启动的 driver 的最大申请内存为 4GB :::

3 内存管理方式

3.1 手动内存管理

  • 做法: 程序员像“精密工程师”,需要显式地编写代码来申请内存(malloc/new)和释放内存(free/delete)。
  • 优点: 控制精准,效率极高。
  • 缺点: 极易出错。忘记释放导致内存泄漏;释放了还在用的内存导致悬空指针/程序崩溃。(这是小白们需要避免的痛点)

3.2 自动内存管理

  • 做法: 语言运行时系统自带一个“保洁员”(GC),它会自动跟踪哪些内存还在使用,哪些已经不再需要,并自动进行回收。
  • 优点: 安全(几乎不可能出现内存管理错误)、省心(开发者专注于业务逻辑,生产力更高)。
  • 缺点: GC活动需要消耗计算资源,可能会在不可预测的时刻引发短暂的停顿(Stop-The-World),但 GS 采用多线程无停顿 GC (Pauseless GC)技术,通过在后台线程执行大部分GC工作,极大减少了应用程序的停顿时间。

4 GS 如何管理内存:自动垃圾回收(GC)

你可以把计算机的内存想象成你的房间。当你创建变量、对象时,就像往房间里增加一个盒子,在里面放东西(数据)。

当你不再需要这些东西时,在一些编程语言(如C/C++)中,你必须自己扮演“保洁员”,亲自申请盒子,并在用完后果断地扔掉它。这很高效,但忘了扔的话,房间就会变得混乱不堪(内存泄漏);扔错了还在用的东西,程序就会崩溃(非法访问)。

在 GS 中,我们有一位贴心、勤劳的自动保洁员——垃圾回收器(Garbage Collector, GC)。你只需要专注于从房间里取用盒子,享受创造的过程。当你用完一个盒子并把它放在一边后,这位保洁员会自动识别并帮你清理掉它,让你的房间始终保持整洁。
简单来说,GS会自动管理内存,让你从手动清扫房间的负担中解放出来

4.1 基本原理

在GS中,垃圾回收机制是自动执行的,通过可达性分析来确定哪些对象可以被回收。GS 会从一组根对象开始遍历所有从跟对象可达的对象,并将其标记为 "存活"。当一个对象不再被任何引用时,也就是其不可达时,垃圾回收器会自动将其标记为可回收对象。并在独立的GC线程中释放内存。

让我们写一个简单的测试用例:

  • get_system_info()用于获取系统信息,该函数会创建一个全新的用于保存系统信息的表(相当于会申请一片内存用于存储信息)。
  • 循环中循环一百万次不断调用get_system_info()申请内存,并输出当前 drvier 使用的总内存信息。
  • 每次循环中get_system_info()申请的 map 表会被赋值到 object 变量sys_map
  • driver 默认的 4GB 内存空间是不足以保存一百万个get_system_info()申请的 map 表的。

示例如下:

map sys_map = nil;
array arr = nil;
void loop_create_map()
{
printf(HIC"Init memory useage is %d MB\n"NOR, mem_stat().total_used_size / 1024 /1024);
arr = array.allocate(1000000);
for(int i = 0 upto 1000000 - 1)
{
sys_map = get_system_info();
if(i % 10000 == 0)
printf("\rCreated map count is %d, total used memory size is %d MB\n", i, mem_stat().total_used_size / 1024 /1024);
}
}
loop_create_map();

示例 4-1:垃圾回收示例
运行如上示例,我们可以看到总使用内存在 95MB - 100MB 之间不断波动,而不是持续的增长。实际上,每次循环赋值都会覆盖掉在object变量 sys_map 保存的的上一次get_system_info()申请的 map 表的引用。GC 会识别到这个旧 map 表没有被任何合法的位置引用,并回收相应的内存空间。

好的,接下来让我们尝试在数组中保留历史的 get_system_info()申请的 map 表引用,让我们看看会发生什么?示例如下:

map sys_map = nil;
array arr = nil;
void loop_create_map()
{
printf(HIC"Init memory useage is %d MB\n"NOR, mem_stat().total_used_size / 1024 /1024);
arr = array.allocate(1000000);
for(int i = 0 upto 1000000 - 1)
{
sys_map = get_system_info();
arr[i] = sys_map;
if(i % 10000 == 0)
printf("\rCreated map count is %d, total used memory size is %d MB\n", i, mem_stat().total_used_size / 1024 /1024);
}
}
loop_create_map();

示例 4-2:合法引用 map 导致内存泄漏示例
运行如上示例,我们可以看到 total used memory size 的数组不断增长到接近 4GB。然后 driver 警告 Warning(3006): Memory is running out! ,此时 driver 会多次触发 GC 尝试回收内存,并尝试在回收内存后重新申请内存。由于我们的所有get_system_info() 申请的 map 表引用均被保存在数组中,对应的内存无法被释放,最终给出多次警告 Warning(3006): Failed to allocate GC memory (wants: 68 byte) at ..., retrying... (2/10). 之后报错 Error(-101): Can not allocate GC memory (wants: 96 byte) at ...

4.2 触发机制

GS支持分代回收机制,当满足以下条件时,GS会触发一次垃圾回收:

  • 当执行 gc.start(false) 时,触发一次 Minor GC。
  • 当执行 gc.start(true) 时,触发一次 Full GC。
  • 当新分配的内存(即自上一次GC完成之后的内存增长量)大于或等于 MinorGC 阈值1时,触发一次 Minor GC。
  • 当新分配的内存(即自上一次GC完成之后的内存增长量)满足 FullGC 触发条件2时,触发一次 Full GC。
  • 当尝试为GC实例分配内存失败时,触发一次 Full GC。

注释:

  1. MinorGC 阈值默认为:

    OLD_SIZE / 16 + YOUNG_SIZE / 2

    其中,OLD_SIZE 是当前的老年代体积,YOUNG_SIZE 是上一轮GC完成后的年轻代体积。如果距离上一次GC的时间少于 5ms 并且有足够的 GC 工作线程,实际检查时还会有额外的修正系数,以避免过于频繁的GC触发。

  2. FullGC 触发条件为:

    先决条件

    新分配的内存大于等于:

    LAST_FULL_GC_OLD_SIZE + OLD_SIZE / 16 + YOUNG_SIZE / 2

    满足先决条件后,还需满足下列三个条件之一:

    OLD_SIZE >= LAST_FULL_GC_OLD_SIZE * 2
    距离上一次 FullGC 3秒以上,且 OLD_SIZE >= LAST_FULL_GC_OLD_SIZE * 3/2
    距离上一次 FullGC 180秒以上,且 OLD_SIZE >= LAST_FULL_GC_OLD_SIZE * 5/4

    LAST_FULL_GC_OLD_SIZE 是上一次 Full GC 之后的老年代体积。

4.3 GC配置

可以通过命令行参数配置GC的阈值,常用的相关的配置项如下:

--gc-quota-to-minor-gc <MinorGC 阈值>

单位为字节,合法取值范围为 16KByte 到 1TByte;用于替换默认的 MinorGC 阈值计算公式。
--gc-count-upper-bound <单次内存计数分配上界>

设置单次内存分配计数的上限,单位是字节,默认值为 1MByte;设置为0表示不设上限。

当一次分配超过此值的内存大小时,计数最大只会计入此值,超过部分不计入内存分配计数。

此机制能降低部分场景下的GC触发频率,但如果设置得过低会抑制GC的触发,导致内存占用偏高。

4.4 引用泄漏

尽管 GS 的 GC 垃圾回收会为我们解决绝大多数情况下的内存管理问题,但其并未完全消除内存泄露的可能。
在带有GC的语言中,​​内存泄漏的本质是"无意的对象保留"​​。当对象不再被程序使用,但由于某些原因忘记移除引用导致对象仍"可达"时,就会发生内存泄漏。

示例 4-2可以视为一个简单的内存泄漏样例,但在实际项目的中的内存泄漏情况却更加复杂。其引用路径可能极长、隐蔽、难以发现。

在后续的调试技巧章节中,我们会介绍 mem record 内存统计工具以及 gc snapshot 内存快照工具用于解决此类内存泄露问题。

5 总结

恭喜,你现在已经掌握了内存管理的核心概念,理解了手动和自动内存管理的区别,熟悉了GS的垃圾回收机制及其触发条件,并认识到 GC 虽能自动管理内存,但仍需警惕内存泄漏问题。接下来让我们了解 GS 的流程控制语句及内容。