重学Node系列03-内存管理及GC算法

简介: Node内存控制这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆...

Node内存控制

这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆...

基本介绍


说到node对于内存的控制,可能最先想到的就是node是基于V8构建,因此在node中通过JavaScript使用内存时就会发现只能使用部分内存(64位系统约为1.4GB,32位系统约为0.7GB)。在这样的限制下,将会导致node无法直接操作大内存对象,即使你本机的物理内存有32GB也不行,这与我们传统上的认知形成了一定的差别,接下来先解释一下为什么有这样的差别:

注:64位系统约为1.4GB,32位系统约为0.7GB为默认,也可以用户自定义--max-old-space-size--max-new-space-size来调整,不过只能在启动时指定,node无法运行中自动扩充

首先,V8的设计就是在浏览器的应用场景下完成的,这套内存管理机制在浏览器下使用是绰绰有余的,只是在node中使用有些限制,当然,也有其他方式来解决,只是不能让开发者随心所欲地使用大内存了。

其实深层原因是V8的垃圾回收机制的限制,每次垃圾回收都必须让JavaScript线程暂停,如果垃圾回收时间过长会导致应用的性能和响应能力都会直线下降,所以V8当时的考虑直接限制堆内存是一个好的选择;这中停顿叫做“全停顿”,V8为此也还做了许多优化,这个后续章节会讲到

回到这一部分,介绍一个比较常见的命令:我们在node中输入process.memoryUsage(),就可以很方便地查看node内存使用量的相关信息:

简单解释一下其中的含义,详细参考

注:V8中的所有JavaScript对象都是通过堆来进行分配的

  • rss:Resident Set Size,是该进程所占用的总空间量,包含所有C++JavaScript对象和代码
  • heapTotal:已经申请到的堆内存
  • heapUsed:当前使用的堆内存
  • external:指绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用情况
  • arrayBuffers:包含的Buffer对象,该值包含在external

值得注意的是:V8在上述变量中只负责了堆内存的分配,external包含的内存并不是通过V8管理的,所以我们在external中操作的东西可以不受V8的内存限

垃圾回收机制


总的来说,V8的垃圾回收策略是基于分代式垃圾回收机制,分为新生代和老生代。其中新生代的对象为存活时间较短的对象,老生代的对象为存活时间较长的对象,这里先介绍结论,具体原因后续讲到。

新生代


新生代中主要通过Scavenge算法进行垃圾回收,这是典型的空间换时间的算法,会牺牲一半的存储空间,但速度较快,正好与新生代中对象的特点相对应

该算法主要采取复制的方式进行的垃圾回收,如下图所示:

之前提到过这个算法与新生代的特点向符合,解释一下,新生代里面对象的特点就是生命周期较短,所以在下一次复制过程中存活的对象一般来说是比较少的,而这个算法是只复制存活的对象,所以时间效率上有优异的表现,同时这中将存活中的对象直接在另一半内存空间中依次排列,不会产生老生代那种算法的内存碎片问题(后续会讲到)

晋升

其实一开始对象声明的时候,V8也不知道这个对象是否生命周期较短,那它是如何判断从而将对象区分到新生代区域和老生代区域之中的呢?

答案就是本小节的标题,“晋升”:当一个对象经过多次复制依然存活时或者该对象To空间的占用比超出限制(一般25%),它将会被执行晋升操作,放入到老生代区域之中(注意:只要这两个条件满足一个,就会执行晋升操作,这是个“或”条件)

老生代


结合老生代的特点,V8在老生代中主要采取了Mark-SweepMark-Compact相结合的方式进行垃圾回收,就是标记清除和标记整理

标记清除

  • 标记阶段:标记活着的对象
  • 清除阶段:清除没被标记的对象

老生代的特点就是存活时间长,即失活对象的占比一般来说是比较小的,所以这里是清除死亡的对象是合理的。而不是与新生代算法一致,复制活着的对象,并且由于老生代对象占用内存较大,所以分出一半空间来说也是不合理的

标记整理:在标记清除的基础上提出来的,在对象标记为死亡后,整理的过程中,会将活着的对象往一边移动,移动完成后,直接清理掉边界外的内存。

注意:V8并不是直接采取标记整理的方式来管理老生代,而是通过标记清除和标记整理相结合的方式进行处理,因为它们在处理效率上有较大差别,毕竟标记清理多做了移动的操作。

回收算法 Mark-Sweep Mark-compact Scavenge
速度 中等 最慢 最快
空间开销 少(有碎片) 少(无碎片) 双倍空间(无碎片)
是否移动对象

这种分层级处理方式在计算机中非常常见,最容易想到的就是比如速度上寄存器 > 内存 > 外存,而价格上寄存器 < 内存 < 外存,所以计算机的存储结构就采取了三级分层策略来平衡速度与价格;就像V8中于内存的处理是在时间和空间以及内存碎片等维度上进行平衡的。

所以在取舍中,V8主要使用标记清除算法,在空间不足以对新生代晋升过来的对象进行分配的时候才使用标记整理算法(如下图)

全停顿问题


上述中的三种基本垃圾回收算法都需要将应用逻辑(JavaScript执行线程)暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这样做是为了避免JavaScript应用逻辑与垃圾回收器看到的不一致的情况。这种行为就是文章前面提到的“全停顿”;

而且老生代通常配置得较大,且活动对象较多,全堆垃圾回收得标记、清理、整理等动作造成得停顿就会比较可怕,需要优化:

这就是“增量标记”出现的原因,具体过程就是将原本要以口气停顿完成的动作改为增量标记的方式,也就是拆分为寻多个小“步进”,每做完一个“步进”就让JavaScript执行一小会儿

同时还有延迟清理与增量式整理,让清理和整理的动作也变成增量式等一系列优化操作,这里不深入研究。

内存泄漏


通常造成内存泄漏的原因有这些:

  • 缓存:把内存作缓存,却没有过期策略清除,导致越来越多
  • 队列消费不及时:生产速度远远大于消费速度,队列长度没做限制的话就会无限变大,导致内存泄漏
  • 作用域未释放

内存监控及内存泄漏解决方案


TODO,功力不够,后续来补,主要是还没实践经验,这里先挖个坑。。。



目录
相关文章
|
29天前
|
存储 Kubernetes 容器
【CKA模拟题】查找集群中使用内存最高的node节点
【CKA模拟题】查找集群中使用内存最高的node节点
16 1
|
4月前
|
算法 Java
JVM GC和常见垃圾回收算法
JVM GC和常见垃圾回收算法
48 0
|
4月前
|
缓存 监控 算法
jvm性能调优实战 - 39一次大促导致的内存泄漏和Full GC优化
jvm性能调优实战 - 39一次大促导致的内存泄漏和Full GC优化
73 0
|
3天前
|
运维 JavaScript Java
Serverless 应用引擎产品使用之Nacos 在集中发版时遇到老年代暂满,并且频繁进行 Full GC,但是 GC 后内存没有降下来如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
11 0
|
2月前
|
搜索推荐 JavaScript
NodeJS实现快速排序算法
NodeJS实现快速排序算法
14 0
|
3月前
|
Java 开发者 iOS开发
8 种 Java- 内存溢出之二 -GC overhead limit exceeded
8 种 Java- 内存溢出之二 -GC overhead limit exceeded
|
3月前
|
算法 安全 Java
「译文」Java 垃圾收集参考手册(三):GC 算法基础篇
「译文」Java 垃圾收集参考手册(三):GC 算法基础篇
|
3月前
|
算法 Java
「译文」Java 垃圾收集参考手册(八):GC 算法总结
「译文」Java 垃圾收集参考手册(八):GC 算法总结
|
4月前
|
Java
一招轻松解决node内存溢出问题
一招轻松解决node内存溢出问题
|
4月前
|
存储 算法 Java
JVM GC 算法
对于JVM的垃圾收集(GC),这是一个作为Java开发者必须了解的内容,那么,我们需要去了解哪些内容呢,其实,GC主要是解决下面的三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 回答了这三个问题,也就对于GC算法的原理有了最基本的了解。