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

3. 内存泄漏

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

3.1 node.js内存补充

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 按钮。过一阵停止记录看结果:

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

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

参考文章

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

js内存深入学习(二)的更多相关文章

  1. js内存深入学习(一)

    一. 内存空间储存 某些情况下,调用堆栈中函数调用的数量超出了调用堆栈的实际大小,浏览器会抛出一个错误终止运行.这个就涉及到内存问题了. 1. 数据结构类型 栈: 后进先出(LIFO)的数据结构  堆 ...

  2. JS三座大山再学习(二、作用域和闭包)

    原文地址 作用域 JS中有两种作用域:全局作用域|局部作用域 栗子1 console.log(name); //undefined var name = '波妞'; var like = '宗介' c ...

  3. JS地毯式学习二

    1.递归算法 a. function box(num){if(num<=1){ return 1;}else{ return num*box(num-1);}}alert(box(5)); b. ...

  4. 转---单页面应用下的JS内存管理

    正文从这开始- 内存问题对于后端童鞋而言可能是家常便饭,特别是C++童鞋.我在实习时做过半年的c++游戏客户端开发(也是前端开发哦),也见识了各式各样的内存问题,就说说我的第一个坑,当时做个需求,就是 ...

  5. 纯JS实现KeyboardNav(学习笔记)二

    纯JS实现KeyboardNav(学习笔记)二 这篇博客只是自己的学习笔记,供日后复习所用,没有经过精心排版,也没有按逻辑编写 这篇主要是添加css,优化js编写逻辑和代码排版 GitHub项目源码 ...

  6. ReactJS入门学习二

    ReactJS入门学习二 阅读目录 React的背景和基本原理 理解React.render() 什么是JSX? 为什么要使用JSX? JSX的语法 如何在JSX中如何使用事件 如何在JSX中如何使用 ...

  7. Web前端-Vue.js必备框架(二)

    Web前端-Vue.js必备框架(二) vue调式工具vue-devtools 过滤器:vue.js允许你自定义过滤器,可被用作一些常见的文本格式化. mustache插值和v-bind表达式. vu ...

  8. JS内存空间详细图解

    JS内存空间详细图解 变量对象与堆内存 var a = 20; var b = 'abc'; var c = true; var d = { m: 20 } 因为JavaScript具有自动垃圾回收机 ...

  9. 前端学习:JS面向对象知识学习(图解)

    前端学习:JS面向对象知识学习(图解) 前端学习:JS(面向对象)代码笔记 JS面向对象图解知识全览 创建类和对象 方式1:使用Object()函数 方式2:使用自变量 方式3:使用工厂函数 创建多个 ...

随机推荐

  1. vbs脚本实现qq定时发消息(初级)

    vbs脚本实现QQ消息定时发送 目标 批处理又称为批处理脚本,强大的强大功能可以高效得实现很多功能,例如批量更改文件格式,批量进行文件读写,今天我们的目标是用vbs脚本编写可以发送qq消息的脚本,并利 ...

  2. Vue.set() this.$set()引发的视图更新思考

    引文 vue文档列表渲染中有条注意事项: 这里提到的两种情况实际改变了数据但是没有触发视图更新. 由此引出Vue.set(),先上文档API: this.$set()和Vue.set()本质方法一样, ...

  3. C. Neko does Maths

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  4. oracle 创建的表为什么在table里没有,但是可以查出来

    有两种的可能: 1这个表在其他用户下创建的,当前用户没有权限访问,此表不在属于当前用户 2查询时写的表名,并不是真正意义的表名,可能指向其他用户,或者就不是这个表

  5. go语言指针理解

  6. python错误和异常

    一:语法错误syntax errors        熟悉语法! 二:异常 ①打印错误信息时,异常的类型作为异常的内置名显示,并以调用栈的形式显示具体信息    ②常见的异常:             ...

  7. 折线图hellocharts的使用说明

    以前用过一次XCL-chart,但是感觉只适合固定图表,不去滑动的那种,因为你一滑动太卡了你懂得(毕竟作者好久没更新优化了),拙言大神我开玩笑的 ,毕竟我加你的群大半年了 - - 第二研究了一下ach ...

  8. Json----简单介绍

    Json 先分享一个网站http://www.bejson.com/,这个是用来检测Json文件的错误的,Json文件一般不好查找错误. 看懂Json只需要四句话: 对象表示为键值对 数据由逗号分隔 ...

  9. gradle环境变量设置_配置注意事项

    看<Spring源码深度解析>的时候,在windows7系统中按照书里配置gradle环境变量,配置完后输入gradle -v,一直显示:'gradle'不是内部或外部命令,也不是可执行的 ...

  10. SSIS - 5.优先约束

      一.优先约束和执行逻辑 任务和容器是SSIS中的可执行文件,一个优先约束连接着两个可执行文件:优先的可执行文件和约束的可执行文件,如下图. 它的执行逻辑如下图: 1)先执行优先可执行文件 2)判断 ...