Skip to content

Latest commit

 

History

History
88 lines (53 loc) · 6.97 KB

q1.md

File metadata and controls

88 lines (53 loc) · 6.97 KB

为什么进程退出的时候没有内存泄漏?


最近,我一直在看关于内存相关的东西,有一个学弟问了我一个东西觉得还挺有意思的,就是他的C程序(很简单的一个)的内存分配malloc()free()没有成对的出现,但是他惊奇的发现即使运行很多次(数量比较大,十几万次),系统并没有崩溃,甚至可以说没有发生内存泄漏,听到这里,我就懂了他对什么没有理解了。

学过C语言的人都知道,我们的mallocfree要成对出现,否则会发生内存泄漏,没错,这句话本身没错,但是如果这句话脱离了一些语境,那么这句话就是错的了,这句话其实是针对一个程序因为内存不够导致崩溃来说的,对于一个长时间运行的程序,比如我们的web服务器,数据库管理系统,操作系统(of course~),那么你长时间的不释放掉一些内存就会出现很严重的问题,可能会出现内存不足而导致程序的崩溃,这就是说我们为什么一旦不用某些内存的时候需要调用free的真正含义,但是对于一个很小的程序,如果你在堆上面申请一些内存的话,那么在程序结束前即使你不释放的话,操作系统也会来帮你释放!

写过操作系统内核的人都知道在管理进程的时候,进程退出的时候,操作系统会回收进程的所有资源,所以从这个角度来看的话,只要操作系统是正确的话,进程退出的时候是没有内存泄漏。

那么如今我们还需要一个要程序员手动释放内存的语言吗?自己掌控内存的优势是什么?

我认为手动内存管理的价值主要体现在 (1) 底层程序的开发、(2) 受系统限制难以接受垃圾回收开销以及 (3) 需要高度实时性的系统开发上。

对于底层程序,比如操作系统,我们显然需要一个手动管理内存的开发方案。我们需要直接利用内存信息表等信息来分配内存,以供内核和上层应用程序利用。对于垃圾回收系统来说,其也需要运行在一个可以手动管理内存的开发方案上。

在嵌入式开发领域,很多机器的 CPU 主频极低、内存极其小,例如有些单片机的主频只有 20MHz,片上内存只有 64B。在这样的设备上进行应用开发,垃圾回收的开销可能比应用程序本身都要大。对于极小内存的设备,往往使用固定的内存地址;而对于内存稍大的设备,则引入手动内存管理。

最后,虽然现在出现了一些无暂停(zero GC-stop)的垃圾回收器设计,但是目前来看,普遍应用的垃圾回收器往往会引入定期的、较大的内存回收暂停。在这段时间,应用程序无法响应外部的一切交互,这种暂停对于一些高度实时的应用场景来说是无法接受的。

像golang这种具有垃圾回收,但在效率敏感的应用上也表现出了很好的效果。

从 Golang 的发展过程来看,早期版本孱弱的垃圾回收器极大地影响了 Golang 应用程序的性能表现。此外,目前来看 Golang 的性能表现还是要显著弱于相当一部分进行手动内存管理的开发方案。

当然,从现实角度来看,以绝大部分网络应用程序的性能需要来看,垃圾回收的性能影响并不是非常显著,事实上这些应用程序也普遍选择了带有垃圾回收的开发方案(如 Java、C#、Golang),因此垃圾回收的引入确实能够提高这些不需要极高性能的开发场景的工作效率。

所以为了那一点效率(有待于验证)引入可能内存泄露的风险到底值不值得?

综上,根据以上的讨论,处于以下几种情景时,我认为手动内存管理是非常必要的:

  • 操作系统与系统应用的开发,包括垃圾回收器的开发
  • 嵌入式和低性能设备的开发
  • 要求系统具备实时性的应用场景
  • 高度要求性能的应用场景

当然,对于目前的程序开发来说,我认为 75% 的情况下,以垃圾回收来交换可靠、高效的开发体验,还是有很显著的意义的。

此外,需要注意的是,引入垃圾回收器并不代表就不存在内存泄漏的问题。不恰当地构造的对象,以及内存回收器本身的设计缺陷,都可能导致应用程序发生内存泄漏。

最后,恰当地遵循一些程序设计规则,可以有效降低发生内存泄漏的风险,包括使用 RAII、所有权的概念。此外,2009 年出现的 Rust 语言通过引入生命周期的概念,提供了编译期检查的内存安全,将内存泄漏的风险降低到了最低。

或者说目前有语言将这两个特性结合起来吗?

这里举出几个例子,虽然和「可以认定为发生了内存泄露,这时候再启动垃圾回收机制」有一些距离,但是也算作是在手动内存管理和自动内存管理之间的一个平衡。

C++ 11 标准引入了智能指针,允许用户创建采用引用计数机制进行自动内存回收的对象 std::shared_ptr<T>

std::shared_ptr<Base> p = std::make_shared<Derived>();

std::cout << "Created a shared Derived (as a pointer to Base)\n"
          << "  p.get() = " << p.get()
          << ", p.use_count() = " << p.use_count() << '\n';
std::thread t1(thr, p), t2(thr, p), t3(thr, p);
p.reset(); // release ownership from main
std::cout << "Shared ownership between 3 threads and released\n"
          << "ownership from main:\n"
          << "  p.get() = " << p.get()
          << ", p.use_count() = " << p.use_count() << '\n';
t1.join(); t2.join(); t3.join();
std::cout << "All threads completed, the last one deleted Derived\n";

C++/CLR 中,既允许出现传统的指针和手动内存管理机制,又允许开发者创建在堆上分配的对象

G ^ g1 = gcnew G;
G ^% g2 = g1;
g1 -> i = 12;

Rust 采用 lifetime 实现了编译期确定的自动内存管理,但 Rust 也允许在 unsafe 块中手动分配内存和使用野指针

let address = 0x01234usize;
let r = address as *mut i32;

let slice: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };

至于「可以认定为发生了内存泄露,这时候再启动垃圾回收机制」的机制,可以认为是很难实现的。现有的垃圾回收器大体上可以分为追踪(tracking)和引用计数(reference count)两种。这两种技术都依赖在分配内存时加入额外的信息来实现,而这种信息需要恰当的维护,这和手动内存管理本身就有一定的冲突。