揭开现代语言内存管理的神秘面纱

–译自:Deepu K Sasidharan,如果你对Deepu K Sasidharan的内容感兴趣可以在twitter上面关注他,随时获取最新动态。

Part1 什么是内存管理?

其实内存管理就是控制和协调应用程序访问计算机内存的过程。这是一个严肃的话题,令人困惑,因为对于很多人而言它是一个黑盒子,让人捉摸不透。

What is it ?

当我们的程序运行在目标计算机上面的时候,它会通过计算机的内存(RAM)完成以下操作:

  • 加载需要执行的代码的字节数据
  • 保存执行时会使用到的数据和结构
  • 加载代码执行时所需要的所有运行时系统(简而言之,指的是已编译好的程序在运行时所需要的支持系统)

我们的程序除了使用内存空间保存代码的字节码之外,还会使用到内存中的堆(Heap)和栈(Stack)。

栈(Stack)

栈通常用于静态内存空间的申请,见名知意,它遵循先进后出的规则。

  • 由于栈FIFO的特性,存储和查找数据从栈顶开始,无需查找对比,所以速度非常快
  • 栈是一块连续的内存区域,其地址的增长方向是向下进行的,向内存地址减小的方向增长。保存在栈上面的数据是静态并且有限的(数据的大小在编译的时候已经确定)。
  • 与函数调用相关的数据将会以栈帧(stack frames)的形式保存,每个栈帧其实就是函数执行的环境,比如说函数每次申请一个新的变量,会将其push到栈的最顶层的块中,当函数退出的时候变量也随之销毁。
  • 对于那些支持多线程的应用,系统将会为每一个线程创建一个栈,数据为每个线程所独有。
  • 栈的内存管理简单明了,即由OS(系统)来完成。
  • 保存在栈中典型的数据包括局部变量,指针和函数块。
  • 相对于堆(Heap)而言,由于栈的大小收到限制,所以会有栈溢出的错误。
  • 对于绝大多数的语言来说报错在栈上面的数据会有大小的限制。

stack in JavaScript

JavaScript中使用的堆栈,对象存储在Heap中,并在需要时引用。

堆(Heap)

不同于栈,堆通常用于动态分配内存,在堆中程序需要使用指针来查找数据。

  • 在查找数据方面堆的效率比栈慢很多,但是可存储的数据量更大。
  • 可以将动态大小的数据存储在堆中。
  • 同一个应用(可以理解为进程)的不同线程可以共享堆中的数据。
  • 由于堆内存是动态的,是语言自己申请和回收,所以堆内存的管理相对棘手。
  • 通常堆中会保存全局变量,对象,映射等负载数据结构。
  • 当你使用的堆内存容量超过你申请的内存容量时会抛出内存溢出的错误(当然也会有其他因素导致,比如GC)。
  • 通常而言,对于堆内存所能保存的数据容量是没有限制的,当然,它会受限于你应用所能申请的最大内存容量。

Why is it important?

不同于硬盘,内存的容量不可能是不受限制的扩张,如果一个应用持续申请内存而不回收,那么最终将会把内存消耗殆尽,进而使系统崩溃。因此程序不能随心所欲的占用内存,这就要求程序开发者能够解决这个问题,而目前大多数语言都提供了解决这个问题的方法和途径。我们讨论的内存管理大多数情况下指的是堆的内存管理。

管理内存的方法

语言不会默认帮你管理内存,需要你手动申请和释放对象所需要的内存。比如,C和C++,他们提供了 malloc, realloc, calloc方法来申请内存,提供free方法来释放内存空间。这些都可以帮助开发者有效的管理堆内存,并且使用指针也是有效管理堆内存的一种方法,因人而异。

垃圾回收(GC)

通过释放未使用的内存分配来自动管理堆内存。GC普遍用于现代语言的内存管理当中。GC会间歇式运行,因此可能会导致较小的开销,称为暂停时间。目前JVM(Java/Scala/Groovy/Kotlin), JavaScript, C#, Golang, OCamlRuby 这些语言也使用GC作为默认的内存管理方式。

Mark & sweep GC

常见垃圾回收机制

标记清除

也称之为 Tracing GC 。它是典型的两阶段算法,首先将被引用的对象标记为“存活”,然后将处于非“存活”状态的对象销毁。JVM, C#, Ruby, JavaScript, 和Golang 都是采用这种方法。与JVM不同的是,JavaScript的V8引擎则是采用引用计数和标记清除相结合的方法实现其内存管理。这种GC方式也可以作为外部的库应用到C和C++上面。

引用计数

在这种方法中,每个对象在创建之初都会得到一个引用计数,当这个对象被引用时计数会加一,当删除该引用时会减一,当引用计数为0的时候,该对象就会被删除释放。引用计数并非一种非常理想的解决方法,因为它无法处理循环引用。

RAII(Resource Acquisition is Initialization)

在这种内存管理中,对象的内存分配伴随着对象从创建到销毁的整个生命周期。RAII技术被认为是C++中内存管理的最佳方法,也被AdaRust所采用。

自动引用计数

它类似于引用计数,但是与引用计数间歇式运行不同,它是在程序编译的时候将保存和释放的指令插入到代码当中,并且当引用计数为0的时候自动清除释放,这个过程不会造成任何程序暂停。但是它也不能处理循环引用并且依赖于开发者使用特定的关键指令来处理。在Objective C & Swift中采用这种方式。

Ownership

它结合了RAII和所有权模型,任何值都必须具有一个变量作为其所有者(一次只有一个所有者),当所有者超出范围时,该值将被释放以释放内存,而不管它在栈或堆内存中。它与编译时有点类似,被应用于Rust语言中,我目前还没发现其他语言使用这种方式。

Ownership in Rust