[JS] 垃圾回收机制

JS的垃圾回收机制

一. 垃圾回收机制

程序运行需要内存,无论是高级语言还是低级语言,内存管理都是:

  • 内存分配
  • 内存使用
  • 内存回收

内存使用结束后需要及时回收,不在使用的内存,如果没有及时回收,就叫做内存泄漏。内存泄漏会导致内存使用率变高,轻则影响性能,重则导致系统崩溃

有些语言,例如C语言, 需要手动释放内存,程序员负责内存管理

这样很麻烦,所以很多语言都提供自动内存管理机制,称为垃圾回收机制

二. 回收机制原理

垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存

对于JavaScript而言,最初的垃圾回收机制是采用引用计数法,后来升级到了标记清除法

1. 引用计数

	var obj = {       //引用次数为1
		value: 1
	}  
	var a = obj       //引用计数为2
	obj = null        // 1
	a = null          // 0  可回收

当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另外一个变量,则该值得引用次数加1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。

问题: 循环引用

	function func() {
		var a = {}
		var b = {}

		a.key = b
		b.key = a
	}

在上面的代码中,a 和 b相互引用, 两个的引用计数都是2

在函数执行完毕后,a,b两个变量还将继续存在,因为他们的引用计数永远不可能是零,因此最终放弃了引用计数法,采用标记清除法。

2. 标记-清除

标记清除算法是当前主流的GC算法,V8里用的就是这个。

分为两个阶段:

  • 标记阶段 : 从根节点出发,标记所有可以到达的对象

    function mark(obj) {
    	if(obj.marked) return       //防止重复标记
    	obj.marked = true           //标记活动对象
    	for(o in obj) {             //标记子对象
    		mark(o)
    	}
    }
    mark(global)                    //从根出发,遍历并标记活动对象
    
  • 收集阶段 : 遍历堆,回收所有未被标记的对象

    p = HEAD_START       //HEAD_START HEADT_END分别是堆的开始位置和结束位置
    while(p<HEAD_END){   //遍历堆
    	size = p.size    //对象的大小
    	if(p.marked){    //对于标记对象,清除,为下一次标记作准备
    		p.marked = false
    	}else {
    		free(p)      //清除标记
    	}
    	p += size        
    }
    

    这里面的free函数也不是简单的回收,而是把空闲空间地址记录到一个链表之中,下一次申请空间的时候,就在链表之中查找大小合适的块分配。

问题 1. 内存碎片化

​ 可以看到, 只对内存进行了清除, 但是没有整理. 而内存的申请又是动态的, 就会导致出现很多离散的小片空闲内存. 极端情况甚至可能内存中还有200mb的空闲内存, 但是申请个10kb的空间却找不到.

问题 2.暂停时间长

​ 每次回收都要遍历一遍堆,遍历的时间与堆的大小成正比,堆越大,遍历的时间越长

三. v8的垃圾回收机制


v8的内存限制

在使用Node的时候,发现只能使用部分内存(64位系统中约为1.4GB),这导致Node无法直接操作大的内存对象。

为什么要做这样的限制?

  1. 以1.5GB的垃圾回收堆内存为例,v8做一次小的垃圾回收要50ms以上,做一次非增量式的垃圾回收要1s以上,垃圾回收过程会阻塞JS线程执行,以此直接限制堆内存是一个很好的选择。
  2. v8是为了浏览器的需求而设计的
  3. 可以手工调整内存大小

v8的内存分配

node
> process.memoryUsage()
{
  rss: 23334912,          // resident set size 进程常驻内存
  heapTotal: 5042176,     // 已经申请到的堆内存
  heapUsed: 3167080,      // 堆内存使用量
  external: 1620480,      // v8引擎内部C++对象占用的内存
  arrayBuffers: 9404      // Buffer 使用量 
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iqW4XhyZ-1603977247104)(http://www.ruanyifeng.com/blogimg/asset/2017/bg2017041702-1.png)]

内存分代算法

​ 将对象按不同的存活时间进行分代,v8中主要把内存分为新生代和老生代,新生代存放存活时间较短的对象,老生代中存放存活时间较长或者常驻内存的对象

​ 没有什么回收算法可以胜任所有的场景,根据对象的存活时间从而使用不同的回收算法,从而达到最好的效果。

node --max-old-space-size=1700 test.js  //单位为MB。设置老生代
 
node --max-new-space-size=1024 test.js //单位为KB。设置新生代

​ 通过上面的代码可以设置新生代和老生代的大小,因为大小是启动时就制定的,所以无法动态扩展新生代和老生代的大小

新生代-Scavenge算法

V8的堆内存示意图

在新生代中,主要通过Scavenge算法进行垃圾回收

在Scanvage算法中,他将堆一分为二,两部分中只有一部分处于使用中,另一部分处于闲置状态,处于使用状态的称为From空间,处于闲置状态的称为To空间,当进行垃圾回收时,会检查From空间中的存活对象,把存活的对象复制到To空间中,释放掉非存活的对象,然后From,To空间角色互换。

那么新生代的对象怎么放到老生代中呢?

  1. 对象经历了Scavenge回收

    对象从From空间复制到To空间时,会进行一次判断,如果对象已经经历过了一次Scavenge回收,就把该对象复制到老生代空间中

  2. To空间的内存占比超过了25%

    当对象从From空间复制到To空间时,如果To空间的占比已经使用了25%,就把该对象放到老生代当中。这么做是为了,之后该To空间会转换为From空间,内存分配会在这个空间进行,如果使用占比过大,会对对象分配产生影响

老生代

​ 老生代中由于存活对象占比比较大,显然不可能使用Scavaenge算法,首先会导致效率问题,而且会浪费空间,v8中老生代的垃圾回收主要使用标记清除算法和标记整理算法

​ 标记清除算法在上面已将说过,使用标记清除算法会导致内存碎片化,而标记整理算法就是用来解决这个问题的

​ 标记整理算法在回收的过程中,将活动对象向一侧移动,移动完成后清除掉边界外的内存

增量标记

​ 由于Node单线程的,V8每次回收垃圾的时候,就会把应用逻辑停下来,执行完垃圾回收后在恢复,被称作全停顿,当回收新生代时,由于存活对象相对较少,全停顿没有多大的影响,但是在老生代中进行回收暂停较长的时间。

​ 所以增量标记被提了出来,从标记阶段入手,把之前一次标记全部的活动对象改为了增量标记,拆分成了多个小步进,每完成一次步进就让JavaScript应用逻辑运行一会,垃圾回收与应用逻辑交替进行直到回收完成

四. 内存泄漏

常见的内存泄漏场景

  1. 缓存

  2. 作用域未释放(闭包)

  3. 没必要的全局变量

  4. 无效的DOM引用

    //button 的引用仍在内存中
    function click() {
    	var button = document.getElementById('button')
      button.click()
    }
    
  5. 未清除的定时器

  6. 事件监听未清除

内存泄漏检测方法

​ 经验法则是,连续进行5次垃圾回收后,内存占用一次比一次大,就有内存泄漏

  1. 浏览器。在浏览器中查看内存占用,如果稳定,就不存在内存泄漏

  2. 通过process.memoryUsage()返回的heapUsed字段判断

  3. Node-heapdump.

    var heapdump = require('node-heapdump')
    

    之后可以通过向服务器发送SIGUSR2信号,让node-heapdump抓拍一份堆内存快照

    $ kill -USR2 <pid>
    

    抓拍的快照是JSON格式的可以通过Chorme工具查看

  4. node-profiler

    与node-heapdump类似的抓取内存快照的工具

内存泄漏优化方法

  1. 解除引用
  2. 提供手动清除变量的方法
  3. 避免过多使用闭包
  4. 避免创建过多生命周期较长的对象,或者分解为多个字对象
  5. 清除定时器和事件监听器
  6. 使用stream和buffer操作大文件,不受nodejs内存限制
  7. 使用redis等缓存数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值