这次说到的面试题是关于node服务端内存溢出的问题,狗厂员工来面试本想难为一下,现在我连console.log也不敢写了
关于这道node内存溢出的问题,大哥从以下几个方面讲的,讲完我觉得自己得到了升华,现在搞得连代码也快不敢写了
目录
一、所谓的1.4G内存空间
1、javascript与java的简单区别
2、V8引擎为什么只能提供1.4G内存空间
二、内存管理
1、栈内存和堆内存
2、所谓的分区
三、垃圾回收
1、为什么要回收?
2、如何回收?
3、回收策略
四、错误的代码习惯
1、过多的全局变量
2、意外的全局变量
3、引用对象未释放
4、滥用闭包
5、定时器
6、类似array.filter的使用
7、console.log
五、肉眼看不出问题,要善用工具
1、荐使用Easy-Monitor
2、安装依赖
3、植入代码
4、然后正常npm run dev,
5、植入异常代码
6、启动easy-monitor
7、点击Start按钮
8、也可以在第6步,控制界面选择“CPU”,然后“Start”
六、终极杀招
1、执行pm2
2、pm2 restart all
3、试一试reload
编程语言分为编译型语言和解释型语言,javascript是解释型语言,意思就是一边编译一边执行,很显然解释型语言的执行速度是慢于类似于java那些编译型语言的。
为了能够使javascript能够更快速的解析和执行,能够带来更好的用户体验,v8来了。
node是基于v8构建的,所以Node中使用的对象基本都是通过v8引擎来进行分配和管理的,甚至是垃圾回收。然而v8本身对内存的使用做了限制,在64位操作系统下只能用1.4GB的系统内存。
一方面是因为v8最初是为浏览器而设计的,对于浏览器页面来说,很少有需要长时间运行或者使用超大内存的场景,所以1.4GB的内存是足够使用的;
另一方面更重要的原因,v8引擎的垃圾回收机制性能也是有限制的。v8做一次简单的垃圾回收至少需要50ms,而一旦用户量过大的时候,用户流量访问过来运行代码后,要新占用的内存速度远远大于这个50ms回收一次。再垃圾回收过程中,javascript线程会完全暂停,而javascript在当前所在进程中又是单线程的,所以1.4GB也是基于性能的考虑。试想如果分配的是5个GB的内存,那么垃圾回收一次,程序基本就暂停了。
程序运行的时候产生栈内存和堆内存。
栈内存 主要基础数据类型(基本数据类型),局部变量,方法(函数),栈内存由系统分配空间,限定存储的大小,后进先出,访问效率高,或者说查找效率高。
栈内存用完就会被系统自动回收
堆内存 主要存全局变量,引用数据类型。动态分配,总存储空间大,查找效率低。v8要回收的就是不用的堆内存。
新生区 代码运行时新分配的对象或者存活期较短的对象都会存储到新生区
老生区 新生区经过垃圾回收就会晋升到老生区。
大对象区 这里存储超过其他区域大小的大对象,比如一个超级大的map
代码运行区 这个很好理解,运行代码也是需要空间的。
程序在运行时占用了内存,这段程序运行完了并且也不被使用了,没有把占用的内存回收掉,这就形成了一块孤立内存,不断产生孤立内存的过程就可以理解为内存泄漏。由于堆内存是动态分配的,孤立内存占的空间越来越大,这样仅有的1.4GB内存空间就越来越小,然后本来生龙活虎的武林高手在一个狭小的空间胳膊都不能动了,施展不出招式了,然后就被自己运行的内功憋死了。
说的简单点就是v8找到不再使用的变量,然后把这个变量占的内存释放掉。他怎么知道哪些变量不再被使用了呢?
△ 标记清除
当变量进入执行环境时,就标记这个变量为“进入环境”,永远不能清除进入环境的变量占用的内存,因为只要变量进入环境,就有可能会被用到。当变量离开环境时,又被标记为“离开环境”。这个时候就可以清除了。
△、引用计数,计数为0的,进行清除
意思就是说有一张表记录了每个变量的被引用的次数。
执行func返回undefined,按说func obj1 obj2都应该被回收。但里面的变量都被引用了,变量计数都不为0,所以就不能被回收。再看下图可能会加深理解
例如我们一些node java 或者phython的接口里的全局数据,请求完把它存储到global全局存储上,供全局各处需要的地方使用。全局变量是不会被回收的,所以全局变量要少用,不能为了贪图方便到处定义全局变量。当然不用全局变量是不现实的,所以要妥当使用。
function hello () { this.name = "xingyu_qie"; } |
又或者定义变量的时候前面不加修饰符,直接 name = "xingyu_qie"
比如上面我们说的,全局存放了一个大的全局对象,然后我们定义一个变量去引用他
let staticVersion = globalData['version']; |
本来staticVersion如果赋值一个基本类型的话,方法走完了就被系统回收了,但却因为引用了一个对象,使栈和堆里面的一个引用产生了关系,这样staticVersion这个变量就不能被回收,甚至导致当前方法都不能被回收。当用完了应该及时staticVersion = null;进行释放
闭包是最容易造成内存溢出的,因为闭包的逻辑就是方法内外的引用,使某些需要用到的变量被存于缓存中
同样,代码中避免不了使用setInterval,setInterval和setTimeout不同,setInterval会在队列中生成定时任务,使程序每隔多久去执行一次。这本身也是在开销内存,如果定时任务函数内再包含有其他几种错误使用的话,那内存溢出也是早晚的事情了
const arr = [1, 2, 3, 4, 5] const myArr = arr.filter((item) => { return item === 2; }) |
其实遍历并没有在2的时候break掉,会一直执行到最后,这也是一个很大的开销
const obj = {"name": "xingyu_qie", ...此处省略N行, }; console.log(obj); |
我就是打印了它一下,我啥都没有做啊。正是因为obj这个堆里面的变量,被console.log引用了一次,相当于新定义了一个变量去引用了他一次,和第3条引用对象未释放意思差不多,这就释放不掉了。再如果这个console.log被写在了某个定时器里,这不就更加糟糕了吗
它有个缺点,做性能分析的时候需要植入代码,但结合其他几种工具,我觉得这个还比较好用。
npm install easy-monitor
然后植入代码,在index.ts里加就行
成功启动后就可以看到easy-monitor打印的日志,如下图
然后加一些很明显影响性能的代码,比如在业务代码里里加,然后访问localhost:3000/ list/detail/10232,看下图,通过对以上的学习,下面的代码是不是很牛,甚至觉得牛到无以复加
然后该启动easy-monitor管理界面了,新开浏览器页签,访问 http://localhost:12333/index,访问的界面如下图:
点项目名称后面的“Start”按钮,然后就开始刷第5步打开的页面,使劲儿的刷新吧,不过你再怎么刷新肯定也比不上双11的时候群众们的访问速度,但是如果我们手动刷新都能看出效果,证明代码的确是有问题的。再返回easy-monitory管理界面看,稍微刷新了几次,效果还是挺明显的。然后把对应的js代码删了或者优化一下,再过来试试这个走势图吧。
heapUsed: 正在使用的堆内存大小
heapTotal: 当前申请的总堆内存大小
rss: 堆外分配的内存大小
也可以在第6步,控制界面选择“CPU”,然后“Start”,他会跑5分钟,然后告诉你代码哪里费时间了,耗性能了。我们的那段业务代码里果然有问题,也可以点“MEM”查看对应的耗费内存情况
我们使用了各种招式,线上还是有内存溢出的情况,或者大促期间访问量太大,v8回收的速度跟不上程序创建对象占用内存的速度了,怎么办?
通过pm2对node服务进行管理,重启服务。
如下图,执行“pm2 list” 获取到当前运行的node进程,得到id 为0,然后重启指定进程,执行命令“pm2 restart 0”
如果觉得找指定进行麻烦,直接执行“pm2 restart all”,把所有进程都重启了。
其实更多时候不必使用restart的方式,可以使用 “pm2 reload all”,restart是真正的把服务关掉再重启,包含一个stop的过程。而reload是把配置文件重新读一遍重新加载。
如果把restart和reload比作 关闭浏览器再打开 和 F5刷新一下 是不是更形象。
经过大哥的一顿虐,我幡然醒悟,原来console.log也会造成内存溢出。但这是服务端代码。因为服务端不像浏览器那样可以随时刷新。
那么C端代码尽量不把console.log带到线上去是为什么呢?我们继续学习。。。