Appearance
17.2 内存泄漏属于内存安全
在编程语言设计这个层面,“内存泄漏”是一个基本上无法在编译阶段彻底解决的问题。在许多场景下,什么是“内存泄漏”、什么不是“内存泄漏”,本身就没有一个完全客观的评判标准。它实质上跟程序员的“意图”有关。程序很难自动判定出哪些变量是以后还会继续用的,哪些是不再被使用的。
即便是在使用 GC 做内存管理的环境下,程序员也有可能不小心将不应该被使用的变量错误引用,造成无法自动回收的问题。因为 GC 判定一个对象是否可回收的标准是,这个对象有没有被“根”对象直接或者间接引用。假如一个对象本来是应该被释放的,可是因为逻辑问题,没有把指向它的有效引用全部释放,那么 GC 依旧把它判定为不可回收。我们可能在不经意的情况下,造成了不再需要继续使用的对象被生命周期更长的对象所引用。面对这样的情况,GC 也会显得无能为力。比如,在 android 编程领域,我们可能会在注册回调函数的时候把一个较大的 activity 引用传递过去,结果 activity 应该被销毁的时候,由于还有其他变量继续持有指向它的引用,从而导致该 activity 变量无法正常被释放,这种现象被称为“Context 泄漏”。解决这个问题的办法只能是,在必要的地方使用“弱引用”(Weak Reference),避免“强引用”对变量生命周期的影响。解决引用计数的循环引用的办法与此类似,也是一样用“弱引用”来打破循环,避免“强引用”对生命周期的影响。再比如在 javascript 中注册一个定时器,而定时器不小心引用了许多大对象,这些对象会随着闭包加入到主事件循环队列中,也会造成类似的结果。在绝大部分情况下,GC 给我们带来了方便。但是,程序员也千万不能因为有 GC 的辅助,而忽略对变量的生命周期的设计考量。
在 C++和 Rust 中是一样的,如果出现了循环引用,那么只能通过手动打破循环的方式来解决内存泄漏的问题。编译器无法通过静态检查来保证你不会犯这个错误。
内存泄漏显然是一种 bug。但它跟“内存不安全”这种 bug 的性质不一样。“内存泄漏”是对“正常数据”的“应该执行但是没有执行”的操作,“内存不安全”是对“不正常数据”的“不应该执行但是执行了”的操作。从后果上说,“内存不安全”导致的后果比“内存泄漏”要严重得多,如下表所示。
操作 | 正常数据 | 不正常数据 |
---|---|---|
执行了 | ✔️ | 内存不安全 |
没执行 | 内存泄漏 | ✔️ |
语言的设计者当然是希望能彻底解决内存泄漏的问题。但是很可惜,这个问题恐怕不是在语言层面能彻底解决的问题 所谓“彻底解决”的意思是,用户无论使用何种技巧,永远无法构造出内存泄漏的情况 。Rust 语言无法给出这样的保证。笔者也不认为 GC“彻底解决”了内存泄漏的问题。内存泄漏当然是不好的事情,作为开发者,我们应该尽可能避免内存泄漏现象的发生。 然而,需要强调的是,内存泄漏不属于内存安全的范畴,Rust 也不会在语言设计层面给出一个“免除内存泄漏”的承诺。
To put it another way,Rust gives you a lot of safety guarantees,but it doesn’t protect you from memory leaks(or deadlocks,which turns out to be a very similar problem).