js内存深入学习(二)

继上一篇文章: js内存深入学习(一)

3. 内存泄漏

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

3.1 v8堆内存补充

node.js中V8中的内存分代:

  • 新生代:存活时间较短的对象,会被GC自动回收的对象及作用域,比如不被引用的对象及调用完毕的函数等。
  • 老生代:存活时间较长或常驻内存的对象,比如闭包因为外部仍在引用内部作用域的变量而不会被自动回收,故会被放在常驻内存中,这种就属于在新生代中持续存活,所以被移到了老生代中,还有一些核心模块也会被存在老生代中,例如文件系统(fs)、加密模块(crypto)等
  • 如何调整内存分配大小:
    • 启动node进程时添加参数即可
      node --max-old-space-size=1700 <project-name>.js 调整老生代内存限制,单位为MB(貌似最高也只能1.8G的样子)(老生代默认限制为 64/32 位 => 1400/700 MB)

    • node --max-new-space-size=1024 <project-name>.js 调整新生代内存限制,单位为KB(老生代默认限制为 64/32 位 => 32/16 MB)
      接!

内存回收时使用的算法:

  • Scavenge 算法(用于新生代,具体实现中采用 Cheney 算法)

    • 算法的结果一般只有两种,空间换时间或时间换空间,Cheney属于前者
    • 它将现有的空间分半,一个作为 To 空间,一个作为 From 空间,当开始垃圾回收时会检查 from 空间中存活的对象并赋复制入 To 空间中,而非存活就会被直接释放,完成复制后,两者职责互换,下一轮回收时重复操作,也就是说我们本质上只使用了一半的空间,明显放在老生代这么大的内存浪费一半就很不合适,而且老生代一般生命周期较长,需要复制的对象过多,正因此所以它就被用于新生代中,新生代的生命周期短,一般不会有这么大的空间需要留存,相对来说这是效率最高的选择,刚和适合这个算法
    • 前面我们提到过,如果对象存活时间较长或较大就会从新生代移到老生代中,那么何种条件下会过渡呢,满足以下2个条件中的一个就会被过渡
      • 在一次 from => to 的过程中已经经历过一次 Scavenge 回收,即经过一次新生代回收后,再下次回收时仍然存在,此时这个对象将会从本次的 from 中直接复制到老生代中,否则则正常复制到 To
      • from => to 时,占用 to 的空间达到 25% 时,将会由于空间使用过大自动晋升到老生代中
  • Mark-Sweep & Mark-Compact(用于老生代的回收算法)

    • 新生代的最后我们提到过,Cheney 会浪费一半的空间,这个缺点在老生代是不可原谅的,毕竟老生代有 1.4G 不是,浪费一半就是 700M 啊,而且每次都去复制这么多常驻对象,简直浪费,所以我们是不可能继续采纳 Scavenge 的;
    • mark-sweep 顾名思义,标记清除,上一条我们提到过,我们要杜绝大量复制的情况,因为大部分都是常驻对象,所以 mark-sweep 只会标记死去的老对象,并将其清除,不会去做复制的行为,因为死对象在老生代中占比是很低的,但此时我们很明显看到它的缺点就是清除死去的部分后,可能会造成内存的不连续而在下次分配大对象前立刻先触发回收,但是其实需要回收的那些在上轮已经被清除了,只是没有将活着的对象连续起来 。缺点举例:这就像 buffer 一样,在一段 buffer 中,我们清除了其中断断续续的部分,这些部分就为空了,但是剩下的部分会变得不连续,下次我们分配大对象进来时,大对象是一个整体,我们不可能将其打散分别插入原本断断续续的空间中,否则将变的不连续,下次我们去调用这个大对象时也将变得不连续,这就没有意义了,这就像你将一个人要塞进一个已经装满了家具的房间里一样,各个家具间可能会存在空隙,但是你一个整体的人怎么可能打散分散到这些空间?并在下次调用时在拼到一起呢(什么纳米单位的别来杠,你可以自己想其他例子)
    • 在这个缺点的基础上,我们使用了 mark-compact 来解决,它会在 mark-sweep 标记死亡对象后,将活着的对象全部向一侧移动,移动完成后,一侧全为生,一侧全为死,此时我们便可以直接将死的一侧直接清理,下次分配大对象时,直接从那侧拼接上即可,仿佛就像把家具变成工整了,将一些没用的小家具整理到一侧,将有用的其他家具全部工整摆放,在下次有新家具时,将一侧的小家具全部丢掉,在将新的放到有用的旁边紧密结合。

buffer 声明的都为堆外内存,它们是由系统限定而非 V8 限定,直接由 C++ 进行垃圾回收处理,而不是 V8,在进行网络流与文件 I/O 的处理时,buffer 明显满足它们的业务需求,而直接处理字符串的方式,显然在处理大文件时有心无力。所以由 V8 处理的都为堆内内存。

3.2 识别方法

1、浏览器方法

  • 打开开发者工具,选择 Memory
  • 在右侧的Select profiling type字段里面勾选 timeline
  • 点击左上角的录制按钮。
  • 在页面上进行各种操作,模拟用户的使用情况。
  • 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。

2、命令行方法
使用 Node 提供的 process.memoryUsage 方法。

console.log(process.memoryUsage());
// 输出
{ 
  rss: 27709440,        // resident set size,所有内存占用,包括指令区和堆栈
  heapTotal: 5685248,   // "堆"占用的内存,包括用到的和没用到的
  heapUsed: 3449392,    // 用到的堆的部分
  external: 8772        // V8 引擎内部的 C++ 对象占用的内存
}

判断内存泄漏,以heapUsed字段为准。

3.3 常见内存泄露场景

  • 意外的全局变量

    function foo(arg) {
        bar = "this is a hidden global variable"; // winodw.bar = ...
    }
    

    或者

        function foo() {
            this.variable = "potential accidental global";
        }
        
        // Foo 调用自己,this 指向了全局对象(window)
        // 而不是 undefined
        foo();
    

    解决方法: 在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。

    尽管我们讨论了一些意外的全局变量,但是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或重新分配)。尤其当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。

  • 被遗忘的计时器或回调函数

    如计时器的使用:

        var someResource = getData();
        setInterval(function() {
            var node = document.getElementById('Node');
            if(node) {
                // 处理 node 和 someResource
                node.innerHTML = JSON.stringify(someResource));
            }
        }, 1000);
    

    定义了一个someResource变量,变量在计时器setInterval内部一直被引用着,成为一个闭包使用,即使移除了Node节点,由于计时器setInterval没有停止。其内部还是有对someResource的引用,所以v8不会释放someResource变量的。

        var element = document.getElementById('button');
        function onClick(event) {
            element.innerHTML = 'text';
        }
        
        element.addEventListener('click', onClick);
    

    对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。

    但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用 了。即回收节点内存时,不必非要调用 removeEventListener 了。(不是很理解)

  • 对DOM 的额外引用

    如果把DOM 存成字典(JSON 键值对)或者数组,此时,同一个 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。如果要回收该DOM元素内存,需要同时清除掉这两个引用。

        var elements = {
            button: document.getElementById('button'),
            image: document.getElementById('image'),
            text: document.getElementById('text')
        };
        
        document.body.removeChild(document.getElementById('button'));
        // 此时,仍旧存在一个全局的 #button 的引用(在elements里面)。button 元素仍旧在内存中,不能被回收。
    

    如果代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,我们以为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。

    所以保存 DOM 元素引用的时候,要小心谨慎。

  • 闭包

        var theThing = null;
        var replaceThing = function () {
          var originalThing = theThing;
          var unused = function () {
            if (originalThing)
              console.log("hi");
          };
          theThing = {
            longStr: new Array(1000000).join('*'),
            someMethod: function () {
              console.log(someMessage);
            }
          };
        };
        setInterval(replaceThing, 1000);
    

    代码片段做了一件事情:每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。思绪混乱了吗?最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升(新建的多个originalThing一直被保存在内存中),垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。

    这时候应该在 replaceThing 的最后添加 originalThing = null,主动解除对象引用。

3.4 具体例子

timeline 标签擅长做这些。在 Chrome 中打开例子,打开 Dev Tools ,切换到 timeline,勾选 memory 并点击记录按钮,然后点击页面上的 The Button 按钮。过一阵停止记录看结果:

Chrome

两种迹象显示出现了内存泄漏,图中的 Nodes(绿线)和 JS heap(蓝线)。Nodes 稳定增长,并未下降,这是个显著的信号。

JS heap 的内存占用也是稳定增长。由于垃圾收集器的影响,并不那么容易发现。图中显示内存占用忽涨忽跌,实际上每一次下跌之后,JS heap 的大小都比原先大了。换言之,尽管垃圾收集器不断的收集内存,内存还是周期性的泄漏了。

具体如何使用谷歌F12的工具来分析内存泄漏会再整理,在实战中,如果用到服务端 渲染页面的话,比如SSR渲染,需要更加关注服务端渲染的内存泄漏问题。可以使用pm2对服务端进程进行管理,然后进行压测,观察一段时间,查看内存是否有出现飞涨情况,如果有的话,需要对业务代码和node.js中间件进行排查。

参考文章

https://github.com/yygmind/blog

http://www.cnblogs.com/vajoy/p/3703859.html

https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/

https://blog.csdn.net/yolo0927/article/details/80471220

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 228,505评论 6 533
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,556评论 3 418
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 176,463评论 0 376
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,009评论 1 312
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,778评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,218评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,281评论 3 441
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,436评论 0 288
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,969评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,795评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,993评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,537评论 5 359
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,229评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,659评论 0 26
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,917评论 1 286
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,687评论 3 392
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,990评论 2 374

推荐阅读更多精彩内容

  • 前言 因为node绝大多数时间都是运行在后端的服务器程序,因此,需要精确控制内存。在以前,js程序员不需要控制内存...
    白昔月阅读 5,695评论 5 11
  • 使用JavaScript进行前端开发时几乎完全不需要关心内存管理问题,对于前端编程来说,V8限制的内存几乎不会出现...
    写Blog不取名阅读 10,927评论 9 20
  • 2. NODE模块端实现 2.2 node模块的实现 引入模块: 路径分析 文件定位 编译执行 2.2.1 优先从...
    yozosann阅读 2,147评论 0 0
  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,431评论 1 14
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,121评论 1 32