黑暗的内存管理

黑暗的内存管理 很多人对 C 语言深恶痛绝,仅仅是因为 C 语言迫使他们在编程中必须手动分配与释放内存,然后通过指针去访问,稍有不慎可能就会导致程序运行运行时出现内存泄漏或内存越界访问。 C 程序的内存泄漏只会发生在程序所用的堆空间内,因为程序只能在堆空间内动态分配内存。NULL 指针、未初始化的指针以及引用的内存空间被释放了的指针,如果这些指针访问内存,很容易就让程序挂掉。 除了堆空间,程序还有个一般而言比较小的栈空间。这个空间是所有的函数共享的,每个函数在运行时会独占这个空间。栈空间的大小是固定的,它是留给函数的参数与局部变量用的。栈空间有点像宾馆,你下榻后,即使将房间搞的一团糟,也不需要你去收拾它,除非你把房间很严重的损坏了——用 C 的黑话来说,即缓冲区溢出。 虽然导致这些问题出现的原因很简单,但是却成为缺乏编程素养的人难以克服的障碍,被 C 语言吓哭很多次之后,他们叛逃到了 Java、C# 以及各种动态类型语言的阵营,因为这些语言将指针隐藏了起来,并提供内存垃圾回收(GC)功能。他们赢了,他们懒洋洋的躺在沙发上,拿着遥控器指挥内存,信号偶尔中断,内存偶尔紊乱。     C 内存的动态分配与回收 C 语言标准库(stdlib)中为堆空间中的内存分配与回收提供了 malloc 与 free 函数。例如,在下面的代码中,我们从堆空间中分配了 7 个字节大小的空间,然后又释放了:

     #include <stdio.h>
     int main(void)
     {
         void *p = malloc(7);
         free(p);

         return 0;
     }

一点儿都不难!这跟你去学校图书馆借了 7 本书,然后又还回去没什么两样。有借有还,再借不难,过期不还,就要罚款。有谁因为去图书馆借几本书就被吓哭了的? 我们也可以向堆空间借点地方存储某种类型的数据:

     int *n = malloc(4);
     *n = 7;
     free(n);

如果你不知道 int 类型的数据需要多大的空间才能装下,那就用 sizeof,让 C 编译器去帮助你计算,即:

     int *n = malloc(sizeof(int));
     *n = 7;
     free(n);

喏,就酱紫...     策略与机制分离 在 C 语言中有关内存管理的机制已经简单到了几乎无法再简单的程度了,那么为何那么多人都在嘲笑讥讽挖苦痛骂诅咒 C 的内存管理呢? 如果你略微懂得一些来自 Unix 的哲学,可能听说过这么一句话:策略与机制分离。如果没听说过这句话,建议阅读 Eric Raymond 写的《Unix 编程艺术》第一章中的 Unix 哲学部分。 malloc 与 free 是 C 提供的内存管理机制,至于你怎么去使用这个机制,那与 C 没有直接关系。例如,你可以手动使用 malloc 与free 来管理内存——最简单的策略,你也可以实现一种略微复杂一点的基于引用计数的内存管理策略,还可以基于 Lisp 之父 John McCarthy 独创的 Mark&Sweep 算法实现一种保守的内存自动回收策略,还可以将引用计数与 Mark&Sweep 这两种策略结合起来实现内存自动回收。总之,这些策略都可以在 C 的内存管理机制上实现。 借助 Boehm GC 库,就可以在 C 程序中实现垃圾内存的自动回收:

     #include <assert.h>
     #include <stdio.h>
     #include <gc.h>

     int main(void)
     {
         GC_INIT();
         for (int i = 0; i < 10000000; ++i)
         {
             int **p = GC_MALLOC(sizeof(int *));
             int *q = GC_MALLOC_ATOMIC(sizeof(int));

             assert(*p == 0);
             *p = GC_REALLOC(q, 2 * sizeof(int));
             if (i % 100000 == 0)
                 printf("Heap size = %zu\n", GC_get_heap_size());
         }

         return 0;
     }

在 C 程序中使用 Boehm GC 库,只需用 GC_MALLOC 或 C_MALLOC_ATOMIC 替换 malloc,然后去掉所有的 free 语句。C_MALLOC_ATOMIC 用于分配不会用于存储指针数据的堆空间。 如果你的系统(Linux)中安装了 boehm-gc 库(很微型,刚 100 多 Kb),可以用 gcc 编译这个程序然后运行一次体验一下,编译命令如下:

$ gcc -lgc test.c

GNU 的 Scheme 解释器 Guile 2.0 就是用的 boehm-gc 来实现内存回收的。有很多项目在用 boehm-gc,只不过很少有人听说过它们,见:http://www.hboehm.info/gc/#users 如果 C 语言直接提供了某种内存管理策略,无论是提供引用计数还是 Mark&Sweep 抑或这二者的结合体,那么都是在剥夺其他策略生存的机会。例如,在 Java、C# 以及动态类型语言中,你很难再实现一种新的内存管理策略了——例如手动分配与释放这种策略。 Eric Raymond 说,将策略与机制揉在一起会导致有两个问题,(1) 策略会变得死板,难以适应用户需求的改变;(2) 任何策略的改变都极有可能动摇机制。相反,如果将二者剥离,就可以在探索新策略的时候不会破坏机制,并且还检验了机制的稳定性与有效性。 Unix 的哲学与 C 有何相干?不仅是有何相干,而且是息息相关!因为 C 与 Unix 是鸡生蛋 & 蛋生鸡的关系——Unix 是用 C 语言开发的,而 C 语言在 Unix 的开发过程中逐渐成熟。C 语言只提供机制,不提供策略,也正因为如此才招致了那些贪心的人的鄙薄。 这么多年来,像 C 语言提供的这种 malloc + free 的内存管理机制一直都没有什么变化,而计算机科学家们提出的内存管理策略在数量上可能会非常惊人。像 C++ 11 的智能指针与 Java 的 GC 技术,如果从研究的角度来看,可能它们已经属于陈旧的内存回收策略了。因为它们的缺点早就暴露了出来,相应的改进方案肯定不止一种被提了出来,而且其中肯定会有一些策略是基于概率算法的……那些孜孜不倦到处寻找问题的计算机科学家们,怎能错过这种可以打怪升级赚经费的好机会? 总之,C 已经提供了健全的内存管理机制,它并没有限制你使用它实现一种新的内存管理策略。     手动管理内存的常见陷阱 在编写 C 程序时,手动管理内存只有一个基本原则是:谁需要,谁分配;谁最后使用,谁负责释放。这里的『谁』,指的是函数。也就是说,我们有义务全程跟踪某块被分配的堆空间的生命周期,稍有疏忽可能就会导致内存泄漏或内存被重复释放等问题。 那些在函数内部作为局部变量使用的堆空间比较容易管理,只要在函数结尾部分稍微留心将其释放即可。一个函数写完后,首先检查一下所分配的堆空间是否被正确释放,这个习惯很好养成。这种简单的事其实根本不用劳烦那些复杂的内存回收策略。 C 程序内存管理的复杂之处在于在某个函数中分配的堆空间可能会一路辗转穿过七八个函数,最后又忘记将其释放,或者本来是希望在第 7 个函数中访问这块堆空间的,结果却在第 3 个函数中将其释放了。尽管这样的场景一般不会出现(根据快递公司丢包的概率,这种堆空间传递失误的概率大概有 0.001),但是一旦出现,就够你抓狂一回的了。没什么好方法,惟有提高自身修养,例如对于在函数中走的太远的堆空间,一定要警惕,并且思考是不是设计思路有问题,寻找缩短堆空间传播路径的有效方法。 堆空间数据在多个函数中传递,这种情况往往出现于面向对象编程范式。例如在 C++ 程序中,对象会作为一种穿着隐行衣的数据——this 指针的方式穿过对象的所有方法(类的成员函数),像穿糖葫芦一样。不过,由于 C++ 类专门为对象生命终结专门设立了析构函数,只要这个析构函数没被触发,那么这个对象在穿过它的方法时,一般不会出问题。因为 this 指针是隐藏的,也没人会神经错乱在对象的某个方法中去 delete this。真正的陷阱往往出现在类的继承上。任何一个训练有素的 C++ 编程者都懂得什么时候动用虚析构函数,否则就会陷入用 delete 去释放引用了派生类对象的基类指针所导致的内存泄漏陷阱之中。 在面向对象编程范式中,还会出现对象之间彼此引用的现象。例如,如果对象 A 引用了对象 B,而对象 B 又引用了对象 A。如果这两个对象的析构函数都试图将各自所引用对象销毁,那么程序就会直接崩溃了。如果只是两个相邻的对象的相互引用,这也不难解决,但是如果 A 引用了 B,B 引用了 C, C 引用了 D, D 引用了 B 和 E,E 引用了 A……然后你可能就凌乱了。如果是基于引用计数来实现内存自动回收,遇到这种对象之间相互引用的情况,虽然那程序不会崩溃,但是会出现内存泄漏,除非借助弱引用来打破这种这种引用循环,本质上这只是变相的谁最后使用,谁负责释放。 函数式编程范式中,内存泄漏问题依然很容易出现,特别是在递归函数中,通常需要借助一种很别扭的思维将递归函数弄成尾递归形式才能解决这种问题。另外,惰性计算也可能会导致内存泄漏。 似乎并没有任何一种编程语言能够真正完美的解决内存泄漏问题——有人说 Rust 能解决,我不是很相信,但是显而易见,程序在设计上越低劣,就越容易导致内存错误。似乎只有通过大量实践,亡羊补牢,塞翁失马,卧薪尝胆,破釜沉舟,久而久之,等你三观正常了,不焦不躁了,明心见性了,内存错误这种癌症就会自动从你的 C 代码中消失了——好的设计品味,自然就是内存友好的。当我们达到这种境界时,可能就不会再介意在 C 中手动管理内存。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 野指针分析

    1. 野指针的概念   所谓的野指针,就是说指针指向的那块内存,你没有合法操作的权限,也就是指针指向非法的内存空间,这样的指针就叫做野指针。 2. 野指针产...

    _gongluck
  • Visual C++内存泄露检测工具(VLD)

    简述 C/C++ 程序越复杂,内存的管理显得越重要,稍有不慎就会出现泄漏。如果内存泄漏不是很严重,在短时间内对程序不会有太大影响,这也使得内存泄漏问题有很强的隐...

    _gongluck
  • 判断是否为回文

    //判断是否为回文——判断用户输入的字符串是否为回文。回文是指正反拼写形式都是一样的词,譬如“racecar”。 #define _CRT_SECURE_NO_...

    _gongluck
  • win32应用程序性能测试-内存篇

    本文主要讲述windows平台下应用程序性能测试的内存相关的知识,通过本文了解内存基本原理和分析内存占用问题。 一、内存是什么? 1内存分为物理内存和虚拟内存 ...

    腾讯移动品质中心TMQ
  • 【Nodejs】516- 分析 Node.js 内存泄漏

    @冰森,日常 qcon、ssdc、Node party、Node 地下铁讲师/分享者。Node 基金会摸鱼成员。

    pingan8787
  • SQL server 2014 内存表特性概述

    Leshami
  • 【编程基础】什么是内存泄露

    内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之...

    程序员互动联盟
  • 故障分析 | MySQL OOM 故障应如何下手

    前阵子处理这样一个案例,某客户的实例 mysqld 进程内存经常持续增加导致最终被 OOM killer。作为 DBA 肯定想知道有哪些原因可能会导致 OOM(...

    爱可生开源社区
  • zephyr笔记 2.3.2 内存池

    内存池是一个内核对象,它允许从指定的内存区域动态分配内存块。 内存池中的内存块可以具有任意大小,从而在应用程序需要为不同大小的数据结构分配存储空间时减少浪费的内...

    twowinter
  • iOS内存优化心得

    当app经过一段儿时间的迭代,往往会出现一些性能问题,这时能够协助开发同学解决这些性能问题也成为我们测试同学的重要工作。凑巧最近一段时间小编就一直在协助开发同学...

    用户5521279

扫码关注云+社区

领取腾讯云代金券