js垃圾回收机制,俗称gc,有兴趣了解一下吗

js的存储管理

众所周知,JavaScript 的数据类型可分为基本类型和引用类型。基本类型存在栈内存,引用类型存在堆内存。

  1. 栈内存(Stack):栈内存用于存储基本数据类型(如Number、String、Boolean、Null、Undefined、Symbol、BigInt)和对象的引用。当声明一个变量时,JavaScript引擎会在栈内存中为这个变量分配空间,并将这个变量的值存储在这个空间中。栈内存的特点是读写速度快,但空间有限
  2. 堆内存(Heap):堆内存用于存储对象(Object)和数组(Array)等复杂数据类型。当创建一个对象或数组时,JavaScript引擎会在堆内存中为这个对象或数组分配空间,并将这个对象或数组的引用存储在栈内存中。堆内存的特点是空间较大,但读写速度相对较慢。

为什么基本类型要存在栈中,引用类型要存在堆中呢?

基本类型存储在栈中有以下优势:

  1. 内存管理:因为基本类型所花销的内存小,而引用类型所花销的内存大。由于基本类型的大小是固定的(基本类型在JavaScript中是不可变的,也就是说它们的值一旦创建就不能改变。例如,如果你有一个数字5,你不能改变这个数字本身),所以将它们存储在栈中是很有意义的,将它们存储在栈中可以更高效地利用内存

  2. 性能:在 JavaScript 中,引擎需要用栈来维护程序执行时的上下文状态(即执行上下文),栈内存的访问速度非常快,因为栈是直接在CPU缓存中的,所以JavaScript引擎可以快速地读取和写入这些值。如果栈空间大了的话,所有数据存放在栈空间中,会影响到上下文切换的效率,从而影响整个程序的执行效率,所以占内存大的数据会放在堆空间中,引用它的地址来表示这个变量。

  3. 垃圾回收:栈内存是由JavaScript引擎自动管理的,它遵循LIFO(后进先出)原则。当函数执行完成后,栈内存会自动释放,这有助于减少内存泄漏的风险。

引用类型存储在堆中有以下优势:

  1. 灵活性:堆内存允许动态分配大小,这对于大小可变的引用类型非常有用。栈内存的大小是固定的,因此不适合存储大小可变的数据。
  2. 共享数据:引用类型创建时JavaScript引擎会在堆内存中为该对象分配空间,并将一个指向该对象的引用存储在栈内存中,访问一个引用类型的值时,实际上是访问存储在堆内存中的实际对象。堆内存中的对象可以被多个变量共享。当多个变量引用同一个对象时,它们实际上是指向堆内存中同一个位置的指针。这种共享机制可以提高内存利用率。
  3. 垃圾回收:JavaScript引擎使用垃圾回收机制来自动管理堆内存。当没有任何变量引用一个对象时,该对象就可以被垃圾回收器回收,从而释放内存空间。这种机制有助于防止内存泄漏

什么是内存泄漏

内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,也就是不再用到的内存,没有及时释放,就被称为内存泄漏。内存泄漏会让系统占用极高的内存,让系统变卡甚至奔溃。所以会有垃圾回收机制来帮助我们回收用不到的内存。

打个比方就像是公司的厕所坑位,上完了的人蹲在里面不出来(典型的占着茅坑不拉屎),外面的人就急得上蹿下跳,个个都是杰克逊。

为什么我们要关注内存

  1. 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  2. Node使用的也是 v8 ,内存对于后端服务的性能至关重要,因为服务的持久性,更容易造成内存溢出

v8 的内存分配:

  • 新生代内存区(new space)
  • 老生代内存区(old space)
  • 大对象区(large object space)
  • 代码区(code space)
  • map 区(map space)
  • cell区(cell space)
  • Property Cell(propery cell space)

我们着重来看一下新生代和老生代这两个部分

内存大小

  1. 和操作系统有关:64位为1.4G(1464MB),32位为0.7G(732MB)
  2. 64位新生代的空间为64MB,老生代为1400MB
  3. 32位新生代的空间为32MB,老生代为700MB
  4. 最新版的node的内存为2GB

Node.js深入浅出 原版书籍第114页

node通过process.memoryUsage()来查看内存使用情况

scss 复制代码
```js
function getMemory() {
    let memory = process.memoryUsage()
    console.log('memory', memory);
    let format = function (bytes) {
        return `${(bytes / 1024 / 1024).toFixed(2)}MB`
    }
    console.log(`heapTotal: ${format(memory.heapTotal)}\theapUsed: ${format(memory.heapUsed)}`)
}
getMemory()
```
process.memoryUsage() 属性
rss(Resident Set Size) 这是进程当前在物理内存中占用的空间大小(以字节为单位)。这部分内存包括程序代码、数据、堆栈等。请注意,这并不意味着该进程实际占用了这么多物理内存,因为操作系统可能会使用内存交换技术(如交换空间或页面文件)来管理内存。
heapTotal 这是 V8 引擎为其堆内存分配的总大小。它表示 V8 已经预留了多少内存空间用于存储 JavaScript 对象(以字节为单位)。这包括了 V8 管理的所有对象,但不包括 V8 管理的代码和数据结构( 虽然 V8 引擎确实管理代码执行和内存分配,但它不仅仅管理堆内存。V8 还有其他内存区域用于存储其内部代码(如 JavaScript 引擎本身的代码)、运行时数据结构(如执行上下文、闭包等)以及垃圾回收机制所需的数据结构。这些内存区域并不包含在 heapTotal 中))。这个值通常会在进程启动时确定,并且可以根据需要进行调整(例如,通过 Node.js 的 --max-old-space-size 标志来增加 V8 的最大堆大小
heapUsed V8 引擎当前使用的堆内存大小(以字节为单位)。这是实际由JavaScript 对象占用的内存量
external 由 V8 的 C++ 绑定创建的外部内存量(以字节为单位)。这包括了如 Buffer 对象和 TypedArray 等由 Node.js 提供的原生类型

请注意,process.memoryUsage() 提供的值是近似值,并且可能因操作系统和 Node.js 版本的不同而有所差异。此外,由于内存管理是由操作系统和 V8 引擎共同处理的,因此这些值可能会随着时间的推移而发生变化

web使用window.performance

  • jsHeapSizeLimit:表示JavaScript堆内存的上限大小。当JavaScript堆内存使用超过这个限制时,浏览器可能会采取某些措施来限制内存使用,例如垃圾回收或减缓脚本执行
  • totalJSHeapSize:这个属性表示当前JavaScript堆内存的总大小。这是浏览器为JavaScript分配的内存总量,包括已使用和未使用的部分
  • usedJSHeapSize:这个属性表示当前已使用的JavaScript堆内存大小。这表示自页面加载以来,JavaScript代码所使用的内存量。

为什么要限制v8的内存呢

  • 设计初衷:V8最初是作为浏览器的JavaScript引擎而设计的,它的应用场景主要是处理网页中的JavaScript代码。在这样的场景下,V8不太可能遇到需要处理大量内存的情况。因此,限制内存可以确保V8在大多数常见的浏览器场景中表现良好。
  • 垃圾回收机制:V8的垃圾回收机制是其内存管理的重要组成部分。然而,垃圾回收过程中可能会导致JavaScript应用逻辑的暂停,这种暂停被称为"全停顿"(stop-the-world)。如果V8的堆内存过大,进行一次垃圾回收可能会需要更长的时间,从而导致更长的全停顿时间。这不仅会影响浏览器的响应性能,还可能导致动画效果受到影响。
  • 内存管理效率:限制内存大小可以帮助V8更有效地管理内存,减少内存碎片化的可能性。同时,通过限制内存,V8可以更容易地预测和管理内存使用情况,从而提高其内存管理的效率。

新生代和老生代

新生代:

新生代内存主要存储新创建的对象,这些对象通常是短生命周期的,例如局部变量和临时对象。由于新生代中的对象生命周期较短,V8引擎采用了高效的垃圾回收策略,称为Scavenge算法,该算法通过复制和清理来管理内存。

老生代:

有些对象在新生代中经历了多次垃圾回收后仍然存活,这表明它们具有较长的生命周期。为了更高效地管理这些对象,V8引擎会将这些对象晋升到老生代内存中。老生代中的对象生命周期较长,V8引擎采用了标记-清除算法来回收不再使用的内存。这种算法通过标记活动对象并清除未标记的对象来管理内存,适用于长期存在的对象。

新生代如何晋升到老生代

  1. 对象在新生代中经历了多次Scavenge回收仍然存活。这通常意味着对象具有较长的生命周期,因此更适合放在老生代内存中管理。
  2. 新生代内存中的To空间使用率超过了某个阈值(例如25%)。当To空间的使用率达到这个阈值时,V8引擎会将From空间中的对象晋升到老生代内存中,以便为新的对象腾出空间

什么是垃圾回收机制 GC(Garbage Collection)

JavaScript 的垃圾回收机制是自动管理内存的过程,它确保在不再使用的变量或对象时释放其占用的内存空间。这样就可以防止内存泄漏,垃圾回收机制就是按照固定的时间间隔,周期性地寻找到不再使用的变量,并释放掉它们所指向的内存。

为什么要有垃圾回收呢?如果任由内存泄漏,会让系统变卡甚至崩溃。导致这问题的原因是 JavaScript 的引擎 V8 只能使用一部分内存,具体来说,在 64 位系统下,V8 最多只能分配 1.4G;在 32 位系统中,最多只能分配 0.7G。因为使用内存大小上限,所以当有用不到的变量时,引擎会帮我们清理掉。

垃圾回收算法

  1. 新生代简单来说就是Copy(复制) Scavenge(翻译过来就是清道夫)算法。

    Scavenge算法是一种复制型垃圾回收算法,它的工作原理如下:

    • 内存划分:所谓 Scavenge 算法,是把新生代空间对半分为两个区域,一半是对象区域(from),一半是空闲区域(to),这两个空间大小相等,并且只有一个空间是活跃的,用于分配新对象。另一个空间则处于空闲状态。
    • 分配对象:当需要分配新对象时,V8引擎会在活跃的From空间中分配内存。当From空间用尽时,垃圾回收器会触发一次Scavenge操作。
    • Scavenge操作 :在Scavenge操作中,V8引擎会暂停JavaScript执行(这被称为"停止-复制"阶段),然后检查From空间中哪些对象仍然是活跃的(即它们仍然被引用),哪些对象不再被引用(即它们可以被回收)。然后,V8引擎会将仍然活跃的对象复制到空闲的To空间中,并清除From空间中的所有对象。一旦复制完成,From空间和To空间的角色会互换,即To空间变为活跃空间,From空间变为空闲空间,准备下一次的Scavenge操作。
    • 对象晋升:在Scavenge过程中,如果某些对象在多次垃圾回收周期中仍然存活,它们会被认为是长生命周期对象,并被晋升到老生代内存中。晋升操作是为了避免频繁地对长生命周期对象进行复制操作,从而提高性能。
  2. 老生代就是标记整理清除:Mark-Sweep(标记清除) 和 Mark-Compact(标记整理)。标记-整理算法是对标记-清除算法的一种改进。

    标记-清除算法分为两个阶段:标记阶段和清除阶段

    • 标记阶段:垃圾回收器从一组根对象(通常是全局变量)开始,递归地访问这些对象引用的所有对象,并将这些对象标记为活跃的。标记过程中,垃圾回收器会追踪对象的引用链,确保所有可达的对象都被标记
    • 清除阶段:垃圾回收器遍历整个老生代内存区域,找到那些未被标记的对象(即不可达的对象),然后释放这些对象的内存空间。清除阶段完成后,老生代内存中的空闲空间会变得不连续,这可能会导致内存碎片化

    标记-清除算法的优点是实现简单,但缺点是可能导致内存碎片化,因为释放的内存空间可能散布在内存区域的各个位置

    标记-整理算法也是分为两个阶段:标记阶段和整理阶段

    为了解决内存碎片化的问题,V8 引擎在老生代内存中使用标记-整理算法。标记-整理算法与标记-清除算法在标记阶段相同,但在清除阶段有所不同

    • 标记阶段:与标记-清除算法相同,标记阶段也是从根对象开始,递归地访问并标记所有活跃的对象。
    • 整理阶段:在整理阶段,垃圾回收器将所有活跃的对象移动到内存区域的一端,使它们紧凑排列,然后将内存区域的另一端标记为空闲。这样,老生代内存中的空闲空间就变得连续了,从而解决了内存碎片化的问题。由于需要移动对象,标记-整理算法通常比标记-清除算法更耗时。

    :通常包括全局对象(如window对象,Nodejs的global对象)以及当前执行的上下文中的活动对象(如函数中的局部变量和参数)

    可达性:那些以某种方式可访问或可用的值,它们被保证存储在内存中,这就是可达性

    如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的。

    js 复制代码
    // user 具有对象的引用
    let user = {
       name: "John"
    };

    这里箭头表示一个对象引用。全局变量"user"引用对象 {name:"John"} 如果 user 的值被覆盖,则引用丢失:

    js 复制代码
    user = null;

    现在 John 变成不可达的状态,没有办法访问它,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。

    再看一张图,可以访问的(活跃的对象)将被访问和标记,不能访问的对象被认为是不可访问的,将被删除

V8 引擎会根据运行时的情况自动选择使用标记-清除算法还是标记-整理算法。对于新生代内存,V8 使用 Scavenge 算法;而对于老生代内存,V8 最初使用标记-清除算法,但当内存碎片化到一定程度时,会切换到标记-整理算法以优化内存布局。

内存优化技巧

  • 谨慎使用全局变量:全局变量存在被使用的可能性,所以不能当做垃圾,会始终存活到程序运行结束,而局部变量当程序执行结束,且没有引用的时候就会随着消失
  • 全局变量用完记得销毁掉:不再使用时记得将全局变量设置为null或undefined,或者不仍保留对它们的引用
  • 定时器:如果在页面销毁或组件卸载后,忘记清除定时器(如setTimeout或setInterval),这些定时器会继续在后台运行,并持有对其回调函数的引用,导致内存泄漏。所以需要确保在页面销毁或组件卸载后清除定时器
  • DOM引用:如果DOM元素被删除或卸载,但JavaScript中仍保留了对该元素的引用,那么该元素将不会被垃圾回收机制回收,导致内存泄漏。这种情况通常发生在事件监听器未正确解绑时。应确保在元素卸载时移除所有相关引用和事件监听器
  • 事件监听器:如果事件监听器被添加到DOM元素上,但随后该元素被删除或卸载,而事件监听器仍然存在,那么这可能导致内存泄漏。确保在元素卸载时移除所有相关的事件监听器
  • 避免创建循环引用:当两个或多个对象相互引用时,可能会导致它们无法被垃圾回收机制回收。例如,在对象A中有一个指向对象B的引用,同时对象B中也有一个指向对象A的引用,那么这两个对象都无法被释放。。单引用,双引用,环引用,这三种情况下,只有将所有指向该变量的对象清除才能够将该变量作为垃圾处理
  • 闭包:闭包可以保留其外部环境的引用,这可能会导致内存泄漏。如果一个闭包被返回并存储在另一个地方,而该闭包又引用了外部环境的变量或对象,那么这些变量或对象将不会被垃圾回收机制回收,直到闭包不再被引用。所以再使用闭包时要注意及时解除不必要的引用
  • 使用weakMap :WeakMap 只接受对象作为键(key),且键是弱引用,并且当这个键对象不再被其他地方引用时,它会自动从 WeakMap 中删除(当键对象被垃圾回收时,对应的键值对也会被自动删除)。这意味着你不需要手动去清理不再需要的键值对,垃圾回收机制会自动为你处理。
    • DOM 元素的临时数据存储:当你需要为 DOM 元素存储一些临时数据,但又不希望这些数据在元素被移除或替换时还保留在内存中时,可以使用 WeakMap

      js 复制代码
      let weakmap = new WeakMap();  
      
      // 为某个 DOM 元素存储数据  
      let element = document.getElementById('elementId');  
      weakmap.set(element, {someData: 'Hello'});  
      // 当这个元素不再被需要并被移除时,与其关联的数据也会自动从 weakmap 中删除
      • 缓存对象:如果你有一个需要缓存对象引用的场景,但又不想阻止这些对象被垃圾回收,WeakMap 是一个很好的选择
相关推荐
用户214118326360226 分钟前
首发!即梦 4.0 接口开发全攻略:AI 辅助零代码实现,开源 + Docker 部署,小白也能上手
前端
gnip2 小时前
链式调用和延迟执行
前端·javascript
SoaringHeart2 小时前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.2 小时前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu3 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss3 小时前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师3 小时前
React面试题
前端·javascript·react.js
木兮xg3 小时前
react基础篇
前端·react.js·前端框架
ssshooter3 小时前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘4 小时前
HTML--最简的二级菜单页面
前端·html