面试相关问题解答
1、浏览器V8是怎么进行垃圾回收的
浏览器的内存占用是有限制的:
64位系统:
物理内存 > 16G => 最大堆内存限制为4G
物理内存 < 16G => 最大堆内存限制为2G
32位系统:
最大堆内存限制为1G
为什么浏览器要对占用内存做限制呢?
在程序运行的过程中,我们要将不用的内存释放出来,否则复杂的程序会占用很大的内存,导致程序运行缓慢或者卡顿。JavaScript的运行时单线程的,在执行垃圾回收时会阻塞JavaScript应用执行,直到垃圾回收结束才继续执行JavaScript应用逻辑,这种阻塞被称为“全停顿”(stop-the-world)
若V8的内存为1.5G,执行一次小的垃圾回收在50ms以上,做一次非增量级的垃圾回收要1s以上,这样浏览器将在这1s内失去对用户的响应,造成假死想象。如果有动画的话,动画也会收到印象,这样会造成严重的影响。
chrome是很占内存的
chrome很占内存是因为浏览器使用了多进程机制,浏览器的每一个标签页以及扩展都独立占一个进程。访问一个网站,至少要占4个进程:1个浏览器进程,一个GPU进程,一个渲染进程和一个网络进程,除此之外,可能还有多个插件组成的进程架构。而最新的Chrome浏览器运行一个网站包括:1个浏览器进程,一个GPU进程,一个网络进程,多个渲染进程和多个插件进程。【多进程架构优点是更稳定、流畅、安全】【缺点是架构复杂(各模块耦合性高、扩展性差)、占用资源高】
我们的javascript是运行在渲染进程中的。
内存空间
在javascript执行过程中,有三种类型的空间:代码空间、栈空间、堆空间
- 代码空间:存储可执行代码
- 栈空间:调用栈,用来存储可执行上下文,原始类型的数据值和引用类型的地址都是直接存在栈空间
- 堆空间:存储引用类型的值
引用类型为什么一定要存在堆空间中,直接存在堆空间中行不行?
答案是不可以,javascript引擎用栈来维护程序执行期间上下文状态,如果栈空间过大,所有的数据都存在栈空间中,会影响到上下文切换的效率,进而影响到整个程序的执行效率
调用栈切换执行上下文状态
栈内存回收
调用一个函数时,V8引擎会创建一个函数的活动对象【执行上下文】并推入调用栈的栈顶。
活动对象包含这个函数的参数、局部变量、返回值
调用完成后,活动对象从栈中弹出,释放内存。继续执行当前执行环境下剩余的代码
当分配的调用栈空间被占满时,会引发“堆栈溢出”问题
一开始调用栈为空,直到函数被调用,便自动的加入调用栈,执行完成,调用栈自动弹出这个函数。依此类推
综上所述,栈内存是随着函数执行完成调用栈弹出活动对象时自动释放的。函数执行完毕,立即释放,节省内存空间。
堆内存回收
堆内存主要用来存放对象和动态数据的地方。是程序对于内存空间最大的一个地方。同时我们常说的垃圾回收就是指堆内存的垃圾回收。
V8使用垃圾回收机制来管理我们的堆内存。简单来说就是释放孤立(非活跃)对象使用的内存。
如何判断非活跃对象
1、引用计数
每当有引用的地方,就加一,去掉引用就减一,这种方式无法解决循环引用的问题,引用计数都无法为0,导致无法GC,所以V8没有使用这种方法
2、可访问性分析法
V8采用了这种方法。
将一个成为GCRoots的对象(在浏览器环境中,GC Roots可以包括:全局的window对象,所有原生dom节点集合等等)做为所有初始存活的对象集合,从这个对象出发,进行遍历,遍历到就认为是可访问的,标记为活动对象,需要保留,如果没有访问到,就是非活动对象,可能会被垃圾回收。
在浏览器环境中,GCRoots有很多,通常包含一下几种(实际更多):
- 全局的window对象
- 文档DOM树。由可以通过遍历文档到达的所有原生 DOM 节点组成。
- 存放栈上变量
代际假说
代际假说是垃圾回收的一个关键术语,它有两个特点:
- 大部分变量对象的生命周期很短,比如函数内部的变量,块级作用域中的变量。这些代码在函数执行完成就可以清除。
- 少量的对象会存活很久,比如全局的window、Dom、全局api等对象
我们将生命周期短的对象叫新生代,生命周期长的称为老生代。
对于这两种不同的活动对象,V8分别采用不同的垃圾回收机制,达到高效垃圾回收的目的
副垃圾回收器:主要负责新生代的垃圾回收
主垃圾回收器:主要负责老生代的垃圾回收
新生代 - Scavenger算法
新生代分为两个区域,From区-To区,或者称为激活区(new space)-未激活区**(inactive new space)**,这两块区域大小相同,称为Semispace
这是一个牺牲空间换取时间的算法
1、新的对象存放在from-space
2、垃圾回收时,将还活跃的对象复制到to-space
3、清空from-space
4、将from-space和to-space互换,依此类推
scavenger算法需要在每次执行时将存活的对象复制到空闲区域,但是复制需要时间成本,如果新生区空间太大了,复制时间会比较久,所以为了执行效率,新生区空间一般都比较小。
晋升机制
- 【第一次回收->
nursery
子代】 =》【第二次回收 ->intermediate子代】 =》【第三次回收->晋升到老生代】 - to空间超过25%
老生代
主要使用【标记-清除】和【标记-整理】两个算法
Mark-Sweep【标记清除】
也就是上面提到的可访问性分析
遍历堆中的所有对象,递归调用这组跟元素,标记存活和未存活的对象,标记完成后,将未存活对象进行清除。因为这里都是生命周期长的对象,未存活对象比较少,所以效率比较高。
Mark-Compact【标记整理】
标记清除后,导致不连续的存储空间比较多,产生大量不连续的内存碎片,碎片过多无法分配大对象。
标记整理是将所有存活对象向一端移动,然后直接释放掉端边界以外的内存。从而让活动对象占连续的内存。
垃圾回收引起的性能问题
JavaScript是运行在主线程上的,为了避免JavaScript应用逻辑与垃圾回收产生不一致的冲突,垃圾回收执行时,就会占用JavaScript引擎,正在执行的JavaScript脚本会被暂停。
在V8的分代式垃圾回收中,新生代内存比较小,对应的活动对象也比较少,所以执行速度快,全停顿影响也不大。老生代内存比较大,且活动对象比较多,全堆的活动对象标记、清除、整理耗费的时间就比较长,造成的停顿会比较严重。
如何避免内存泄漏
-
少创建全局变量
- 全局变量会在页面关闭时才回收,所以避免创建全局变量,和没有声明的变量(变量提升,变成全局变量)
-
手动清除定时器
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { node.innerHTML = JSON.stringify(someResource)); } }, 1000); someResource = null; // 定时器依然在引用变量无法回收
-
少用闭包
var leaks = (function(){ var leak = 'xxxxxx';// 闭包中引用,不会被回收 return function(){ console.log(leak); } })()
-
清除DOM引用
var element = { image: document.getElementById('image'), button: document.getElementById('button') }; document.body.removeChild(document.getElementById('image')); // 如果element没有被回收,这里移除了 image 节点也是没用的,image 节点依然留存在内存中.
绑定事件回收
let oDiv = document.querySelector('div'); oDiv.onclick = function(){ alert(111111111) } document.body.removeChild(oDiv); oDiv.onclick = null; // 解除事件绑定,触发垃圾回收
-
弱引用(WeakMap和WeakSet)
- 特点:key必须是一个对象,不能是原始值
- 优点:可以快速被垃圾回收
- 缺点:不支持迭代以及keys(), values(),和entries()方法。