首页 / 文章 / 手动内存管理的价值:为何在GC时代仍需掌控内存
← 返回
AI技术

手动内存管理的价值:为何在GC时代仍需掌控内存

✍️ zhirenhun 📅 2026/5/28 👁 52 阅读 ⏱ 26 分钟
手动内存管理的价值:为何在GC时代仍需掌控内存

手动内存管理的价值:为何在GC时代仍需掌控内存

现在,编写软件比以往任何时候都更容易,而不需要了解堆或栈。JavaScript、Python 甚至 Go 等语言内置了运行时内存管理机制,对于大多数现代消费级硬件来说,性能损耗是可以接受的。然而,消费级硬件并非计算的终点。在嵌入式系统、游戏开发和高性能计算等领域,手动内存管理带来的性能优势仍然值得付出努力。

我甚至认为,即使日常工作中用不到,学习手动内存管理也是一项有价值的技能。它能让你更深入地理解计算机的工作原理,以及如何优化代码以获得更好的性能。

当你理解了内存管理的工作原理,你的系统在底层是如何运作的,作为第三阶效应,你也会更理解自己的代码和软件。此外,你还能使用像 Zig、Odin 和 C3 这样非常酷且时髦的、以手动内存管理为核心特性的语言。

常见陷阱

以下是进行手动内存管理时需要注意的一些问题:

悬空指针

悬空指针是指向已释放内存的指针。想象一下,你把一本书借给朋友并做了记录,当你要回来时,他们告诉你已经把书扔了——真没礼貌,但现在你的笔记指向了一本不再存在的书。这个糟糕的类比暂且不表,悬空指针就是一个指向已释放内存的指针,如果你试图访问它,就会触发未定义行为(undefined behavior),可能导致崩溃、数据损坏和安全漏洞。

双重释放

继续我糟糕的类比:假设你把一本书借给朋友,现在你想扔掉它,所以你首先得从朋友那里拿回来——但朋友已经粗鲁地把书扔了,尽管你自己也想扔掉它。于是你试图扔掉一本已经被扔掉的书。不管怎样,双重释放(double free)就是你试图释放已释放的内存,这会导致未定义行为,进而导致崩溃、数据损坏和安全漏洞。

释放后使用

我决定坚持我的糟糕类比。假设你把一本书借给朋友,然后朋友已经扔掉了书你却试图去读它——你会得到一本不存在的书,困惑且沮丧。这就是释放后使用(use after free)——你试图访问已释放的内存,得到未定义行为,导致……你猜对了,崩溃、数据损坏和安全漏洞。

内存泄漏

你的朋友住在一个小公寓里,你借给他们一本书,然后你又借了10本书,然后你不停地借书给他们——像疯狂的图书出借者一样,从不索要回来。最终,朋友会有一大堆无法管理的书,他们的公寓里再也没有空间了。这就是当你分配内存但从不释放时发生的情况——你会耗尽内存,应用程序崩溃。这叫内存泄漏(memory leak),如果管理不当,可能是一个严重问题。

缓冲区溢出

你的朋友有点强迫症,他们专门为你的书预留了一个书架,只能放大约10本书,因为他们非常喜欢数字10。所以你这周借了10本,下周又借了1本。你的朋友崩溃了,因为11本书放不下只能容纳10本书的书架。缓冲区溢出(buffer overflow)本质上就是你试图向缓冲区写入超出其容量的数据。猜猜这会导致什么?对的——崩溃、数据损坏和安全漏洞。

内存碎片

你那强迫症的朋友造了一个更大的书架,并有了新的摆放策略,想按字母顺序或颜色排序。你继续杂乱地借书,导致书架上的书之间有大量空白空间。书架本身变大了,但书之间的空白空间被浪费了,无法用于其他用途。这就是当你进行大量小规模分配和释放时会发生的情况——导致内存碎片(memory fragmentation),进而导致内存使用效率低下,最终耗尽内存。

不正确的所有权

我实在不知道怎么把糟糕的类比套用到这个上了。你和你的……室友?同时试图修改书架上同一本书,这在时空连续统中打开了一个裂缝,结果你产生了一本同时存在于多种状态的量子书,你完全不知道哪个状态才是正确的。不正确的所有权基本上就是多个指针指向同一块内存,每个都试图修改它。这会导致竞争条件(race conditions)和数据损坏,而且非常难以调试。


如你所见,手动内存管理比使用垃圾回收语言复杂得多,但也更有成就感和乐趣。你可以更多地控制应用程序的内存使用,真正针对性能和已知硬件优化你的软件。然而,你也必须非常小心,注意细节,因为一旦犯错可能导致非常严重的后果。

如何避免这些陷阱

直接用垃圾回收语言就好了,问题解决了。文章结束。开玩笑的——如果你想进行手动内存管理,有一些方法可以避免这些陷阱。迄今为止你已经解决了最大的问题:理解陷阱是什么(希望如此)。但理解问题只是旅程的一半,解决方案是另一半。让我们思考一些解决办法。

心智模型

你首先需要建立一个内存管理的心智模型。让我们从两个能直接描述其功能的术语开始:

说到栈,我们总是默认用一摞盘子来类比,因为这是最直观的方式——你只能从顶部添加或移除盘子,因为你不想打碎任何盘子。所以栈是一块用于存储局部变量和函数调用信息的内存区域。它以"后进先出"(LIFO)的方式组织,意味着最近添加的项目最先被移除。栈通常比堆快得多,因为它有固定大小,不需要动态内存分配。

至于堆,想象一堆杂乱无章的衣服。你可以非常快速地往衣服堆上扔衣服,也可以快速从堆里抓取衣服,但你不知道任何一件衣服在哪里,而且很快就会变得非常混乱。堆是用于动态内存分配的内存区域。它比栈的组织方式更灵活,意味着你可以按任意顺序分配和释放内存。堆通常比栈慢,因为它需要动态内存分配和释放。

我可以单独写一整篇文章来讲栈和堆,但为了保持你的注意力,我认为这个心智模型足够让你入门了。

工具与技巧

利用你可用的工具。我们有调试器(debugger)和消毒器(sanitizer)、日志和性能分析工具,甚至还有静态分析工具,可以帮助你在问题变成真正的问题之前捕获它们。使用它们——它们存在是有原因的,可以节省你大量时间和头痛。

最佳实践与习惯

工具固然很好,但手动内存管理的核心是你——开发者。你需要有一套好的习惯和实践来首先避免这些陷阱。通过深入手动内存管理的世界,你为自己的代码承担责任,并且必须约束自己如何管理内存。以下是一些可以帮助你避免这些陷阱的最佳实践和习惯:

不同语言如何处理手动内存管理

我们暂时结束糟糕的类比,讨论一下不带垃圾收集器的不同语言如何处理手动内存管理,以及它们如何帮助避免上述陷阱。

C

C 是一门经典的优秀语言,它的简洁之美永恒不朽。我很欣赏它的极简以及仅凭原始 C 和标准库就能做多少事情。众所周知,C 使用 mallocfree 进行手动内存管理,开发者需要确保正确使用它们。C 没有任何内置工具来帮助你避免手动内存管理的陷阱,所以使用它时需要非常小心。不过,C 生态中有许多第三方库和工具可以帮助管理内存,例如 Valgrind 用于检测内存泄漏,AddressSanitizer 用于检测缓冲区溢出和释放后使用错误。

但最近 C 生态中也有了一些新的工具和辅助,例如 Fil-C。

Fil-C

Fil-C 是一个改进版的工具链,以极小的成本为 C 和 C++ 添加了大量内存安全性,无需将代码重写为其他语言或范式——那可能让你抛弃数十年经过实战检验的代码和 bug 修复。

Fil-C 提供的内容:

这些组件共同使 Fil-C 在保持 C 的简洁性和生态的同时,增加了针对最常见和危险的内存错误的保护层。

神奇之处在于:当你用 Fil-C 编译器编译现有的 C 或 C++ 代码时,它会自动重写你的代码以添加安全检查、插入运行时检查并链接到 Fil-C 运行时——所有这些都不需要你修改一行代码。这意味着你可以将现有的 C 或 C++ 代码库用 Fil-C 编译,获得内存安全保证,而无需重写代码或改变编程范式。

本质上,像这样的代码:

int x = arr[i];

会编译成类似这样的东西:

check_pointer_bounds(arr, i);
load_value(arr + i);

C++

啊,没错——每个人都喜欢讨厌的语言,但也是对整个软件行业影响最大的语言之一。我知道 C++ 名声不好,事实上我甚至写了一整篇文章为 C++ 辩护,有时间可以看看。回到正题,C++ 自诞生以来已经发展了很多,增加了大量特性来帮助以安全和可控的方式进行手动内存管理,例如 smart pointers(智能指针),它们基本上是对原始指针的包装,自动管理它们指向的内存。

此外,现代 C++ 强烈鼓励使用容器而不是手动内存分配。通常如果你使用以下内容:std::vectorstd::stringstd::mapstd::unordered_map 等,底层会使用手动内存管理,但你不需要担心,因为容器会为你处理一切。

如果你使用类似这样的代码,问题就来了:

int* arr = new int[10];

这是一个原始指针,你需要手动管理它的内存。同样的效果可以通过以下方式实现:

std::vector<int> arr(10);

这会给你一个 10 个整数的向量,你不需要担心内存管理。

Zig

我最喜欢的语言之一。Zig 是一种以手动内存管理为核心特性的现代编程语言。它对内存管理有独特的方法,旨在安全和高效,同时让开发者完全控制内存使用。你可以通过理解 Zig 分配器并在代码中正确使用它们来实现这一点。Zig 还有 defer 关键字,允许你安排在块结束时运行的清理代码,这可以帮助你避免内存泄漏。

基本上,Zig 为你提供了安全高效管理内存所需的工具,几乎不需要你付出额外努力,且基本上没有开销或显著的性能损失——这是双赢的局面。

此外,Zig 可以与 C 互操作,甚至可以用作 C 或 C++ 编译器。

Odin

我在《Odin 只是更无聊的 C 吗?》中讨论了 Odin 如何管理内存。标题有点标题党,对此我表示抱歉——我的意思是这是好的方面。我喜欢一种简单直接、不碍事、让我高效工作的语言。Odin 正是这样的语言,我很喜欢。我真的建议阅读我那篇关于 Odin 的文章,因为它在内存管理方面有一种非常酷且独特的方法,值得学习。

Odin 在内存管理方面的有趣之处在于,它不试图向你隐藏内存。相反,它通过分配器(allocator)使内存管理变得显式和灵活。在 Odin 中,你可以像传递普通值一样传递分配器,并决定程序的某个部分应该如何处理内存。许多 API 默认使用 context.allocator,这给了你一个方便的默认值,同时在需要时仍允许你覆盖它。

这意味着你可以根据问题轻松切换策略。也许你想为短期数据使用 arena 分配器,或者为性能敏感子系统使用自定义分配器。Odin 使这类事情变得简单。

在实践中,这导致了非常可预测的代码。你知道分配发生在哪里,你知道谁拥有它们,你可以在不重写一半程序的情况下更改分配策略。这与 Odin 简化和控制的总体理念非常吻合。

C3

我还没有时间深入探索 C3。我稍微摆弄了一下,喜欢他们正在走的方向。

从目前来看,C3 强烈依赖于基于作用域的分配和确定性清理。其思想是,你分配的大部分内存只需要在函数或块的生命周期内存在。与其手动追踪每个分配并在之后释放,语言及其库使得将分配绑定到作用域并在该作用域结束时清理变得容易。

C3 还在标准库中提供了像 mem::new_arraymem::alloc_array 这样的辅助工具,因此你不必总是处理原始的分配原语。该项目还推广使用临时或 arena 风格的分配器来处理短期数据。

我喜欢这种方法的地方在于,它解决了内存错误的最大来源之一——生命周期混淆。如果分配明确属于某个作用域,很多错误就自然消失了。

我仍然需要花更多时间了解 C3 才能形成强烈的意见,但从目前看到的来说,它看起来是另一个让手动内存管理变得实用而非痛苦的有趣尝试。

Rust 之旅

老实说,我不认为 Rust 是一种手动内存管理语言。它确实有独特的内存管理方法,但任何时候都不需要你手动分配和释放内存。它内置的所有权系统会自动处理这一切。Rust 本质上是一组编译器规则,只是把内存管理的责任从开发者身上移开了——如果这听起来很熟悉,那是因为这正是垃圾回收语言所做的。它们假设你会搞砸,并为你处理它,Rust 只是在编译时而不是运行时做同样的事情。

这是一个好方法吗?老实说,不是。我认为这是一个糟糕的方法,带来的问题比解决的还多,而且最终它甚至不能保证完全的内存安全——这就是为什么会有像"内存泄漏被认为在 Rust 中是内存安全的"这样的著名引语。目前为止,Rust 在生产环境也未能给人留下深刻印象,比如 Cloudflare 因为 Rust 代码中的内存问题而宕机——这是 Rust 专门设计要预防的问题。

你可以看出我不是 Rust 的粉丝,我认为它根本不是一个好语言。它大张旗鼓地来了,许下了疯狂的承诺,但迄今为止一个都没有兑现——不过这是另一篇文章的话题了。

何时应该使用手动内存管理?

手动内存管理非常强大,如果你理解得好,可以编写出极其高效和性能优秀的代码,但它也伴随着大量的责任和犯错的可能性。那么,什么时候应该使用手动内存管理呢?

性能关键代码

在高性能系统中,即使是微小低效也会积累。游戏、交易系统、实时音频处理和高频网络软件通常受益于可预测的内存使用和确定性清理。

垃圾回收器可能会引入暂停或不可预测的分配模式。手动内存管理允许开发者精确控制分配发生的时间和释放的时间。

嵌入式系统

嵌入式设备通常运行在极其有限的内存和 CPU 资源上。在这些环境中,垃圾回收器可能太昂贵或太不可预测。

手动内存管理让开发者仔细控制内存的使用方式,确保系统永远不会超过其约束。

系统编程

操作系统、驱动程序、编译器和网络栈通常需要直接控制内存。在这些领域,你经常需要显式管理内存布局、与硬件交互,或在根本不存在运行时的环境中操作。

大型现有代码库

有时原因很简单。世界上有数十年经过实战检验的 C 和 C++ 代码。将这些系统重写为不同的范式或语言通常不现实,或者会导致抛弃数十年积累的代码和 bug 修复。

在这些情况下,改进工具链、添加安全层或采用更好的实践比从头开始重写更有意义。

而这正是关键所在。手动内存管理不是因为你"可以"才去用的。而是当你需要更高级运行时无法始终提供的控制力、可预测性和性能时才使用的。

理解它还会改变你对软件的整体思考方式。一旦你知道了内存分配、释放和重用背后实际发生的事情,编程的许多部分都会变得更有意义。

结论

手动内存管理有着危险和过时的名声,但这种名声大多来自误解和误用。

是的,陷阱是真实的。但性能优势和对软件工作原理的更深理解也是真实的。

现实是,手动内存管理仍然无处不在。操作系统、游戏引擎、数据库、嵌入式系统、网络栈和高性能软件每天都依赖它。整个行业依赖手动管理内存的系统运行,并且已经成功运行了几十年。

发生变化的是其周边的生态系统。我们现在有了更好的调试工具、消毒器、静态分析器和改进的语言特性,使得编写安全的手动内存代码比以前更加可行。现代 C++、Zig、Odin 和 C3 都在探索不同的方式来给予开发者控制权,同时减少不必要的痛苦。

归根结底,手动内存管理只是工具箱中的另一个工具。不是每个项目都需要它,对于许多应用程序来说,垃圾回收语言完全够用。但当性能、确定性和底层控制很重要时,理解手动内存管理就变得非常有价值。

说实话,一旦你理解它,它就不再可怕了。它只是编写优秀软件的另一部分。

感谢你的时间,这是一篇长文章,我真诚希望你喜欢它。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

programming zig systems memory
← 上一篇
神经算法推理:当神经网络学会执行经典算法
下一篇 →
用AI构建数据库性能测试工具:诚实复盘

📌 相关推荐

走向 Agent 记忆的标准模型
2026/5/31
浏览器内部的悄然 AI 战争
2026/5/31
为什么 AI 会忘记你说过的话(以及如何解决)
2026/5/31
← 返回文章列表