内存管理
本章主要内容包括内存管理的概念 、常见的内存管理方式、 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。
注释:
-
MinorGC 阈值默认为:
OLD_SIZE / 16 + YOUNG_SIZE / 2其中,
OLD_SIZE是当前的老年代体积,YOUNG_SIZE是上一轮GC完成后的年轻代体积。如果距离上一次GC的时间少于 5ms 并且有足够的 GC 工作线程,实际检查时还会有额外的修正系数,以避免过于频繁的GC触发。 -
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/4LAST_FULL_GC_OLD_SIZE 是上一次 Full GC 之后的老年代体积。