运行时的内存管理

前言

在上一篇总结 垃圾回收入门 中,我的本意是以了解垃圾回收相关概念为主,暂时忽略其他相关的概念。不过,这导致了有一些内容非常的模糊,容易引起疑惑。比如,是所有被创建的对象都需要回收吗?为什么在垃圾回收课题中会经常出现“堆区”的概念?对象在程序运行时到底是如何分配管理的?

对此,我又向前查找,阅读了《编译原理》第七章的1~4节,在这里再做一下简要总结,同时解答上面的几个疑惑。

运行时刻环境

程序语言中包含很多抽象概念,比如:名字(names)、作用域(scopes)、绑定(bindings)、数据类型(data types)、操作符(operators)、过程(procedures)、参数(parameters)和控制流构造(flow-of-control constructs)。编译器为了实现在计算机上支持这些概念,使得程序可以成功运行,需要创建并管理一个运行时刻环境(run-time environment)。

这个环境需要处理的事务如下:

  • 为源程序中命名的对象分配空间、安排存储位置(the layout and allocation of storage locations for the objects named in the source program)
  • 确定程序访问变量的机制(the mechanisms used by the target program to access variables)
  • 过程间的连接(the linkages between procedures)
  • 确定参数传递机制(the mechanisms for passing parameters)
  • 实现对操作系统、输入/输出设备及其他程序的接口(the interfaces to the operating system, input/output devices, and other programs)

这里只关注第一个问题:为对象分配空间、安排存储位置

存储组织(Storage Organization)

程序在它的逻辑地址空间中运行,这个逻辑地址空间是由编译器、操作系统和计算机共同管理和组织的。

逻辑地址空间中的程序,在运行时刻的映像包含两部分:数据区(data areas)和代码区(program areas)。

某个语言在某个操作系统上,可能按照如下方式划分内存空间:

内存空间划分

为了使运行时刻的空间利用率最大化,设计令堆区和栈区的空间对向增长

具体的空间使用如下表格:

区域 空间 存储内容
代码区 静态 生成的目标代码
静态区 静态 全局常量、一些编译器产生的数据
堆区 动态 具有长生命周期的数据、编译时不能确定大小的数据
栈区 动态 函数调用过程中产生的活动记录(activation records)

这里就可以看到,需要考虑动态回收空间的只有数据会动态变化的堆区和栈区,那么为什么垃圾回收只面向堆区呢?这就要详细了解一下栈区的特点了。

空间的栈式分配

看到“栈”,就很容易联想到数据结构中的“栈”。总之,它们都是一种适用先入后出(First In, Last Out)场景的结构。

为了理解程序对空间的栈式分配,这里需要以函数的嵌套调用为例,同时引入一个概念:活动记录(activation records)。

首先,看这段使用递归的方式实现的快速排序算法。

quickSort

我们可以使用简单的函数调用活动序列来描述它:

possible activations

接下来,把它完整递归过程抽象成一个树形结构,这个结构被称为活动树(Activation Tree)

activation tree

那么:

  • 函数的完整调用过程,和活动树的前序遍历对应
  • 函数的完整返回过程,和活动树的后序遍历对应
  • 某一次正在进行的函数调用,就对应活动树上的某一个结点。当前全部未结束(活跃的)的调用,就对应当前结点及其祖先节点;调用的顺序,就是从根结点到当前结点的路径;返回的路径,就是调用路径的反向。

活动树中的每一个节点,都对应着一次函数调用的活动记录,它可能包含的数据如下图所示:

activation record

图中最下方的位置是接近栈顶的位置,上方接近栈底

到这里,就可以看到,活动记录中,有一部分空间是分配给这一次函数调用的局部数据的。

整个嵌套调用流程的全部活动记录,是由一个称为控制栈(control stack)的运行时刻栈管理的。每一次函数调用的活动记录(activation record),也可以被称为(frame)。

在进入某个函数时,其活动记录被压入栈顶;退出时,则相应的将其活动记录弹出栈顶,复用空间。在这个过程中,活动记录里的局部数据也随之消亡。

growing stack

因此,虽然很多时候,现代程序设计语言中,将无法在编译时刻确定大小的数据放在堆区。但是,当一个对象完整的生命周期都在这个过程(procedure)的局部空间中完成,在局部中创建,在过程(procedure)退出后不需要再进行访问,那么就可以为这个对象在栈区分配空间,同时也不必再考虑其失效后单独回收空间的问题。

这也就是垃圾回收只需要面向堆区的原因。

以上。

参考

编译原理(第二版)[7.1~7.2]