0xAA55 发表于 2021-11-14 04:48:20

【泛】漫谈内存管理与GC(垃圾回收机制)

# 漫谈内存管理与GC(垃圾回收机制)

在不同的编程语言和不同的开发环境之间,内存的管理方式和使用方式是千差万别的,而这里面差异最明显的管理方式之一就是GC的有无。无GC的内存管理方式里,最常见的就是像C语言那样、使用内存分配函数(比如`malloc()`)从堆上索取一块可以使用的内存空间,并在完成了使用后,使用内存回收释放函数(比如`free()`)来归还这块内存的所有权,使其成为空闲内存。C++在这方面借助了语言本身的特性,可以自动在变量、类实例的生命周期结束时,释放其在堆上占用的内存。通过主动的内存索取与归还来实现内存的管理。

而有GC的内存管理则完全不一样。在有GC的内存管理方式中,一个最典型的特征就是编程语言本身似乎**并不需要**主动去释放内存。被使用完毕的内存会在某个时间点上,自动被回收。那么这样的内存管理方式到底是如何工作的呢?它有什么样的好处和坏处呢?本篇文章将要介绍这里的内容。

## 如何进行内存管理

在有限的内存里,随着程序的运行和数据的处理,你需要动态划分哪些内存被用于存储什么内容,哪些内存是空闲的,以及如何灵活高效地组织内存空间的使用。

### 静态划分内存空间

在设计程序的时候,把内存按照固定的大小和数量划分为不同用途的内存块。没有动态的内存分配,只有静态的内存分配。这种方式多见于嵌入式开发、单片机等本身物理内存较少、没有多少空间可用的环境。在这样的环境下,做动态的内存管理带来的内存开销会让本来捉襟见肘的内存空间资源更加紧张。

但,在内存空间资源足够大、程序的设计更加复杂的情况下,动态对内存空间进行划分、分配、释放,则可以实现更加高效的内存使用。

### 动态管理内存空间

为了达到这个目标,一个典型的做法是进行**主动内存管理**,即:实现一个索取内存的功能和归还内存的功能,比如C语言的`malloc()`和`free()`,主动索取、分配内存,主动归还、释放内存。在这样的内存管理的设计中,内存空间被标记为*已占用*和*未占用*的两种状态。在索取内存的时候,先找出连续空间足够大、能够满足需求的*未占用*内存,然后将其标记为*已占用*,此时这块内存即被视为已分配的内存,可被自由读写。在后续的内存索取的过程中,先前这块被标记为*已占用*的内存不会受到任何影响。而当你不再需要这块内存的空间的时候,归还内存的过程会将对应大小的内存区域标记为*未占用*。这样在后续的内存索取过程中,这块内存可以得到重复的分配和使用。

对于如何把内存标记为*已占用*或者*未占用*,有多种多样的方式来实现,最典型的实现方式之一是使用一部分内存空间来存储内存的分块大小和是否被占用的属性,比如在每个索取的内存块的前部,存储这次索取的内存块的大小和其已被占用的属性,再把后续的内存空间拿来使用。在索取内存的时候,先从内存池的起始地址开始判断内存块的大小和是否被占用的属性,如果被占用,则根据占用的大小判断下一个内存块的位置,然后判断其大小和是否被占用的属性,以此类推,直到找到连续空间足够大的*未占用*内存。

这种内存管理方式简单有效,但它有缺点:
* 会产生大量小块分散的空闲内存。这些内存难以被有效利用,因为它不连续。
* 在多线程环境下需要使用线程锁来避免冲突,造成内存分配的过程成为程序的性能瓶颈。
* 需要消耗内存用于内存空间大小和占用状态的标记过程。
* 受编程语言本身特性和程序设计的影响,内存管理器无法移动已被占用的内存来把空闲内存集中起来。
* 设计失误的程序会因为设计者忘记归还已被占用的内存而导致内存泄漏、空间被全部浪费。

我们来简单模拟一下这样的内存分配与释放的过程。



## 如何解决内存管理遇到的问题

普通内存管理的问题里,不少都可以通过借助语言自身的自动化特性来实现其解决。除此以外,还可以自己设计自己的内存池,在自己的内存池里进行自己特有的内存管理。当然,引入垃圾回收机制的概念,则能更好地解决内存管理时会遇到的问题。

### 利用编程语言的自动化特性进行资源的自动释放

对于由于忘记释放内存造成的内存空间的浪费,在C++可以通过使用带包装的*智能指针*,比如`std::shared_ptr`等,进行资源的管理和释放。C++的智能指针利用其生命周期的变化来分配、释放内存,或者记录指针指向的内存被引用的次数。在引用次数归零的时候释放内存等。C语言则因为其缺乏一些自动化特性,只能靠程序员养成良好的编码习惯来防止这样的错误发生,比如所有的内存分配、数据创建等的函数都有对应的内存释放、数据销毁等函数。

### 利用操作系统自身的进程管理进行资源释放

这种管理方式其实也不失为一种策略,直观来说就是**不进行**内存资源的释放。程序退出后,操作系统自动回收内存。在不需要频繁分配、释放内存的情况下,一次性分配足够的内存,然后使用这些内存,直到程序退出。

### 利用操作系统提供的内存相关API进行内存的分配、管理、释放

这里我要提的是 Windows 的 API 可以进行更加细致、更多功能的内存管理。在现代操作系统上运行的程序,其对内存的使用都是由操作系统负责管理的。操作系统把内存按页来管理,其可以决定哪些页被映射到什么样的线性地址上,哪些页可以进入CPU缓存,哪些页被放入硬盘上的页面文件用于腾出物理内存。这里说到线性内存的一个概念:你不仅需要物理内存本身的容量资源,你还需要内存的地址资源,尤其是在你编写32位程序,却想要使用超过2 GB的内存的时候。在 Windows 上,你可以调用 `VirtualAlloc()` 来分配内存地址和物理内存。你可以只分配地址而不分配内存,也就是让分配的地址范围内的内存与物理内存没有关联,从而避免这个地址被别的程序占用的同时,不消耗物理内存。你也可以设置内存的读写属性,利用CPU的硬件功能可以做到让内存拥有可读、可写、可被当作指令执行等不同的属性,并且在指令上做出了违背内存属性的操作的时候,被检测出,程序被中止,或者被调试。你还可以通过文件IO的方式来读写内存,并且可以借助这种方式实现“内存文件”被多个进程共享,并通过调用 `MapViewOfFile` 进行内存文件映射,实现跨进程的物理内存直接访问等。

## 垃圾回收机制:与一般的内存管理有何不同

垃圾回收机制与编程语言自身的机制关系非常密切。并不是所有的编程语言都可以应用垃圾回收机制。虽说,要强行应用也是可以的,但很麻烦。让编程语言自身支持垃圾回收机制更合适。所以一般都有一个概念:**垃圾回收机制是某些编程语言特有的**。

### 带垃圾回收机制的内存管理有何不同?

一般的内存管理是在内存中找出符合需求的足够大的空闲内存来分配,来获取内存块并进行工作。但,带垃圾回收机制的内存分配方式则快很多:**像栈一样,用一个值来表示内存已占用的量**。通过类似于“压栈”的操作,直接获取内存空间用以使用。内存以极快的速度分配,**且在多线程环境下的内存分配成本最低只需要一个原子操作**。

但这样一来,内存就只能从所有已分配的内存的尾部分配。**虽然分配的速度极快,但你不能主动释放内存**。换言之,随着程序的运行,整个内存池会被快速用满。

在内存池用满后,垃圾回收器便开始了工作,*此处即是魔法的发生*。在垃圾回收器结束工作后,**所有正在被使用的内存块都被集中移动到了整个内存池的开始处**。没有任何内存空洞。并且,对于程序而言,**所有指向内存池里分配的内存块的指针都指向了被移动后的新地址**。所有不被引用的内存块被自动释放。

### 垃圾回收器是如何工作的

垃圾回收,顾名思义,就是把不被引用的内存块识别出来并进行回收。要做到这一点,首先需要找出所有正在被引用的内存块,然后使用排除法,把其它的内存块作为不被引用的、应当被回收的内存块进行回收处理。

有GC的编程语言往往被设计为高度面向对象的语法形式,比如C#或者Java那样,并且往往都会被设计为使用JIT方式生成二进制指令来运行。原因之一就是为了方便实现GC,因为内存池中的每个内存块都是一个类的实例,而类与类之间的引用关系都能很容易被找出来。

在确定了哪块内存块需要被保留、哪块内存块不需要被保留后,GC接下来要做的就是把所有需要保留的内存块移动到内存池的起始处。**这一过程可以消除内存空洞**。所有空闲内存都会在这一操作后被释放出来,集中在内存池后部。但,为了操作内存,必定要用到内存指针,移动内存块的过程会导致这些指针变得无效,因为这些指针在此时仍然指向内存块的旧位置。于是,GC还需要把所有这些指针的指向进行修改,使其指向被移动后的内存块的位置。这样的指针修改同时也包括了正在运行的线程的寄存器值的修改。为此,**GC需要暂停所有正在运行的线程**,然后通过修改其线程上下文中的寄存器来找出其正在引用的类实例(内存块),并在移动了内存后修改指针值为移动后的指针值。其它的类对这个类的引用的指针也会被修改。

### 对比一般的内存管理

和一般的内存管理做比较,具有GC垃圾回收机制的内存管理方式有很多的优点。
* 明显高效的内存分配过程,**像栈一样快**,完全不怕多线程的频繁的内存分配请求。
* 占用内存连续,无内存空洞,空闲内存空间被集中到一块连续的地址上。**内存利用效率高**。
* 不存在因为忘记释放内存导致的内存泄漏、空间浪费的问题。
* 不需要消耗内存用于内存空间大小和占用状态的标记过程。
* 可以完美解决循环引用的问题,因为共享的类的资源的释放与否不需要靠引用计数来实现。

然而GC机制在带来这些优势的同时,也会带来其缺点:
* 垃圾回收过程需要暂停所有正在工作的线程。这一过程本身具有较高的时间成本。
* 垃圾回收过程需要移动正在被使用的内存块。移动内存本身耗时,而移动时还需要修改所有指向这块内存块的指针。
* 在内存池较小,但内存分配较频繁的场合,会导致GC被频繁触发,引起性能问题。
* 高度依赖语言特性的配合。这直接决定了如何去判断一个内存块是否被引用。

### 举例说明



通过这种内存回收机制可以发现,垃圾回收器需要根据根实例的引用树、运行线程的栈的局部指针变量和寄存器来判断实例是否应当被销毁。这一过程导致垃圾回收器**对指针敏感**。正因为如此,带GC的编程语言一般不会让你使用指针去引用托管内存中的数据,而是要让你避免使用指针。

## 参考资料

(https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals)

真中合欢offical 发表于 2022-1-9 21:07:05

感谢分享,知识面又拓宽了~~

VB小白 发表于 2022-4-5 11:21:53

感谢分享
页: [1]
查看完整版本: 【泛】漫谈内存管理与GC(垃圾回收机制)