likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 31

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)

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

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

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

内存大小

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

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

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

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

```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()
```

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

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

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

web使用window.performance

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

  • 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空间中的对象晋升到老生代内存中,以便为新的对象腾出空间 js垃圾回收机制,俗称gc,有兴趣了解一下吗

什么是垃圾回收机制 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过程中,如果某些对象在多次垃圾回收周期中仍然存活,它们会被认为是长生命周期对象,并被晋升到老生代内存中。晋升操作是为了避免频繁地对长生命周期对象进行复制操作,从而提高性能。

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

  2. 老生代就是标记整理清除:Mark-Sweep(标记清除) 和 Mark-Compact(标记整理)。标记-整理算法是对标记-清除算法的一种改进。

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

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

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

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

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

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

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

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

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

    // user 具有对象的引用
    let user = {
       name: "John"
    };
    

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

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

    user = null;
    

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

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

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

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

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

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

内存优化技巧

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