Web 应用内存分析与内存泄漏定位【转】

摘要:
自动内存回收并不意味着我们可以忽略与内存管理相关的操作,但可能会导致更多无法检测的内存泄漏。内存分配和回收作者在《JavaScript EventLoop机制的详细解释和Vue.js中的实际应用》一文中介绍了JavaScript的内存模型。它主要由堆、堆栈和队列组成。队列是指消息队列,堆栈是函数执行堆栈。它的基本结构如下:用户创建的主要对象存储在堆中,这也是我们需要注意的内存分析和内存泄漏定位的主要区域。

作者:王下邀月熊    https://juejin.im/post/6844903508337164296

内存分析与内存泄漏定位是笔者现代 Web 开发工程化实践之调试技巧的一部分,主要介绍 Web 开发中需要了解的内存分析与内存泄露定位手段,本部分涉及的参考资料统一声明在Web 开发界面调试资料索引。

无论是分布式计算系统、服务端应用程序还是 iOS、Android 原生应用都会存在内存泄漏问题,Web 应用自然也不可避免地存在着类似的问题。虽然因为网页往往都是即用即走,较少地存在某个网页长期运行的问题,即使存在内存泄漏可能表现地也不明显;但是在某些数据展示型的,需要长期运行的页面上,如果不及时解决内存泄漏可能会导致网页占据过大地内存,不仅影响页面性能,还可能导致整个系统的崩溃。前端每周清单推荐过的 How JavaScript works 就是非常不错地介绍 JavaScript 运行机制的系列文章,其也对内存管理与内存泄漏有过分析,本文部分图片与示例代码即来自此系列。

类似于 C 这样的语言提供了 malloc()free() 这样的底层内存管理原子操作,开发者需要显式手动地进行内存的申请与释放;而 Java 这样的语言则是提供了自动化的内存回收机制,笔者在垃圾回收算法与 JVM 垃圾回收器综述一文中有过介绍。JavaScript 也是采用的自动化内存回收机制,无论是 Object、String 等都是由垃圾回收进程自动回收处理。自动化内存回收并不意味着我们就可以忽略内存管理的相关操作,反而可能会导致更不易发现的内存泄漏出现。

内存分配与回收

笔者在 JavaScript Event Loop 机制详解与 Vue.js 中实践应用一文中介绍过 JavaScript 的内存模型,其主要也是由堆、栈、队列三方面组成:

Web 应用内存分析与内存泄漏定位【转】第1张

其中队列指的是消息队列、栈就是函数执行栈,其基本结构如下所示:

Web 应用内存分析与内存泄漏定位【转】第2张

而主要的用户创建的对象就存放在堆中,这也是我们内存分析与内存泄漏定位所需要关注的主要的区域。所谓内存,从硬件的角度来看,就是无数触发器的组合;每个触发器能够存放 1 bit 位的数据,不同的触发器由唯一的标识符定位,开发者可以根据该标识符读写该触发器。抽象来看,我们可以将内存当做比特数组,而数据就是在内存中顺序排布:

Web 应用内存分析与内存泄漏定位【转】第3张

JavaScript 中开发者并不需要手动地为对象申请内存,只需要声明变量,JavaScript Runtime 即可以自动地分配内存:

  1. var n = 374; // allocates memory for a number

  2. var s = 'sessionstack'; // allocates memory for a string

  3. var o = {

  4.  a: 1,

  5.  b: null

  6. }; // allocates memory for an object and its contained values

  7. var a = [1, null, 'str'];  // (like object) allocates memory for the

  8.                           // array and its contained values

  9. function f(a) {

  10.  return a + 3;

  11. } // allocates a function (which is a callable object)

  12. // function expressions also allocate an object

  13. someElement.addEventListener('click', function() {

  14.  someElement.style.backgroundColor = 'blue';

  15. }, false);

某个对象的内存生命周期分为了内存分配、内存使用与内存回收这三个步骤,当某个对象不再被需要时,它就应该被清除回收;所谓的垃圾回收器,Garbage Collector 即是负责追踪内存分配情况、判断某个被分配的内存是否有用,并且自动回收无用的内存。大部分的垃圾回收器是根据引用(Reference)来判断某个对象是否存活,所谓的引用即是某个对象是否依赖于其他对象,如果存在依赖关系即存在引用;譬如某个 JavaScript 对象引用了它的原型对象。最简单的垃圾回收算法即是引用计数(Reference Counting),即清除所有零引用的对象:

  1. var o1 = {

  2.  o2: {

  3.    x: 1

  4.  }

  5. };

  6. // 2 objects are created.

  7. // 'o2' is referenced by 'o1' object as one of its properties.

  8. // None can be garbage-collected

  9. var o3 = o1; // the 'o3' variable is the second thing that

  10.            // has a reference to the object pointed by 'o1'.

  11. o1 = 1;      // now, the object that was originally in 'o1' has a        

  12.            // single reference, embodied by the 'o3' variable

  13. var o4 = o3.o2; // reference to 'o2' property of the object.

  14.                // This object has now 2 references: one as

  15.                // a property.

  16.                // The other as the 'o4' variable

  17. o3 = '374'; // The object that was originally in 'o1' has now zero

  18.            // references to it.

  19.            // It can be garbage-collected.

  20.            // However, what was its 'o2' property is still

  21.            // referenced by the 'o4' variable, so it cannot be

  22.            // freed.

  23. o4 = null; // what was the 'o2' property of the object originally in

  24.           // 'o1' has zero references to it.

  25.           // It can be garbage collected.

不过这种算法往往受制于循环引用问题,即两个无用的对象相互引用:

  1. function f() {

  2.  var o1 = {};

  3.  var o2 = {};

  4.  o1.p = o2; // o1 references o2

  5.  o2.p = o1; // o2 references o1. This creates a cycle.

  6. }

  7. f();

稍为复杂的算法即是所谓的标记-清除(Mark-Sweep)算法,其根据某个对象是否可达来判断某个对象是否可用。标记-清除算法会从某个根元素开始,譬如 window 对象开始,沿着引用树向下遍历,标记所有可达的对象为可用,并且清除其他未被标记的对象。

Web 应用内存分析与内存泄漏定位【转】第4张

2012 年之后,几乎所有的主流浏览器都实践了基于标记-清除算法的垃圾回收器,并且各自也进行有针对性地优化。

内存泄漏

所谓的内存泄漏,即是指某个对象被无意间添加了某条引用,导致虽然实际上并不需要了,但还是能一直被遍历可达,以致其内存始终无法回收。本部分我们简要讨论下 JavaScript 中常见的内存泄漏情境与处理方法。在新版本的 Chrome 中我们可以使用 Performance Monitor 来动态监测网页性能的变化:

Web 应用内存分析与内存泄漏定位【转】第5张

上图中各项指标的含义为:

  • CPU usage - 当前站点的 CPU 使用量;

  • JS heap size - 应用的内存占用量;

  • DOM Nodes - 内存中 DOM 节点数目;

  • JS event listeners- 当前页面上注册的 JavaScript 时间监听器数目;

  • Documents - 当前页面中使用的样式或者脚本文件数目;

  • Frames - 当前页面上的 Frames 数目,包括 iframe 与 workers;

  • Layouts / sec - 每秒的 DOM 重布局数目;

  • Style recalcs / sec - 浏览器需要重新计算样式的频次;

当发现某个时间点可能存在内存泄漏时,我们可以使用 Memory 标签页将此时的堆分配情况打印下来:

Web 应用内存分析与内存泄漏定位【转】第6张

Web 应用内存分析与内存泄漏定位【转】第7张Web 应用内存分析与内存泄漏定位【转】第8张

Web 应用内存分析与内存泄漏定位【转】第9张

全局变量

JavaScript 会将所有的为声明的变量当做全局变量进行处理,即将其挂载到 global 对象上;浏览器中这里的 global 对象就是 window:

  1. function foo(arg) {

  2.    bar = "some text";

  3. }

  4. // 等价于

  5. function foo(arg) {

  6.    window.bar = "some text";

  7. }

另一种常见的创建全局变量的方式就是误用 this 指针:

  1. function foo() {

  2.    this.var1 = "potential accidental global";

  3. }

  4. // Foo called on its own, this points to the global object (window)

  5. // rather than being undefined.

  6. foo();

一旦某个变量被挂载到了 window 对象,就意味着它永远是可达的。为了避免这种情况,我们应该尽可能地添加 usestrict 或者进行模块化编码(参考 JavaScript 模块演化简史)。我们也可以扩展类似于下文的扫描函数,来检测出 window 对象的非原生属性,并加以判断:

  1. function scan(o) {

  2.  Object.keys(o).forEach(function(key) {

  3.    var val = o[key];

  4.    // Stop if object was created in another window

  5.    if (

  6.      typeof val !== "string" &&

  7.      typeof val !== "number" &&

  8.      typeof val !== "boolean" &&

  9.      !(val instanceof Object)

  10.    ) {

  11.      debugger;

  12.      console.log(key);

  13.    }

  14.    // Traverse the nested object hierarchy

  15.  });

  16. }

定时器与闭包

我们经常会使用 setInterval 来执行定时任务,很多的框架也提供了基于回调的异步执行机制;这可能会导致回调中声明了对于某个变量的依赖,譬如:

  1. var serverData = loadData();

  2. setInterval(function() {

  3.    var renderer = document.getElementById('renderer');

  4.    if(renderer) {

  5.        renderer.innerHTML = JSON.stringify(serverData);

  6.    }

  7. }, 5000); //This will be executed every ~5 seconds.

定时器保有对于 serverData 变量的引用,如果我们不手动清除定时器话,那么该变量也就会一直可达,不被回收。而这里的 serverData 也是闭包形式被引入到 setInterval 的回调作用域中;闭包也是常见的可能导致内存泄漏的元凶之一:

  1. var theThing = null;

  2. var replaceThing = function () {

  3.  var originalThing = theThing;

  4.  var unused = function () {

  5.    if (originalThing) // a reference to 'originalThing'

  6.      console.log("hi");

  7.  };

  8.  theThing = {

  9.    longStr: new Array(1000000).join('*'),

  10.    someMethod: function () {

  11.      console.log("message");

  12.    }

  13.  };

  14. };

  15. setInterval(replaceThing, 1000);

上述代码中 replaceThing 会定期执行,并且创建大的数组与 someMethod 闭包赋值给 theThing。someMethod 作用域是与 unused 共享的,unused 又有一个指向 originalThing 的引用。尽管 unused 并未被实际使用,theThing 的 someMethod 方法却有可能会被外部使用,也就导致了 unused 始终处于可达状态。unused 又会反向依赖于 theThing,最终导致大数组始终无法被清除。

DOM 引用与监听器

有时候我们可能会将 DOM 元素存放到数据结构中,譬如当我们需要频繁更新某个数据列表时,可能会将用到的数据列表存放在 JavaScript 数组中;这也就导致了每个 DOM 元素存在了两个引用,分别在 DOM 树与 JavaScript 数组中:

  1. var elements = {

  2.    button: document.getElementById('button'),

  3.    image: document.getElementById('image')

  4. };

  5. function doStuff() {

  6.    elements.image.src = 'http://example.com/image_name.png';

  7. }

  8. function removeImage() {

  9.    // The image is a direct child of the body element.

  10.    document.body.removeChild(document.getElementById('image'));

  11.    // At this point, we still have a reference to #button in the

  12.    //global elements object. In other words, the button element is

  13.    //still in memory and cannot be collected by the GC.

  14. }

此时我们就需要将 DOM 树与 JavaScript 数组中的引用皆删除,才能真实地清除该对象。类似的,在老版本的浏览器中,如果我们清除某个 DOM 元素,我们需要首先移除其监听器,否则浏览器并不会自动地帮我们清除该监听器,或者回收该监听器引用的对象:

  1. var element = document.getElementById('launch-button');

  2. var counter = 0;

  3. function onClick(event) {

  4.   counter++;

  5.   element.innerHtml = 'text ' + counter;

  6. }

  7. element.addEventListener('click', onClick);

  8. // Do stuff

  9. element.removeEventListener('click', onClick);

  10. element.parentNode.removeChild(element);

  11. // Now when element goes out of scope,

  12. // both element and onClick will be collected even in old browsers // that don't handle cycles well.

现代浏览器使用的现代垃圾回收器则会帮我们自动地检测这种循环依赖,并且予以清除;jQuery 等第三方库也会在清除元素之前首先移除其监听事件。

iframe

iframe 是常见的界面共享方式,不过如果我们在父界面或者子界面中添加了对于父界面某对象的引用,譬如:

  1. // 子页面内

  2. window.top.innerObject = someInsideObject

  3. window.top.document.addEventLister(‘click’, function() { … });

  4. // 外部页面

  5. innerObject = iframeEl.contentWindow.someInsideObject

就有可能导致 iframe 卸载(移除元素)之后仍然有部分对象保留下来,我们可以在移除 iframe 之前执行强制的页面重载:

  1. <a href="http://t.zoukankan.com/sylvia-Camellia-p-13748080.html#">Remove</a>

  2. <iframe src="http://t.zoukankan.com/url" />

  3. $('a').click(function(){

  4.    $('iframe')[0].contentWindow.location.reload();

  5.    // 线上环境实测重置 src 效果会更好

  6.    // $('iframe')[0].src = "javascript:false";

  7.    setTimeout(function(){

  8.       $('iframe').remove();

  9.    }, 1000);

  10. });

或者手动地执行页面清除操作:

  1. window.onbeforeunload = function(){

  2.    $(document).unbind().die();    //remove listeners on document

  3.    $(document).find('*').unbind().die(); //remove listeners on all nodes

  4.    //clean up cookies

  5.    /remove items from localStorage

  6. }

Web Worker

现代浏览器中我们经常使用 Web Worker 来运行后台任务,不过有时候如果我们过于频繁且不加容错地在主线程与工作线程之间传递数据,可能会导致内存泄漏:

  1. function send() {

  2. setInterval(function() {

  3.    const data = {

  4.     array1: get100Arrays(),

  5.     array2: get500Arrays()

  6.    };

  7.    let json = JSON.stringify( data );

  8.    let arbfr = str2ab (json);

  9.    worker.postMessage(arbfr, [arbfr]);

  10.  }, 10);

  11. }

  12. function str2ab(str) {

  13.   var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char

  14.   var bufView = new Uint16Array(buf);

  15.   for (var i=0, strLen=str.length; i<strLen; i++) {

  16.     bufView[i] = str.charCodeAt(i);

  17.   }

  18.   return buf;

  19. }

在实际的代码中我们应该检测 Transferable Objects 是否正常工作:

  1. let ab = new ArrayBuffer(1);

  2. try {

  3.   worker.postMessage(ab, [ab]);

  4.   if (ab.byteLength) {

  5.      console.log('TRANSFERABLE OBJECTS are not supported in your browser!');

  6.   }

  7.   else {

  8.     console.log('USING TRANSFERABLE OBJECTS');

  9.   }

  10. }

  11. catch(e) {

  12.  console.log('TRANSFERABLE OBJECTS are not supported in your browser!');

  13. }



免责声明:文章转载自《Web 应用内存分析与内存泄漏定位【转】》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Java数组,去掉重复值、增加、删除数组元素IIS部署.net core 3.1踩坑总结下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

window.open参数和技巧

【1、最基本的弹出窗口代码】      <SCRIPT LANGUAGE="javascript">   <!--   window.open ('page.html')   -->   </SCRIPT>      因为着是一段javascripts代码,所以它们应该放在<SCRIPT LANGUAG...

linux性能评估-内存案例实战篇

1.内存泄漏,该如何定位和处理 2.内存中的Buffer 和 Cache 在不同场景下的使用情况 场景 1:磁盘和文件写案例 场景 2:磁盘和文件读案例 1.内存泄漏,该如何定位和处理 机器配置:2 CPU,4GB 内存 预先安装 sysstat、Docker 以及 bcc 软件包,比如: # install sysstat docker s...

js中setTimeout和setInterval的应用方法(转)

JS里设定延时: 使用SetInterval和设定延时函数setTimeout 很类似。setTimeout 运用在延迟一段时间,再进行某项操作。 setTimeout("function",time) 设置一个超时对象 setInterval("function",time) 设置一个超时对象 SetInterval为自动重复,setTimeout不会重...

[转帖]javascript版 UrlEncode和UrlDecode函数

VBScript<script language="vbscript">Function str2asc(strstr)     str2asc = hex(asc(strstr)) End Function Function asc2str(ascasc)     asc2str = chr(ascasc) End Function<...

JavaScript异步编程 ( 一 )

1. 异步编程   Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览...

用近似静态语言、强类型语言的TypeScript开发属于动态语言、弱类型语言的JavaScript

    对于我们写习惯了强类型、静态类型语言的开发人员来讲,开发弱类型、动态类型语言的程序可真是头痛呀。特别是的走微软技术路线,用习惯了强大无比的VS系列工具的开发人员,VS2003,VS2005,VS2008,VS2010,VS2012。。。。。。还有这些工具与其相结合的强类型语言,比如C#,那用起来多爽呀。     先来看看弱类型语言有些特点吧,如果自...