🚀 揭开现代编程语言中内存管理的神秘面纱
第一部分:内存管理简介
参考
最初发表于deepu.tech。
在这个由多个部分组成的系列文章中,我旨在揭开内存管理背后的概念,并深入探讨一些现代编程语言中的内存管理。我希望本系列文章能让您深入了解这些语言在内存管理方面的底层原理。学习内存管理还能帮助我们编写更高性能的代码,因为无论语言使用何种自动内存管理技术,我们编写代码的方式都会对内存管理产生影响。
第一部分:内存管理简介
内存管理是控制和协调软件应用程序访问计算机内存的方式的过程。它是软件工程中一个严肃的话题,它让一些人感到困惑,对一些人来说就像一个黑匣子。
它是什么?
当软件在计算机上的目标操作系统上运行时,它需要访问计算机的RAM(随机存取存储器)来:
- 加载需要执行的字节码
- 存储执行的程序所使用的数据值和数据结构
- 加载程序执行所需的任何运行时系统
当软件程序使用内存时,除了用于加载字节码的空间、堆栈和堆内存之外,它们还使用两个内存区域。
堆
堆栈用于静态内存分配,顾名思义,它是后进先出(LIFO)堆栈(可以将其视为一堆盒子)。
- 由于这种特性,从堆栈存储和检索数据的过程非常快,因为不需要查找,只需从其最顶层的块存储和检索数据。
- 但这意味着存储在堆栈上的任何数据都必须是有限的和静态的(数据的大小在编译时是已知的)。
- 函数的执行数据以栈帧的形式存储在栈中(因此,这就是实际的执行栈)。每个栈帧都是一块空间,用于存储该函数所需的数据。例如,每次函数声明一个新变量时,它都会被“推送”到栈顶的块中。之后,每次函数退出时,最顶层的块都会被清除,因此该函数推送到栈上的所有变量都会被清除。由于此处存储的数据具有静态属性,因此这些可以在编译时确定。
- 多线程应用程序每个线程可以有一个堆栈。
- 堆栈的内存管理简单直接,由操作系统完成。
- 存储在堆栈上的典型数据是局部变量(值类型或原语、原始常量)、指针和函数框架。
- 在这里您会遇到堆栈溢出错误,因为与堆相比,堆栈的大小是有限的。
- 对于大多数语言来说,堆栈中可存储的值的大小是有限制的。
JavaScript 中使用的栈 (Stack),对象存储在堆 (Heap) 中,并在需要时引用。这里有一个相同的视频。
堆
堆用于动态内存分配,与堆栈不同,程序需要使用指针查找堆中的数据(可以将其视为一个大型的多层库)。
- 它比堆栈慢,因为查找数据的过程更复杂,但它可以存储比堆栈更多的数据。
- 这意味着可以在这里存储动态大小的数据。
- 堆在应用程序的线程之间共享。
- 由于其动态特性,堆的管理更加棘手,这也是大多数内存管理问题产生的地方,也是语言的自动内存管理解决方案发挥作用的地方。
- 存储在堆上的典型数据是全局变量、引用类型(如对象、字符串、映射)和其他复杂数据结构。
- 如果您的应用程序尝试使用比分配的堆更多的内存,那么您就会遇到内存不足错误(尽管这里还有许多其他因素在起作用,例如 GC、压缩)。
- 通常,堆上可以存储的值的大小没有限制。当然,分配给应用程序的内存大小是有上限的。
为什么它很重要?
与硬盘不同,RAM 并非无限。如果程序持续消耗内存而不释放,最终会耗尽内存并导致自身崩溃,甚至更糟的是导致操作系统崩溃。因此,软件程序不能随意使用 RAM,因为这会导致其他程序和进程耗尽内存。因此,大多数编程语言都提供了自动内存管理的方法,而不是让软件开发人员自行解决这个问题。我们所说的内存管理,主要是指管理堆内存。
不同的方法?
由于现代编程语言不想给最终开发者增加管理应用程序内存的负担(更像是信任👅),大多数编程语言都设计了自动内存管理的方法。一些较老的语言仍然需要手动处理内存,但许多语言确实提供了简洁的内存管理方法。有些语言使用多种内存管理方法,有些甚至允许开发者选择最适合自己的方法(C++ 就是一个很好的例子)。这些方法可以分为以下几类:
手动内存管理
语言默认不会为你管理内存,你需要为创建的对象分配和释放内存。例如,C和C++。它们提供了malloc
、realloc
、calloc
和free
方法来管理内存,而开发者则需要在程序中分配和释放堆内存,并有效地使用指针来管理内存。只能说,它并不适合所有人😉。
垃圾回收(GC)
通过释放未使用的内存分配来自动管理堆内存。垃圾回收 (GC) 是现代语言中最常见的内存管理方式之一,其进程通常以一定的时间间隔运行,因此可能会产生称为暂停时间的少量开销。JVM (Java/Scala/Groovy/Kotlin)、JavaScript、C#、Golang、OCaml和Ruby等语言默认使用垃圾回收进行内存管理。
- 标记 & 清除 GC:也称为跟踪 GC。它通常是一个两阶段算法,首先将仍被引用的对象标记为“存活”,然后在下一阶段释放非存活对象的内存。例如, JVM、C#、Ruby、JavaScript和Golang都采用了这种方法。JVM 提供了多种 GC 算法可供选择,而像 V8 这样的 JavaScript 引擎则使用标记 & 清除 GC 以及引用计数 GC 来补充它。这种 GC 也可以作为外部库用于 C 和 C++ 。
- 引用计数垃圾回收 (GC):在这种方法中,每个对象都会获得一个引用计数,该计数会随着对其引用的变化而递增或递减,当计数变为零时,垃圾回收就会执行。这种方法不太受欢迎,因为它无法处理循环引用。例如, PHP、Perl和Python就使用这种类型的 GC,并结合一些变通方法来克服循环引用。C++ 也可以启用这种类型的 GC。
资源获取即初始化(RAII)
在这种内存管理方式中,对象的内存分配与其生命周期(从构造到析构)相关联。它是在C++中引入的, Ada和Rust也使用了这种内存管理方式。
自动引用计数(ARC)
它类似于引用计数垃圾回收 (GC),但它不是以特定间隔运行运行时进程,而是在编译时将retain
和release
指令插入到已编译代码中,当对象引用变为零时,它会在执行过程中自动清除,无需暂停程序。它也无法处理循环引用,需要开发人员使用某些关键字来处理。它是 Clang 编译器的一项功能,并为Objective C和Swift提供 ARC 功能。
所有权
它将 RAII 与所有权模型相结合,任何值都必须有一个变量作为其所有者(并且一次只能有一个所有者)。当所有者超出范围时,无论该值位于栈内存还是堆内存中,该值都将被丢弃并释放内存。这有点像编译时引用计数。Rust 使用了它,在我的研究中,我找不到任何其他使用这种机制的语言。
我们只是触及了内存管理的皮毛。每种编程语言都有自己的内存管理版本,并针对不同的目标采用了不同的算法。在本系列的下一篇文章中,我们将深入探讨一些流行语言中具体的内存管理解决方案。
请继续关注本系列的后续部分:
- 第二部分:JVM 中的内存管理(Java、Kotlin、Scala、Groovy)
- 第 3 部分:V8(JavaScript/WebAssembly)中的内存管理
- 第四部分:Go 中的内存管理
- 第五部分:Rust 中的内存管理
- 第六部分:Python 中的内存管理
参考
- homepages.inf.ed.ac.uk
- javarevisited.blogspot.com
- net-informations.com
- gribblelab.org
- medium.com/computed-comparisons
- en.wikipedia.org/wiki/垃圾收集
- en.wikipedia.org/wiki/Automatic-Reference-Counting
- blog.sessionstack.com
如果您喜欢这篇文章,请点赞或留言。
图片来源:
堆栈可视化:基于pythontutor创建。
插图所有权:Link Clark,Rust 团队根据Creative Commons Attribution Share-Alike License v3.0进行创作。