最后
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
最后写上我自己一直喜欢的一句名言:
世界上只有一种真正的英雄主义就是在认清生活真相之后仍然热爱它
process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,含义如下:
clipboard.png
rss(resident set size):所有内存占用,包括指令区和堆栈
heapTotal:V8引擎可以分配的最大堆内存,包含下面的 heapUsed
heapUsed:V8引擎已经分配使用的堆内存
external:V8管理C++对象绑定到JavaScript对象上的内存
以上所有内存单位均为字节(Byte)。
如果说想要扩大Node可用的内存空间,可以使用Buffer等堆外内存内存,这里不详细说明了,大家有兴趣可以去看一些资料。
下面是Node的整体架构图,有助于大家理解上面的内容:
clipboard.png
Node Standard Library: 是我们每天都在用的标准库,如Http, Buffer 模块
Node Bindings: 是沟通JS 和 C++的桥梁,封装V8和Libuv的细节,向上层提供基础API服务
第三层是支撑 Node.js 运行的关键,由 C/C++ 实现:
1. V8 是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说它就是 Node.js 的发动机
2. Libuv 是专门为Node.js开发的一个封装库,提供跨平台的异步I/O能力
-
C-ares:提供了异步处理 DNS 相关的能力
-
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力
垃圾回收机制
======
如何判断是否可以回收
1.1 标记清除
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。如何标记变量并不重要,关键在于采取什么策略。
-
(1)垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
-
(2)然后,它会去掉运行环境中的变量以及被环境中变量所引用的变量的标记
-
(3)此后,依然有标记的变量就被视为准备删除的变量,原因是在运行环境中已经无法访问到这些变量了。
-
(4)最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
目前,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾回收策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
clipboard.png
活动对象就是上面的root,如果不清楚活动对象的可以先查一下资料,当一个对象和其关联对象不再通过引用关系被当前root引用了,这个对象就会被垃圾回收。
1.2 引用计数
引用计数的垃圾收集策略不太常见。含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。
如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量改变了引用对象,则该值引用次数减1。
当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
Netscape Navigator 3.0是最早使用引用计数策略的浏览器,但很快它就遇到了一个严重的问题:循环引用。
循环引用是指对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用,看个例子:
function foo () {
var objA = new Object();
var objB = new Object();
objA.otherObj = objB;
objB.anotherObj = objA;
}
这个例子中,objA和objB通过各自的属性相互引用,也就是说,这两个对象的引用次数都是2。
在采用标记清除策略的实现中,由于函数执行后,这两个对象都离开了作用域,因此这种相互引用不是问题。
但在采用引用次数策略的实现中,当函数执行完毕后,objA和objB还将继续存在,因为它们的引用次数永远不会是0。
加入这个函数被重复多次调用,就会导致大量内存无法回收。为此,Netscape在Navigator 4.0中也放弃了引用计数方式,转而采用标记清除来实现其垃圾回收机制。
还要注意的是,我们大部分人时刻都在写着循环引用的代码,看下面这个例子,相信大家都这样写过:
var el = document.getElementById(‘#el’);
el.onclick = function (event) {
console.log(‘element was clicked’);
}
我们为一个元素的点击事件绑定了一个匿名函数,我们通过event参数是可以拿到相应元素el的信息的。
大家想想,这是不是就是一个循环引用呢?el有一个属性onclick引用了一个函数(其实也是个对象),函数里面的参数又引用了el,这样el的引用次数一直是2,即使当前这个页面关闭了,也无法进行垃圾回收。
如果这样的写法很多很多,就会造成内存泄露。我们可以通过在页面卸载时清除事件引用,这样就可以被回收了:
var el = document.getElementById(‘#el’);
el.onclick = function (event) {
console.log(‘element was clicked’);
}
// …
// …
// 页面卸载时将绑定的事件清空
window.onbeforeunload = function(){
el.onclick = null;
}
V8垃圾回收策略
========
自动垃圾回收有很多算法,由于不同对象的生存周期不同,所以无法只用一种回收策略来解决问题,这样效率会很低。
所以,V8采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)和老生代(old generation)。
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象,分别对新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程也叫晋升,后面我会详细说明。
分代内存
====
默认情况下,32位系统新生代内存大小为16MB,老生代内存大小为700MB,64位系统下,新生代内存大小为32MB,老生代内存大小为1.4GB。
新生代平均分成两块相等的内存空间,叫做semispace,每块内存大小8MB(32位)或16MB(64位)。
新生代
===
1. 分配方式
新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就可以了,当存储空间快要满时,就进行一次垃圾回收。
2. 算法
新生代采用Scavenge垃圾回收算法,在算法实现时主要采用Cheney算法。
Cheney算法将内存一分为二,叫做semispace,一块处于使用状态,一块处于闲置状态。
clipboard.png
处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。
我画了一套详细的流程图,接下来我会结合流程图来详细说明Cheney算法是怎么工作的。垃圾回收在下面我统称为 GC(Garbage Collection)。
step1. 在From空间中分配了3个对象A、B、C
step2. GC进来判断对象B没有其他引用,可以回收,对象A和C依然为活跃对象
step3. 将活跃对象A、C从From空间复制到To空间
step4. 清空From空间的全部内存
step5. 交换From空间和To空间
step6. 在From空间中又新增了2个对象D、E
step7. 下一轮GC进来发现对象D没有引用了,做标记
step8. 将活跃对象A、C、E从From空间复制到To空间
step9. 清空From空间全部内存
step10. 继续交换From空间和To空间,开始下一轮
通过上面的流程图,我们可以很清楚的看到,进行From和To交换,就是为了让活跃对象始终保持在一块semispace中,另一块semispace始终保持空闲的状态。
Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的体现。Scavenge的缺点是只能使用堆内存的一半,这是由划分空间和复制机制所决定的。
由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模的应用到所有的垃圾回收中。但我们可以看到,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。
3. 晋升
当一个对象经过多次复制仍然存活时,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。
对象从新生代移动到老生代的过程叫作晋升。
对象晋升的条件主要有两个:
-
对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间移动到老生代空间中,如果没有,则复制到To空间。总结来说,如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。
-
当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
老生代
===
1. 介绍
在老生代中,存活对象占较大比重,如果继续采用Scavenge算法进行管理,就会存在两个问题:
后话
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
对于面试,说几句个人观点。
面试,说到底是一种考试。正如我们一直批判应试教育脱离教育的本质,为了面试学习技术也脱离了技术的初心。但考试对于人才选拔的有效性是毋庸置疑的,几千年来一直如此。除非你有实力向公司证明你足够优秀,否则,还是得乖乖准备面试。这也并不妨碍你在通过面试之后按自己的方式学习。
其实在面试准备阶段,个人的收获是很大的,我也认为这是一种不错的学习方式。首先,面试问题大部分基础而且深入,这些是平时工作的基础。就好像我们之前一直不明白学习语文的意义,但它的意义就在每天的谈话间。
所谓面试造火箭,工作拧螺丝。面试往往有更高的要求,也迫使我们更专心更深入地去学习一些知识,也何尝不是一种好事。