
各位编程爱好者们,早上好!
我最近思考了一个很有意思的问题。
我们已经有了 C++ 的 RAII、Java 的 GC、Rust 的所有权机制,甚至还有各种内存分析工具,为什么几十年过去了,程序员仍然无法彻底解决内存泄漏?
我觉得这里有一个很值得思考的问题:
内存泄漏是否不仅是内存分配与释放的问题,还涉及程序无法准确判断一块内存是否还有用?
各位可以稍稍思考一下,我们接着往下看。
什么是内存泄漏
我们先看两个例子。
C++:忘记释放内存
cpp
void test() {
int* p = new int(100);
}
函数执行结束,p 消失了,但堆上的内存没有被释放。
这是最经典的内存泄漏:程序失去了访问这块内存的途径,也没有办法再释放它。
这种问题其实已经有相当成熟的解决方案。
例如 Rust:
rust
fn test() {
let p = Box::new(100);
}
函数正常返回时,p 被销毁,堆上的内存自动释放。
看起来问题解决了,对不对?
但我们再看第二种情况。
Java:内存一直被引用
java
import java.util.ArrayList;
import java.util.List;
class Cache {
static List<byte[]> data = new ArrayList<>();
static void add() {
data.add(new byte[1024 * 1024]);
}
}
每调用一次 add(),就新增一个 1 MiB(1024 × 1024 字节)的数组,此外还有对象和列表管理的开销。
即使有垃圾回收器,内存也可能一直增长。
为什么?
因为这些数组始终可以通过静态字段 data 访问。
当然,内存增长本身不等于泄漏。如果这些数据已经不再需要,却仍然被保留,这就是通常所说的逻辑内存泄漏。
GC 能判断对象是否可达,却不能判断这个对象在业务上是否还有用。
这就引出了内存泄漏真正困难的地方。
内存管理机制无法理解程序员的意图
Kotlin:假设我们有一个缓存
kotlin
object Cache {
val images = mutableMapOf<String, Bitmap>()
}
随着程序运行,图片越来越多。
现在的问题是:
什么时候应该删除某一张图片?
- 10 秒没有访问?
- 内存使用超过 100 MB?
- 用户关闭当前页面?
- 还是以后永远不会再访问这张图片?
这些判断都依赖具体业务。
编译器、GC 或者操作系统很难自动知道答案。
从计算机的角度来看,只要对象仍然可达,它就可能在未来被访问。
但从程序员的角度来看,这个对象可能早就没有价值了。
因此:
内存的生命周期和业务对象的有效生命周期,并不总是一回事。
这问题 Rust 也没解决
Rust 非常擅长解决资源所有权不清晰的问题。
但 Rust 并不保证程序绝对不会发生内存泄漏。
例如:
rust
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: RefCell<Option<Rc<Node>>>,
}
fn main() {
let a = Rc::new(Node {
next: RefCell::new(None),
});
let b = Rc::new(Node {
next: RefCell::new(None),
});
*a.next.borrow_mut() = Some(Rc::clone(&b));
*b.next.borrow_mut() = Some(Rc::clone(&a));
}
这里形成了一个循环引用。

即使 main() 返回、局部变量被销毁,两个节点仍然通过 Rc 相互持有。
因此它们的强引用计数都不能归零,节点不会执行析构,堆内存也不会通过正常的析构流程释放。进程退出后,操作系统仍会回收其占用的内存。
Rust 提供了 Weak 来打破这种循环,但前提是程序员知道哪条引用关系不应该拥有对象。
这再次说明,语言能够帮助管理所有权,却不一定能替程序员决定合理的所有权关系。
另外,Rust 还允许使用 std::mem::forget 主动阻止析构。
对了,内存泄漏并不属于 Rust 所承诺杜绝的内存安全错误。
为什么这活儿计算机也无法判断
这个问题其实就更复杂了。
Kotlin:假设程序中有一个对象
kotlin
val data = createLargeObject()
while (someCondition()) {
doSomething()
}
use(data)
假设 someCondition() 和 doSomething() 都不会访问 data,也不会通过异常等方式提前退出。
在循环执行期间,data 将来还会不会被使用?这取决于循环能否最终结束。
如果循环永远不结束,那么 data 永远不会被使用。
如果循环最终结束,那么在后面的 use(data) 执行前,就不能随意丢弃它所需的数据。
问题来了:对于任意程序,我们无法设计一个通用算法,准确判断它最终是否会结束。
这与计算机科学中著名的**停机问题(Halting Problem)**有关。
不过要注意,停机问题并不是说所有内存泄漏都无法检测。很多泄漏可以通过静态分析、运行时分析或特定语言约束发现。
它说明的是:如果我们要求一个通用算法,在任意程序、任意执行情况下,都准确理解未来行为和对象是否还有用,那么就会遇到理论上的限制。
未来能彻底解决吗
针对这个问题,我认为要区分几种目标:
| 目标 | 目前的情况 |
|---|---|
避免忘记调用 free |
RAII、GC、Rust 已经大量解决 |
| 避免悬垂指针、重复释放 | Rust 等语言可以提供很强的安全保证 |
| 自动回收不可达对象 | 追踪式 GC 已经能够实现 |
| 自动识别所有无用但仍可达的对象 | 没有通用、完全准确的解决方案 |
| 保证程序内存永远不异常增长 | 还涉及缓存策略、业务需求和资源预算 |
所以现在的内存管理技术,其实已经解决了大量机械性的问题。
但没有彻底解决的是:一个对象究竟应该活多久?
这个决定往往属于程序设计,而不是内存分配器。
多说一点
这个问题还可以再推进一步。
GC 和 Rust 的所有权机制,本质上是在回答两个不同的问题:
- GC:这个对象是否仍然能够被访问?
- Rust:谁拥有这个资源,什么时候应该执行析构?
但还有第三个问题:
这个对象是否还应该存在?
前两个问题可以通过相对明确的规则处理,而第三个问题与业务语义密切相关。
也正因为如此,即使未来出现比 Rust 更严格的编程语言,或者比现有 GC 更先进的垃圾回收器,也不代表内存泄漏会彻底消失。
我甚至认为,程序员一直没有彻底解决内存泄漏,不是因为我们还没发明足够聪明的内存管理算法,而是因为内存管理机制能够理解对象之间的关系,却不一定理解这些对象存在的意义。