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

🚀 Demystifying memory management in modern programming languages | Technorage (deepu.tech)

这是”内存管理”系列的一部分
  1. [[揭开现代编程语言中内存管理的神秘面纱]]
  2. [[在JVM中可视化内存管理(Java、Kotlin、Scala、Groovy、Clojure)]]
  3. [[在V8引擎中可视化内存管理 (Javascript、NodeJS、Deno、WebAssembly)]]
  4. [[在Go语言中可视化内存管理]]
  5. [[在Rust中可视化内存管理]]
  6. [[避免NodeJS中的内存泄露:性能最佳实践]]

在这个多个部分的系列中,我的目标是揭开内存管理背后的概念,并深入了解一些现代编程语言中的内存管理。我希望这个系列能让你对这些语言在内存管理方面发生的事情有所了解。学习内存管理也将帮助我们编写更高性能的代码,因为我们编写代码的方式也会对内存管理产生影响,而不管语言使用的自动内存管理技术如何。


Part1:内存管理介绍

内存管理是控制和协调软件应用程序访问计算机内存方式的过程。这是软件工程中一个严肃的话题,它让一些人感到困惑,对一些人来说是一个黑匣子。

它是什么?

当一个软件运行在目标操作系统上的计算机上,它需要访问计算机RAM(随机访问内存)来:
– 加载自己需要执行的字节码
– 存储被执行的程序所使用的数据值和数据结构
– 加载程序执行所需的任何运行时系统
当软件程序使用内存时,除了用于加载字节码的空间、堆栈和堆内存之外,它们会使用两个内存区域。

Stack

栈用于静态内存分配,顾名思义,它是后进先出(LIFO)栈(将其视为一堆盒子)。
– 由于这种性质,从栈存储和检索数据的过程非常快,因为不需要查找,您只需从最顶层的块 存储和检索数据。
– 但这意味着存储在栈上的任何数据都必须是有限的和静态的(数据的大小在编译时是已知的)。
– 这是函数的执行数据作为栈帧存储的地方(因此,这是实际的执行栈)。每一帧都是一块空间,用于存储该功能所需的数据。例如,每次函数声明一个新变量时,它都会被“推送”到栈中的最顶部块。然后每次函数退出时,最顶层的块被清除,因此该函数压入栈的所有变量都被清除。由于此处存储的数据的静态性质,这些可以在编译时确定。
多线程应用程序每个线程可以有一个栈
– 栈的内存管理简单明了,由操作系统完成。
– 存储在栈上的典型数据是局部变量(值类型或基元、基元常量)、指针函数帧
– 这是您会遇到栈溢出错误的地方,因为栈的大小与堆相比是有限的。
– 对于大多数语言,可以存储在栈中的值的大小是有限制的

![[7KpvEn1.gif]]

JavaScript中使用的堆栈,对象存储在堆中并在需要时引用。Here is a video of the same.

堆用于动态内存分配,与栈不同,程序需要使用指针在堆中查找数据(将其视为大型多级库)。
– 它比栈,因为查找数据的过程涉及更多,但它可以存储比堆栈更多的数据。
– 这意味着可以在此处存储具有动态大小的数据。
– 堆在应用程序的线程之间共享
– 由于其动态特性,堆管理起来比较棘手,这是大多数内存管理问题的根源,也是该语言的自动内存管理解决方案发挥作用的地方。
– 存储在堆上的典型数据是全局变量引用类型(如对象、字符串、映射)和其他复杂的数据结构。
– 如果您的应用程序尝试使用比分配的堆更多的内存,这就是您会遇到内存不足错误(OOM)的地方(尽管这里还有许多其他因素在起作用,例如 GC、压缩)。
– 一般来说,堆上可以存储的值的大小是没有限制的。当然,分配给应用程序的内存量是有上限的。

它为什么这么重要?

与硬盘驱动器不同,RAM 不是无限的。如果一个程序继续消耗内存而不释放它,最终它将耗尽内存并自行崩溃,甚至更糟糕的是导致操作系统崩溃。因此,软件程序不能随心所欲地继续使用 RAM,因为它会导致其他程序和进程耗尽内存。因此,大多数编程语言都没有让软件开发人员解决这个问题,而是提供了自动内存管理的方法。当我们谈论内存管理时,我们主要是在谈论管理堆内存。

不同的方法?

由于现代编程语言不希望给最终开发人员带来负担(更像信任👅),让他们管理自己应用程序的内存,大多数开发人员都设计了一种自动内存管理的方法。一些较旧的语言仍然需要手动内存处理,但许多确实提供了巧妙的方法来做到这一点。有些语言使用多种内存管理方法,有些甚至让开发人员选择最适合他/她的方法(C++ 就是一个很好的例子)。方法可以分为以下几类:

手动管理内存

默认情况下,该语言不会为您管理内存,而是由您为创建的对象分配和释放内存。 例如,C和c++。 它们提供了malloc、realloc、calloc和free方法来管理内存,由开发人员在程序中分配和释放堆内存,并有效地利用指针来管理内存。 我们只能说它并不适合每个人😉。

垃圾回收(GC)

通过释放未使用的内存分配来自动管理堆内存。 GC 是现代语言中最常见的内存管理之一,该进程通常以特定间隔运行,因此可能会导致称为暂停时间的小开销。 JVM(Java/Scala/Groovy/Kotlin)JavaScriptC#GolangOCamlRuby 是默认使用垃圾收集进行内存管理的一些语言。
![[AZaR0LP.gif]]

  • 标记 & 扫描 GC:也称为跟踪 GC。它通常是一个两阶段算法,首先将仍然被引用的对象标记为“活动”,然后在下一阶段释放不活动对象的内存。例如,JVM、C#、Ruby、JavaScript 和 Golang 都采用了这种方法。在 JVM 中有不同的 GC 算法可供选择,而 V8 等 JavaScript 引擎使用 Mark & Sweep GC 和引用计数 GC 来补充它。这种 GC 也可用于 C 和 C++ 作为外部库。
  • 引用计数 GC:在这种方法中,每个对象都会获得一个引用计数,该计数随着对它的引用的变化而增加或减少,并且当计数变为零时完成垃圾收集。它不是很受欢迎,因为它不能处理循环引用。例如,PHP、Perl 和 Python 使用这种类型的 GC 和解决方法来克服循环引用。也可以为 C++ 启用这种类型的 GC。

资源获取即初始化 (RAII)

在这种类型的内存管理中,对象的内存分配与其生命周期相关,即从构造到销毁。它是在 C++ 中引入的,也被 Ada 和 Rust 使用。

自动引用计数(ARC)

它类似于引用计数 GC,但不是以特定时间间隔运行运行时进程,而是在编译时将retainrelease指令插入到已编译的代码中,并且当对象引用变为零时,它会作为执行的一部分自动清除,无需任何程序暂停.它也无法处理循环引用,并依赖开发人员通过使用某些关键字来处理。它是 Clang 编译器的一个特性,并为 Objective C 和 Swift 提供了 ARC。

所有权

它将 RAII 与所有权模型相结合,任何值都必须有一个变量作为其所有者(并且一次只有一个所有者)当所有者超出范围时,该值将被删除以释放内存,无论它是在堆栈内存还是堆内存中.它有点像编译时引用计数。 Rust 使用它,在我的研究中,我找不到任何其他使用这种确切机制的语言。
![[Pasted image 20220718155709.png]]


我们刚刚触及了内存管理的皮毛。每种编程语言都使用自己的版本,并采用针对不同目标调整的不同算法。在本系列的下一部分中,我们将仔细研究一些流行语言中的确切内存管理解决方案。

请继续关注本系列的后续部分:
– 在JVM中可视化内存管理(Java、Kotlin、Scala、Groovy、Clojure)
– 在V8引擎中可视化内存管理 (Javascript、NodeJS、Deno、WebAssembly)
– 在Go语言中可视化内存管理
– 在Rust中可视化内存管理
– 避免NodeJS中的内存泄露:性能最佳实践

引用


已发布

分类

来自

标签:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注