We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
作为目前最流行的javaScript引擎,�V8引擎在底层为我们处理了很多麻烦事,包括自动的垃圾回收管理。
所以在日常开发中,很少会碰到由于内存泄漏导致的程序崩溃问题,尤其是浏览器端。
但了解V8引擎的垃圾回收策略能够帮助我们规避导致内存泄漏的代码,以及写出对V8引擎友好的代码。
简单来说,将失去作用的对象所占用的内存空间进行清空,就是垃圾回收。
当我们创建一个对象时,JS引擎都要在内存空间中分配相应的内存空间来储存这个对象的实体。
试想一下,如果每个对象分配的内存空间都不再被回收,那么整体占用的内存就会越来越大,直到超过V8引擎所持有的内存,从而引起程序崩溃。
所以为了内存的合理分配,就要找到那些不再被使用的对象并将其清除,从而释放内存空间,以供程序的流畅运行。
由于JS是单线程的,当垃圾回收工作正在进行时,其他任务都要等待。
为了避免因为垃圾回收而阻塞主线程,V8引擎有一套回收机制。
V8引擎可使用的内存不是无限制的,具体点来说,在64位操作系统中可使用内存大概有1.4GB,32位操作系统则为0.7GB。
至于为什么要做内存限制,主要有两个原因:
JS在设计之初是作为浏览器脚本语言的,作用就是让用户与浏览器交互、操作DOM、表单验证。
在这种场景下,就很少有占用大内存的情况。
当然后来出现了node.JS,涉及到的复杂I/O操作导致很可能出现内存溢出的情况。
因此V8也为我们提供了调整内存大小的可配置项,不过需要在node初始化时配置,这个就不在本文深究了。
虽然V8会自动进行垃圾回收,但进行一次垃圾回收是很耗时的,并且还会堵塞主线程。
如果V8不对内存进行限制,很可能一次垃圾回收就要处理大量的垃圾,从而导致应用的堵塞。
基于以上两点,V8引擎可使用内存是有限制的。
V8引擎的垃圾回收机制基于分代式垃圾回收机制,简单来说就是将不同存活时长的对象分区管理,并使用不同的回收机制。
分代式垃圾回收机制
基于分代式的理念,V8引擎将堆内存分为新生代和老生代两个区,并有相应的回收算法。
新生代区主要用来存放存活时间较短的对象,大多数的对象刚创建时都会被分配到这里,这个区域较小但是回收频率特别频繁。
新生代区由两部分组成:激活空间(From)与未激活空间(To)。
这两个空间至始至终都只会有一个处于激活状态。
当新生代区开始进行垃圾处理工作时,主要会经历这么几个流程:
标记所有存活的对象(从根节点出发,标记所有可以访问到的变量)。
将存活的对象从激活空间(From)复制到未激活空间(To)。
清空激活空间(From)。
两个空间角色反转, 未激活空间(To)变为新的激活空间(From),原先的激活空间(From)变为未激活空间(To)。
新生代区的垃圾回收过程就是将存活的对象从激活空间(From)复制到未激活空间(To),并完成空间角色的互换。所以缺点也很明显,浪费了一半的空间用于复制。
当一个对象在新生代区中经历多次复制仍然存活,V8引擎就会判定它是一个生命周期较长的对象。
就会在下一次新生代区垃圾回收时,将其移入老生代区进行储存。
这个过程,称之为对象晋升。
对象晋升有两个条件:
新生代区进行垃圾回收时,会判断该对象是否已被复制过一次,如果符合条件,就会将其移入老生代区进行管理。
如果对象没有经过一次复制,但是未激活空间(�To)的内存占比已经超过了25%,则该对象依旧会被转移到老生代区。
这是由于未激活空间(�To)随后就会变成激活空间(From),为了保证后续对象的内存分配,就必须限制内存占比。
老生代区存储着大量的存活对象,所以不可能再像新生区一样浪费一半空间提供复制操作。
老生代区使用两种算法来进行垃圾处理工作:
在标记阶段,V8引擎会从根节点出发,标记所有可以访问到的变量。
在清除阶段,将所有未标记的对象进行清除。
但这个标记-清除过程会导致内存中出现大量不连续的内存空间,也就是出现所谓的内存碎片。从而导致没有足够的内存储存大内存对象。
为了避免出现内存碎片。标记-整理(Mark-Compact)被提了出来。
同样的标记阶段,会标记所有可以访问到的对象。
在整理阶段,会将所有标记的对象往内存的一端移动,移动完成后清理边界外的全部内存。
简单来说,老生代区整体的垃圾回收流程可以总结为:标记-整理-清除三个阶段。
在前面提到过,由于JS是单线程的,所以垃圾回收会阻塞主线程的执行。
如果垃圾回收的标记阶段(遍历堆内存)耗时过长,就会导致页面的卡顿、甚至程序的无响应。
所以为了避免卡顿,V8引擎又引入了增量标记(Incremental Marking)的概念。
简单理解就是,V8引擎会先标记一部分内存对象,然后暂停标记,让出主线程的执行权。等待主线程清空再继续标记工作,直到标记完所有可访问的对象。
后来V8引擎又继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也引入了并行标记和并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
由于全局变量挂载在全局对象上,所以当把全局对象作为根节点进行标记工作时,一定是能访问到全局变量的。
所以全局变量会一直存活,直到整个程序退出,全局执行上下文出栈。
如果不是有必要的,尽可能少创建全局变量。
定时器本身是一个非常有用的功能,但是如果我们稍不注意,忘记在适当的时间手动清除定时器,那么很有可能就会导致内存泄漏。
示例如下:
const numbers = []; const foo = function() { for(let i = 0;i < 100000;i++) { numbers.push(i); } }; window.setInterval(foo, 1000);
在这个示例中,由于我们没有手动清除定时器,导致回调任务会不断地执行下去,回调中所引用的numbers变量也不会被垃圾回收,最终导致numbers数组长度无限递增,从而引发内存泄漏。
闭包是一个能访问已销毁作用域中的变量的函数,其实本质上就是在其内部属性scope中保存了对上级作用域的引用。
所以使用闭包会导致已销毁的作用域占用的内存无法被回收。
ES6中为我们新增了两个有效的数据结构WeakMap和WeakSet,就是为了解决内存泄漏的问题而诞生的。
WeakMap和WeakSet的键名所引用的对象均是弱引用。
也就是说垃圾回收的过程中不会将该键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存。
一文搞懂V8引擎的垃圾回收
The text was updated successfully, but these errors were encountered:
No branches or pull requests
作为目前最流行的javaScript引擎,�V8引擎在底层为我们处理了很多麻烦事,包括自动的垃圾回收管理。
所以在日常开发中,很少会碰到由于内存泄漏导致的程序崩溃问题,尤其是浏览器端。
但了解V8引擎的垃圾回收策略能够帮助我们规避导致内存泄漏的代码,以及写出对V8引擎友好的代码。
什么是垃圾回收?
简单来说,将失去作用的对象所占用的内存空间进行清空,就是垃圾回收。
为什么需要垃圾回收?
当我们创建一个对象时,JS引擎都要在内存空间中分配相应的内存空间来储存这个对象的实体。
试想一下,如果每个对象分配的内存空间都不再被回收,那么整体占用的内存就会越来越大,直到超过V8引擎所持有的内存,从而引起程序崩溃。
所以为了内存的合理分配,就要找到那些不再被使用的对象并将其清除,从而释放内存空间,以供程序的流畅运行。
V8引擎的垃圾回收
由于JS是单线程的,当垃圾回收工作正在进行时,其他任务都要等待。
为了避免因为垃圾回收而阻塞主线程,V8引擎有一套回收机制。
内存限制
V8引擎可使用的内存不是无限制的,具体点来说,在64位操作系统中可使用内存大概有1.4GB,32位操作系统则为0.7GB。
至于为什么要做内存限制,主要有两个原因:
JS在设计之初是作为浏览器脚本语言的,作用就是让用户与浏览器交互、操作DOM、表单验证。
在这种场景下,就很少有占用大内存的情况。
当然后来出现了node.JS,涉及到的复杂I/O操作导致很可能出现内存溢出的情况。
因此V8也为我们提供了调整内存大小的可配置项,不过需要在node初始化时配置,这个就不在本文深究了。
虽然V8会自动进行垃圾回收,但进行一次垃圾回收是很耗时的,并且还会堵塞主线程。
如果V8不对内存进行限制,很可能一次垃圾回收就要处理大量的垃圾,从而导致应用的堵塞。
基于以上两点,V8引擎可使用内存是有限制的。
分代式垃圾回收
V8引擎的垃圾回收机制基于
分代式垃圾回收机制
,简单来说就是将不同存活时长的对象分区管理,并使用不同的回收机制。基于分代式的理念,V8引擎将堆内存分为新生代和老生代两个区,并有相应的回收算法。
新生代区
新生代区主要用来存放存活时间较短的对象,大多数的对象刚创建时都会被分配到这里,这个区域较小但是回收频率特别频繁。
新生代区由两部分组成:激活空间(From)与未激活空间(To)。
这两个空间至始至终都只会有一个处于激活状态。
当新生代区开始进行垃圾处理工作时,主要会经历这么几个流程:
标记所有存活的对象(从根节点出发,标记所有可以访问到的变量)。
将存活的对象从激活空间(From)复制到未激活空间(To)。
清空激活空间(From)。
两个空间角色反转, 未激活空间(To)变为新的激活空间(From),原先的激活空间(From)变为未激活空间(To)。
新生代区的垃圾回收过程就是将存活的对象从激活空间(From)复制到未激活空间(To),并完成空间角色的互换。所以缺点也很明显,浪费了一半的空间用于复制。
对象晋升
当一个对象在新生代区中经历多次复制仍然存活,V8引擎就会判定它是一个生命周期较长的对象。
就会在下一次新生代区垃圾回收时,将其移入老生代区进行储存。
这个过程,称之为对象晋升。
对象晋升有两个条件:
新生代区进行垃圾回收时,会判断该对象是否已被复制过一次,如果符合条件,就会将其移入老生代区进行管理。
如果对象没有经过一次复制,但是未激活空间(�To)的内存占比已经超过了25%,则该对象依旧会被转移到老生代区。
这是由于未激活空间(�To)随后就会变成激活空间(From),为了保证后续对象的内存分配,就必须限制内存占比。
老生代
老生代区存储着大量的存活对象,所以不可能再像新生区一样浪费一半空间提供复制操作。
老生代区使用两种算法来进行垃圾处理工作:
在标记阶段,V8引擎会从根节点出发,标记所有可以访问到的变量。
在清除阶段,将所有未标记的对象进行清除。
但这个标记-清除过程会导致内存中出现大量不连续的内存空间,也就是出现所谓的内存碎片。从而导致没有足够的内存储存大内存对象。
为了避免出现内存碎片。标记-整理(Mark-Compact)被提了出来。
同样的标记阶段,会标记所有可以访问到的对象。
在整理阶段,会将所有标记的对象往内存的一端移动,移动完成后清理边界外的全部内存。
简单来说,老生代区整体的垃圾回收流程可以总结为:标记-整理-清除三个阶段。
V8的垃圾回收优化
在前面提到过,由于JS是单线程的,所以垃圾回收会阻塞主线程的执行。
如果垃圾回收的标记阶段(遍历堆内存)耗时过长,就会导致页面的卡顿、甚至程序的无响应。
所以为了避免卡顿,V8引擎又引入了增量标记(Incremental Marking)的概念。
简单理解就是,V8引擎会先标记一部分内存对象,然后暂停标记,让出主线程的执行权。等待主线程清空再继续标记工作,直到标记完所有可访问的对象。
后来V8引擎又继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也引入了并行标记和并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
如何避免内存泄漏
由于全局变量挂载在全局对象上,所以当把全局对象作为根节点进行标记工作时,一定是能访问到全局变量的。
所以全局变量会一直存活,直到整个程序退出,全局执行上下文出栈。
如果不是有必要的,尽可能少创建全局变量。
定时器本身是一个非常有用的功能,但是如果我们稍不注意,忘记在适当的时间手动清除定时器,那么很有可能就会导致内存泄漏。
示例如下:
在这个示例中,由于我们没有手动清除定时器,导致回调任务会不断地执行下去,回调中所引用的numbers变量也不会被垃圾回收,最终导致numbers数组长度无限递增,从而引发内存泄漏。
闭包是一个能访问已销毁作用域中的变量的函数,其实本质上就是在其内部属性scope中保存了对上级作用域的引用。
所以使用闭包会导致已销毁的作用域占用的内存无法被回收。
ES6中为我们新增了两个有效的数据结构WeakMap和WeakSet,就是为了解决内存泄漏的问题而诞生的。
WeakMap和WeakSet的键名所引用的对象均是弱引用。
也就是说垃圾回收的过程中不会将该键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存。
参考
一文搞懂V8引擎的垃圾回收
The text was updated successfully, but these errors were encountered: