内存分配

内存分配器一直是性能优化的重头戏,其结构复杂、内容抽象,涉及的数据结构繁多,相信很多人都曾被它搞疯了。

本文将从内存的基本知识入手,到一般的内存分配器,进而延伸到 Go 内存分配器,对其进行全方位深层次的讲解,希望能让你对进程内存管理有一个全新的认识。

物理内存 VS 虚拟内存

在研究内存分配器之前,让我们先看一下物理内存和虚拟内存的背景知识。

剧透一下,内存分配器实际上操作的不是物理内存而是虚拟内存。

go-mem

内存细胞作为物理内存结构的最小单元,工作原理如下:

地址线(三相晶体管)其实是连接数据线与数据电容的三相开关。

当地址线负载时(红线),数据线开始向电容中写数据,电容处于充电状态,逻辑值变为 1

当地址线空载时(绿线),数据线不能向电容中写数据,电容处于未充电状态,逻辑值为 0

当 CPU 从 RAM 中读值时,它首先会给地址线发送一个电流信号从而合上开关,连通数据电路。这时如果电容处于高电位,则电容中的电流会流向数据线,CPU 读数为 1;否则,数据线中没有电流负载,CPU 读数为 0。

go-cpu-ram

CPU 实际上通过地址总线、数据总线和控制总线实现对内存的访问。

数据总线:在 CPU 和内存之间传递数据的通道;

控制总线:在 CPU 和内存之间传递各种控制/状态信号的通道;

地址总线: 传送地址信号,以确定所要访问的内存地址。

让我们进一步分析一下 地址线 和 按字节寻址:

在 DRAM 中,每一个字节都有一个唯一的地址。“可寻址字节不一定等于地址线的数量”,

例如 16 位的 Intel 8088、PAE(物理地址扩展)等,其物理字节大于地址线数量。

每一条地址线可以传送 1-bit 的数值,可表示寻址字节中的一位。

图中有 32 位地址线,所以可认为可寻址字节是 32 位的。

[ 00000000000000000000000000000000 ] —低位内存地址。

[ 11111111111111111111111111111111 ] — 高位内存地址。

  1. 因为上图物理字节有 32 条地址线,所以其寻址空间大小为 2 的 32 次方,也就是 4GB

可寻址字节的大小其实取决于地址线的数量,例如具有 64 个地址线的 CPU(x86–64 处理器)可以寻址 2 的 64 次方,但是目前大多数 64 位的 CPU 其实只使用了其中的 48 位(AMD)或者 42 位(Intel)。尽管理论上可访问 2 的 64 次方(256TB)大小的地址空间,但是通常操作系统并没有完全支持它们(Linux 的 四层页表结构 允许处理器访问 128TB 大小的地址空间,Windows 支持 192TB)。

由于实际物理内存的大小是有限制的,所以每个进程都运行在各自的沙盒中,也就是所谓的“虚拟地址空间”,简称虚拟内存。

虚拟内存中的字节地址其实并不是实际的物理地址。操作系统需要记录所有虚拟地址到物理地址的映射转换,也就是我们熟知的页表。

详细介绍一下 go 的 gc

Go语言的垃圾回收(Garbage Collection,GC)是其内存管理的重要组成部分。Go的垃圾回收器负责自动分配和释放内存,使开发者无需手动管理内存,减少了内存泄漏和野指针等常见的内存错误。

以下是Go语言垃圾回收的一些特点和工作原理:

  1. 基于标记-清除算法:Go语言的垃圾回收器采用了标记-清除(Mark and Sweep)算法,是一种常用的垃圾回收算法。它通过追踪对象之间的引用关系,标记出活跃对象,然后清除未标记的对象,释放其所占用的内存。

  2. 并发标记:Go的垃圾回收器采用并发标记的方式,可以在程序运行时进行垃圾回收,而不需要停止整个程序。这意味着垃圾回收过程与应用程序的执行可以同时进行,减少了停顿时间,提高了系统的响应性能。

  3. 分代回收:Go的垃圾回收器采用了分代回收(Generational Collection)的策略。它将堆分为几个代(Generation),并针对每个代采取不同的垃圾回收策略。一般来说,新分配的对象位于较年轻的代中,而生命周期较长的对象则会逐渐晋升到更老的代中。这种策略可以更加高效地回收内存,因为年轻代中的对象通常更容易被回收。

  4. 写屏障:Go的垃圾回收器还使用了写屏障(Write Barrier)来追踪对象之间的引用关系变化。当程序执行一条写操作时,写屏障会记录下引用关系的变化,并将相关信息传递给垃圾回收器,以确保垃圾回收器能够正确地标记和清除对象。

  5. 调优选项:Go语言提供了一些垃圾回收的调优选项,可以根据应用程序的需求进行调整。例如,可以调整堆的大小、设置垃圾回收的时间阈值等,以优化垃圾回收的性能和延迟。

总体而言,Go的垃圾回收器采用了一系列的技术和策略,旨在提供高效、低延迟的内存管理。开发者可以专注于业务逻辑的实现,而无需过多关注内存管理的细节。这使得Go语言在构建高性能、并发的应用程序时具有优势。

go 和 java gc 的异同

Go和Java是两种不同的编程语言,它们在垃圾回收(GC)方面有一些异同之处:

异同点:

  1. 自动内存管理:Go和Java都提供了自动内存管理机制,通过垃圾回收器自动分配和释放内存,减轻了开发者的负担。

  2. 并发垃圾回收:Go和Java的垃圾回收器都支持并发进行垃圾回收,可以在程序运行时进行回收而无需停止整个应用程序。

  3. 分代回收:Go和Java的垃圾回收器都采用了分代回收的策略,将内存堆分为不同的代,并针对不同代采取不同的回收策略。

差异点:

  1. 语言设计和用途:Go是一门面向系统编程和并发编程的语言,而Java是一门面向通用应用开发的语言。Go更注重简洁和高效,而Java更注重跨平台和广泛的生态系统。

  2. 垃圾回收算法:Go的垃圾回收器采用了标记-清除算法,而Java的垃圾回收器则采用了标记-清除算法的变种,如标记-整理算法、分代算法等。

  3. 内存模型:Go的垃圾回收器采用了写屏障(Write Barrier)来追踪引用关系变化,而Java的垃圾回收器通常使用指针碰撞(Bump Pointer)或者标记位(Marking)等技术来管理内存。

  4. 性能特点:Go的垃圾回收器在追求低延迟方面有优势,适合构建高性能、实时性要求较高的应用。而Java的垃圾回收器在追求吞吐量方面有优势,适合构建大规模、高并发的应用。

总体而言,Go和Java的垃圾回收器在实现细节和性能特点上存在差异,这些差异是基于各自语言设计的目标和用途而产生的。开发者在选择编程语言和垃圾回收机制时,应根据具体应用场景和需求进行综合考虑。

二者 gc 设计的差异的优缺点,为什么要这么设计?

Go和Java的垃圾回收(GC)设计在一些方面存在差异,以下是它们的优缺点以及设计原因:

Go的垃圾回收设计特点:

优点:

  1. 低延迟:Go的垃圾回收器设计追求低延迟,尽量减少应用程序的停顿时间。这对于实时性要求较高的应用场景非常重要,例如网络服务和游戏开发。

  2. 并发执行:Go的垃圾回收器可以与应用程序并发执行,无需暂停整个程序。这种并发执行的特性使得Go适用于高并发的应用场景,可以提高系统的响应性能。

  3. 分代回收:Go的垃圾回收器采用分代回收策略,将内存堆分为不同的代,并根据对象的生命周期采取不同的回收策略。这种策略可以更有效地回收内存,提高垃圾回收的效率。

缺点:

  1. 垃圾回收频繁:由于Go的垃圾回收器追求低延迟,它会更频繁地进行垃圾回收。这可能会导致一些额外的CPU开销,尤其是对于长时间运行的应用程序。

Java的垃圾回收设计特点:

优点:

  1. 吞吐量优化:Java的垃圾回收器设计追求高吞吐量,即每单位时间内回收的垃圾量。这对于处理大规模应用和高并发负载的场景非常有利。

  2. 高效内存管理:Java的垃圾回收器通常使用指针碰撞或标记位等技术来管理内存,这些技术在内存分配和回收上有较高的效率。

缺点:

  1. 高延迟:相比Go的垃圾回收器,Java的垃圾回收器可能会导致较长的停顿时间,对于实时性要求较高的应用可能存在影响。

  2. 较复杂的配置和调优:Java的垃圾回收器提供了多个选项和算法进行配置和调优,这需要开发者具备一定的垃圾回收理论和实践知识。

设计原因:

Go和Java的垃圾回收器设计差异主要是基于它们所针对的应用场景和目标。

Go语言注重简洁、高效和并发编程,因此其垃圾回收器设计追求低延迟和高并发执行。这样的设计可以提高系统的实时性和响应性能,适用于构建高性能的网络服务和并发应用。

Java语言更偏向通用应用开发,追求高吞吐量和高效的内存管理。

Java的垃圾回收器设计更加注重整体的吞吐量优化,适用于处理大规模应用和高并发负载。

个人收获

  1. 要学会自己读英文文档,这篇文档就是直接翻译过来的。

  2. 这个基础就是计算机组成原理。知道这一个,所有的语言都无法逃脱最基础的知识。

参考资料

图解 Go 内存分配器

https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed