javascript典型内存泄漏及chrome的排查方法

javascript的内存泄漏

对于JavaScript这门语言的使用者来说,大多数的使用者的内存管理意识都不强。因为JavaScript一直以来都只作为在网页上使用的脚本语言,而网页往往都不会长时间的运行,所以使用者对JavaScript的运行时长和内存控制都比较漠视。但随着Spa(单页应用)、node.js服务端程序和各种js工具的诞生,我们需要重新重视JavaScript的内存管理。

内存泄漏的定义

指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

JavaScript的内存管理

首先JavaScript是一个有Garbage Collection 的语言,也就是我们不需要手动的回收内存。不同的JavaScript引擎有不同的垃圾回收机制,这里我们主要以V8这个被广泛使用的JavaScript引擎为主。

JavaScript内存分配和回收的关键词:GC根、作用域

GC根:一般指全局且不会被垃圾回收的对象,比如:window、document或者是页面上存在的dom元素。JavaScript的垃圾回收算法会判断某块对象内存是否是GC根可达(存在一条由GC根对象到该对象的引用),如果不是那这块内存将会被标记回收。

作用域:在JavaScript的作用域里,我们能够新建对象来分配内存。比如说调用函数,函数执行的过程中就会创建一块作用域,如果是创建的是作用域内的局部对象,当作用域运行结束后,所有的局部对象(GC根无法触及)都会被标记回收,在JavaScript中能引起作用域分配的有函数调用、with和全局作用域。

作用域的分类:局部作用域、全局作用域、闭包作用域

局部作用域

函数调用会创建局部作用域,在局部作用域中的新建的对象,如果函数运行结束后,该对象没有作用域外部的引用,那该对象将会标记回收

全局作用域

每个JavaScript进程都会有一个全局作用域,全局作用域上的引用的对象都是常驻内存的,直到进程退出内存才会自动释放。
手动释放全局作用域上的引用的对象有两种方式:

  • global.foo = undefined

重新赋值改变引用

  • delete global.foo

删除对象属性

闭包作用域

在JavaScript语言中有闭包的概念,闭包指的是包含自由变量的代码块、自由变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。

var closure = (function(){
    //这里是闭包的作用域
    var i = 0 // i就是自由变量
    return function(){
        console.log(i++)
    }
})()

闭包作用域会保持对自由变量的引用。上面代码的引用链就是:

window -> closure -> i

闭包作用域还有一个重要的概念,闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量

常见的几种内存泄漏的方式及使用chrome dev tools的排查方法

用全局变量缓存数据

将全局变量作为缓存数据的一种方式,将之后要用到的数据都挂载到全局变量上,用完之后也不手动释放内存(因为全局变量引用的对象,垃圾回收机制不会自动回收),全局变量逐渐就积累了一些不用的对象,导致内存泄漏

   var x = [];
    function createSomeNodes() {
        var div;
        var i = 10000;
        var frag = document.createDocumentFragment();
        for (; i > 0; i--) {
            div = document.createElement("div");
            div.appendChild(document.createTextNode(i + " - " + new Date().toTimeString()));
            frag.appendChild(div);
        }
        document.getElementById("nodes").appendChild(frag);
    }
    function grow() {
        x.push(new Array(1000000).join('x'));
        createSomeNodes();
        setTimeout(grow, 1000);
    }
    grow()

上面的代码贴一张 timeline的截图
图片描述
主要看memory区域,通过分析代码我们可以知道页面上的dom节点是不断增加的,所以memory里绿色的线(代表dom nodes)也是不断升高的;而代表js heap的蓝色的线是有升有降,当整体趋势是逐渐升高,这是因为js 有内存回收机制,每当内存回收的时候蓝色的线就会下降,但是存在部分内存一直得不到释放,所以蓝色的线逐渐升高

js错误引用DOM元素
    var nodes = '';
    (function () {
        var item = {
            name:new Array(1000000).join('x')
        }
        nodes = document.getElementById("nodes")
        nodes.item = item
        nodes.parentElement.removeChild(nodes)
    })()

这里的dom元素虽然已经从页面上移除了,但是js中仍然保存这对该dom元素的引用。
因为这段代码是只执行一次的,所以用timeline视图会很难分析出来是否存在内存泄漏,所以我们可以用 chrome dev tool 的 profile tab里的heap snapshot 工具来分析。
上面的代码贴一张 heap snapshot 的summary模式的截图
clipboard.png

通过constructor的filter功能,我们把上面代码中创建的长字符串找出来,可以看到代码运行结束后,内存中的长字符串依然没有被垃圾回收掉。
顺带提一下的是右边红框里的shadow size和 retainer size的含义

  • shadow size 指的是对象本地的大小

  • retainer size 指的是对象所引用内存的大小,回收该对象是会将他引用的内存也一并回收,所以retainer size 指代的是回收内存后会释放出来的内存大小

上面我们可以看到 长字符串本身的shadow size和retainer size是一样大的,这是引用长字符串没有引用其他的对象,如果有引用其他对象,那shadow size 和retainer size将不一致。

闭包循环引用
(function(){
    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 someMethod() {
                console.log('someMessage')
            }
        };
    };
    setInterval(replaceThing,100)
})()

首先我们明确一下,unused是一个闭包,因为它引用了自由变量 originalThing,虽然它被没有使用,但v8引擎并不会把它优化掉,因为 JavaScript里存在eval函数,所以v8引擎并不会随便优化掉暂时没有使用的函数。

theThing 引用了someMethod,someMethod这个函数作用域隐式的和unused这个闭包共享一个闭包上下文。所以someMethod也引用了originalThing这个自由变量。

这里面的引用链是:

GCHandler -> replaceThing -> theThing -> someMethod -> originalThing -> someMethod(old) -> originalThing(older)-> someMethod(older)

随着setInterval的不断执行,这条引用链是不会断的,所以内存会不断泄漏,直致程序崩溃。
因为是闭包作用域引起的内存泄漏,这时候最好的选择是使用 chrome的heap snapshot的container视图,我们通过container视图能清楚的看到这条不断泄漏内存的引用链
clipboard.png

由于作者水平有限,文中如有错误还望指出,谢谢!

参考文档:

百科内存泄漏介绍
chrome devtolls
深入浅出nodejs
node-interview


go for front end
go for front end
1.1k 声望
54 粉丝
0 条评论
推荐阅读
淘宝新势力周H5性能优化实战
淘宝新势力周(春上新)是命运石kimi链路(H5链路)第一次承接S级大促,面对S级大促内容丰富且复杂的页面需求,kimi链路遇到了一些性能问题,在未进行性能优化之前,搭建出来的页面,业务方普遍反馈页面卡顿严重...

kukuv237阅读 10.9k评论 4

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木150阅读 12.3k评论 10

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 7.9k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy46阅读 6k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.2k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.3k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木44阅读 7.4k评论 6

1.1k 声望
54 粉丝
宣传栏