深入理解js内存生命周期和GC(垃圾回收)机制

2024-06-18 00:21:02

浏览:74

评论:0

像 C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。相反,JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让 JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:

  • 分配你所需要的内存
  • 使用(读、写)分配到的内存
  • 不需要时将其释放\归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像 JavaScript 这些高级语言中,大部分都是隐含的。

内存空间概念

实际上,JavaScript确实有堆(Heap)和栈(Stack)的概念,这两个概念是计算机科学中内存管理的基础部分,同样适用于JavaScript的内存模型。

JavaScript 中的数据存储在两种主要的内存空间中:

  • 栈(Stack): 原始类型数据(如Number、String、Boolean、undefined、null)存储在此,这些值直接存储在变量中,当变量不再使用时,栈中的内存可以快速回收。
  • 堆(Heap): 引用类型数据(如Object、Array、Function)存储在这里。堆内存中存储的是数据的引用(内存地址),真正的数据则存储在这个地址指向的位置。当引用类型的数据不再被任何变量引用时,它们占用的内存才被视为垃圾。

分配内存

// 原始类型数据 存储在栈(Stack)中
let age = 18;
let str = "https://tool.vscing.com";

// 引用类型数据 在堆(Heap)内存中为对象分配空间,然后在栈(Stack)中存储指向这个堆地址的引用
let obj = new Object();

读写内存

读取:当你访问一个变量或对象属性时,JavaScript引擎根据变量的类型采取不同的策略。对于原始类型,直接从栈中读取值;对于引用类型,先从栈中获取指向堆中数据的引用,然后通过这个引用访问堆内存中的实际数据。

写入:对于原始类型,直接修改栈中的值即可。对于引用类型,修改对象属性或数组元素时,实际是在堆内存中对应的地址上进行操作。如果赋值给一个引用类型的变量一个新对象(如obj = {};),则是在栈中为新对象分配了一个新的引用,而旧对象如果不再被任何变量引用,将会在未来的垃圾回收周期中被回收。

GC(垃圾回收)

高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

javascript中使用到GC(垃圾回收)算法主要有:引用计数、标记-清除、标记-压缩、分代收集、增量标记与并发标记等

引用计数

这是最直观的垃圾回收算法,每个对象都有一个引用计数。当一个新对象被创建时,其引用计数为1。当其他地方有新的引用指向该对象时,引用计数增加。当引用被删除或变量被重新赋值时,引用计数减少。当对象的引用计数降为0时,该对象就被认为是不可达的,可以被回收。

缺点:引用计数算法不能处理循环引用的问题,这会导致一些不再需要的对象无法被回收。

标记-清除

为了解决循环引用的问题,JavaScript引擎使用了标记-清除算法。该算法执行两个阶段:

标记阶段:从根对象(通常是全局对象)开始,遍历所有的可达对象并标记它们。 清除阶段:遍历堆内存,未被标记的对象被视为垃圾并被回收。

缺点: 这种方法解决了循环引用问题,但可能会导致内存碎片。

标记-压缩

与标记-清除类似,但在清除阶段,不是简单地清除未标记的对象,而是将存活的对象向一端移动并压缩,从而减少内存碎片。

分代收集

将内存分为新生代和老生代。新生代通常使用高效的复制算法,因为大部分小对象会在短时间内变为垃圾。经过一定次数的存活周期后,对象会被晋升到老生代,老生代通常使用标记-清除或标记-压缩算法。

增量标记与并发标记

为了避免长时间的UI冻结或程序暂停,现代垃圾回收器采用增量标记和并发标记技术,允许标记过程在多个小步骤中完成,或者与程序的执行并发进行。