JavaScript暂时性死区与垃圾回收机制深度解析

作者:闭包旅人时间:2026-10-26 14:03:52浏览:201

本文深入解析JavaScript中的暂时性死区(TDZ)概念,阐明let/const声明的变量提升限制及ReferenceError异常成因。同时详细梳理JS垃圾回收机制,涵盖引用计数与标记清除算法的优劣,并重点介绍V8引擎的分代回收、增量回收及标记压缩等优化策略,帮助开发者理解内存管理与避免内存泄漏。

暂时性死区(TDZ)

暂时性死区是什么

我们来看一个例子

 var tmp = 123;
 if (true) {      
     tmp = 'abc';     
     console.log(tmp);
     let tmp;  
 }

上述两条语句均会触发报错,原因在于在变量完成初始化之前,尝试访问该变量是不被允许的。

我们知道,使用var关键字定义的变量存在变量提升现象,下面我们来剖析其背后的原理:

在任何代码执行之前,JavaScript引擎都会经历预编译阶段阶段。尽管这一阶段耗时极短,通常难以被直接感知,但其核心作用是在内存中开辟空间以存储变量和函数声明。

在预编译期间,JavaScript引擎会创建执行上下文,并将当前作用域内的变量声明和函数声明提升至作用域的顶部。

而暂时性死区(TDZ)正是对这种变量提升机制的一种限制。

当一个变量被声明时,如果在声明语句之前访问该变量,引擎会抛出ReferenceError异常。这种行为被称为暂时性死区(Temporal Dead Zone,简称TDZ),它主要存在于使用let和const关键字声明的变量中。

其本质原因在于,虽然变量声明本身会被提升,但变量的赋值操作并不会被提升。与var不同,TDZ不会将变量初始化为undefined,因此在声明前访问该变量会抛出异常。这类似于C语言中使用未初始化的野指针,指针指向的堆或栈空间在暂时无法访问时也会引发问题。

例如:

console.log(a);
let a;
//会报错

js垃圾回收机制

内存泄漏

在探讨垃圾回收机制之前,我们需要先明确什么是内存泄漏。

简而言之,主机的内存空间是有限的。内存泄漏是指在程序运行过程中,减少了可用内存。通常,有用的内存占用被视为正常使用;而如果某些数据在使用后不再需要,却仍占据内存空间且未被释放,这就被称为内存泄漏。

在JavaScript中,内存泄漏通常由以下几个原因导致:

  • 全局变量:未使用var、let或const关键字声明的变量会自动添加到全局对象中。如果意外创建了全局变量,这些变量可能无法被垃圾回收器释放。
  • 定时器或回调函数:在创建定时器或回调函数时,若未及时清除它们,它们可能会一直占用内存空间,直到页面关闭。
  • DOM节点引用:在操作DOM节点时,如果保存了节点的引用但未及时释放,节点可能会一直占用内存空间,直到页面关闭。
  • 闭包:在使用闭包时,如果闭包中引用了外部变量但未及时释放闭包,外部变量可能无法被垃圾回收器释放。
  • 循环引用:在创建对象时,如果对象之间存在循环引用关系,这些对象可能会一直占用内存空间,直到页面关闭。

垃圾回收机制

而JavaScript垃圾回收机制则是利用自动内存管理技术,自动检测哪些变量、对象和数据不再被使用,并自动释放它们所占用的内存空间。

那么它是如何实现的呢?一般有以下两种算法:

  • 引用计数,其基本思路是为每个对象维护一个引用计数器。当一个对象被引用时,计数器加1;当对象不再被引用时,计数器减1。当计数器的值为0时,表示该对象不再被使用,可以被垃圾回收器释放。引用计数算法可以快速地处理不再被引用的对象,但是它无法处理循环引用的情况,因此在实际应用中很少使用。

  • 标记清除,其基本思路是通过标记所有可以访问到的对象,然后清除所有未被标记的对象。在JavaScript中,垃圾回收器会从全局对象开始遍历内存中的所有对象,标记所有可以访问到的对象,然后清除所有未被标记的对象。标记清除算法可以有效地处理循环引用的情况,但是它需要占用大量的时间和内存空间,因此在执行过程中可能会出现性能问题。

基于此,v8引擎就对垃圾回收机制做了优化

  • 首先是分代垃圾回收,将堆分为新生代和老生代两个区域。新生代中存储的是生命周期较短的对象,而老生代中存储的是生命周期较长的对象。新生代区域使用Scavenger算法,老生代区域使用Mark-Sweep(标记清除)和Mark-Compact(标记压缩)即标记算法。
  • 其次是增量式垃圾回收,将整个垃圾回收的过程分为多个小步骤,在每个小步骤之间可以插入一些JavaScript代码的执行。这种方式可以避免垃圾回收造成的长时间的页面卡顿。
  • 最后是标记压缩Mark-Compact,由于标记清除Mark-Sweep会清除未标记的对象,导致只回收不连续的内存块,这样还有有很多内存块碎片虽然被清除,仍无法使用。标记压缩法就是,V8引擎在老生代区域中,对标记存活的对象进行迁移,再将移动后的存活对象的地址重新映射到新的位置,然后清除原地址内存块,这样可用内存块就不会是碎片化,导致难以使用。但是移动对象的过程肯定也是影响性能的,不能过于频繁。

再有就是在V8引擎中,垃圾回收的频率是动态可变的,

  • V8引擎在启动时设置的最大堆大小,一旦堆中的对象数量超过了阈值,V8引擎会立即启动垃圾回收器。一般情况下,V8引擎是根据当前堆中的对象数量、内存使用情况、CPU占用率等来自动计算的垃圾回收间隔时间。
  • 除自动调节垃圾回收频率外,还可以通过手动触发垃圾回收来调节垃圾回收的频率。如在js中使用window.gc()方法手动触发垃圾回收。
  • 还可以在Node.js中可以通过--max-old-space-size参数来设置老生代堆内存的阈值大小,通过--max-new-space-size参数来设置新生代阈值的大小

到此这篇关于浅谈JavaScript暂时性死区与垃圾回收机制的文章就介绍到这了,更多相关JavaScript暂时性死区与垃圾回收机制内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章