热线电话:0755-23712116
邮箱:contact@shuangyi-tech.com
地址:深圳市宝安区沙井街道后亭茅洲山工业园工业大厦全至科技创新园科创大厦2层2A
内存,以及编程语言如何管理内存,是一个让开发者们头疼不已的问题。我们所写的程序时刻不停地分配着内存,但我们却很难搞清楚,这一切到底是怎么发生的。
存储空间,正如它一开始所定义的,是我们存储特定信息,以备之后使用的地方,这种存储可能是永久的(直到我们手动删除),也可能是临时的(直到电脑自动删除)。实际上,我们和电脑之间的每一次交互,都涉及信息的存储。比如说,打开一个浏览器时,它的执行步骤就从永久存储(硬盘)加载到临时存储(内存RAM)中。
主存储,或者说 RAM,是电脑使用的内部存储空间,有别于 USB 、硬盘之类的外部存储设备。电脑可以与内存直接交互,所有程序也必须加载到内存中才能执行。有时,整个程序都会被加载到内存中,也有时,只有程序的一部分(一个进程)被加载到内存中——这个机制被叫做动态加载。如果这部分程序依赖于另一个程序,那么,还会有一个动态链接机制建立起这个程序与主程序之间的关系。
内存管理影响到电脑中的每一个程序,极为关键,因此,现代操作系统都有一套复杂的机制来完成这项工作。通过各个层次(硬件层、操作系统层、应用软件层)的协调与控制,确保内存使用合理高效。
本文聚焦于操作系统与应用软件中内存管理。在系统层,内存管理主要涉及特定存储块(可以被理解为地址与空间)的分配;在应用层,内存管理主要涉及向系统发送内存空间请求,以及确保程序定义的对象与数据结构有足够的存储空间(内存的分配、重新分配以及释放)。
当一个程序申请一段内存时,一个“分配者”会负责将内存分配给它,并在不再需要的时候释放出来,以供重新分配。这个过程可以手动控制,也可以自动完成,主要取决于编程语言的特性以及程序员自己的选择。
手动内存管理可以理解为程序员通过自己的代码分配或释放内存。比较著名的,是 C 语言使用的动态内存分配技术。不过,得力于 ObjectiveC 和 Swift 的大力推广,现在流行的大多数编程语言都通过垃圾回收器或自动引用计数(ARC)实现了自动内存管理。
错误的内存操作会破坏内存区块的分配与释放过程,导致很严重的后果。从更高层面看,内存区块总是会恢复正常的,一个简单的错误似乎并没有那么严重,但系统中总是同时运行着成百上千个进程,不可能都卡在那里,等着某个内存区块恢复正常。
于是,这些错误会用光程序运行所需的必要内存空间,或者更糟糕的是,如果区块被错误地释放或分配,区块中存储的敏感信息,比如密码、密钥或者其它隐私信息,会被攻击者所窃取。
以下是错误的内存操作产生的常见后果:
由于错误的算术计算,原来分配的内存区块无法存储最后的结果。比如说,一个程序可能定义了一个占用 8 位内存的值,只能存储 -128 到 +127 之间的数字,假设程序先将这个数字赋值为 127,之后又加了 1,就会导致一个预期外的结果,因为 8 位内存空间无法存储 128 这个值。
这个 Bug 由 Brumley, Chiueh 和 Johnson 在 2012 年定义,具体描述是,“一个变量的值超出了机器存储这个值所用字节的表示范围”。产生这个 Bug 的原因很多,比如向上溢出、向下溢出、数据截取、符号错误等,主要是由于错误定义的语句或整数操作,而程序员要定位问题往往很困难。不同语言处理这个问题的方式也不一样——例如,Smalltalk 与 Scheme 会自动升级变量类型,而其它一些语言则把问题留给程序员自己。
如果一个程序一直向系统申请,但不释放内存——也就是说,告诉系统哪些内存可以重新利用了——就会导致内存泄露,程序最终会用完所有可用内存。另外,如果程序中的某个对象被存储在内存中,但运行中的代码实际上已经没法访问到它了,也会导致同样问题。
当某个程序访问它没有权限访问的、另作它用的内存空间,或者对某部分内存执行超越权限的操作,比如试图对只读内容进行写操作时,就会导致段错误。段错误可能导致程序挂起、崩溃或退出。
当程序要写入的内容超过了被分配的空间长度,它继续写入到之后的,另作它用,或者没有写权限的内存空间时,就会导致缓冲区溢出。缓冲区溢出也会使程序挂起、崩溃或退出。
当程序试图删除一个已经被删除的对象,因而导致堆污染或者段错误时,就叫删除错误。删除错误也可以认为是段错误的一个子集。
对程序员来说,最常见的内存问题就是如何操作内存的问题——如果说系统可以把内存分配给程序,那么,程序所使用的编程语言是手动还是自动完成内存分配的呢?以及更重要的,这种分配方式会导致什么结果呢?
手动内存管理是指在特定语言中,程序员必须通过自己的代码来管理内存,与之相对地,自动内存管理是指程序员不需要或基本不需要执行什么动作来操作内存。我们这里所说的“操作”和“管理”,是指申请、重新分配内存,或者释放掉我们认为已经成为“垃圾”的内存空间。
直到上世纪 90 年代中期,主流编程语言都支持手动内存管理,即使在今天也依然如此(以关键词 “new” 或 “alloc” 的形式)。不过,这仅仅是因为对象创建,也就是为对象分配内存的过程很容易而已——程序员在创建对象的时候,可以清楚地知道对象的大小、名称以及初始化过程。然而,销毁对象就困难多了,由于销毁过程往往在对象创建很久之后才触发,程序员可能并不知道对象的大小。更麻烦的是,程序员可能也不知道具体在哪个时间点应该销毁对象,很有可能,软件中的某部分代码依然在使用这个对象。
如之前所说,如果不能正确地初始化或销毁对象,就会导致内存错误。编程语言如何处理内存错误取决于它的具体实现:大多情况下,内存错误会导致“未定义行为(undefined behavior)”——也就是说,说不准会发生什么。(注意,在准确的手动内存管理下,一切都是确定的,程序员总是清楚一个对象什么时候被创建或被销毁。)
1959 年,一个内存管理的新概念——垃圾回收——被引入 Lisp 编程语言。垃圾回收是自动内存管理中最著名的一个例子,通过垃圾回收,之后不再使用的对象会被销毁,空间会被释放。这种技术减少了 Bug,提高了内存管理水平。垃圾回收的具体实现采用了多种策略,包括对象追踪、引用计数、时间戳、心跳等。
其它自动内存管理技术包括基于栈的内存管理(stack-based memory allocation)、基于作用域的内存管理(region-based memory management)、自动引用计数(ARC)等。不过,这些技术都存在一些性能问题,也带来了某种不确定性,因为程序员并不能准确地知道对象是在什么时候被销毁的。
当然,手动内存管理与自动内存管理都还被今天的编程语言广泛应用:前者以 C 语言家族为代表,后者以 Lisp、Java 以及其它众多语言为代表。事实上,大多数语言都混合使用这两种技术:如前文所说,通过手动方式分配内存,通过垃圾回收技术释放内存。
如我们所见,电脑帮助人类解决复杂问题的方式,让程序员有一种“宇宙之主”的感觉。我们也注意到,这个宇宙存在着种种规则和限制,其中一个,就是内存总是有限的。不过,正如哈姆雷特所说,作为程序员,我们依然可以“藏身果壳之中,而把自己看作拥有无限疆域的君王。”