├── docs ├── find references from code.md ├── the mechanism of memory management in V8.md ├── the value type in V8.md ├── how to identify leaks.md ├── images │ ├── chapter1 │ │ ├── Crash.png │ │ ├── code.png │ │ ├── badend.png │ │ ├── devtools.png │ │ ├── toCode.png │ │ ├── memory status.png │ │ └── chrome inspect.png │ └── chapter2 │ │ ├── leak.png │ │ ├── bad code.png │ │ ├── memory line.png │ │ ├── memory scope.png │ │ ├── memory stack.png │ │ ├── memory inspect.png │ │ ├── memory line fixed.png │ │ ├── allocation timeline.png │ │ ├── memory dependencies.png │ │ └── memory stack details.png ├── how to prevent leaks.md ├── how to debug memory with Chrome Devtools.md ├── getting started.md ├── how to find memory leaks.md └── demo analyse.md ├── .gitignore ├── demo ├── simpleServer │ ├── package.json │ ├── server.js │ ├── server-lru.js │ └── package-lock.json └── vue leak │ ├── index.html │ └── index_fixed.html ├── README.md └── LICENSE.md /docs/find references from code.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | .DS_Store -------------------------------------------------------------------------------- /docs/the mechanism of memory management in V8.md: -------------------------------------------------------------------------------- 1 | 1. 什么时候会消耗内存 2 | 2. 什么时候会触发内存回收 3 | 3. 长生态和老生态 -------------------------------------------------------------------------------- /docs/the value type in V8.md: -------------------------------------------------------------------------------- 1 | 1. 数值的类型 2 | 2. 数值在内存中是如何存储的? 3 | 3. 什么样的数据才是垃圾 4 | 4. 内存泄露背后的原因 -------------------------------------------------------------------------------- /docs/how to identify leaks.md: -------------------------------------------------------------------------------- 1 | 2 | 1. 应用内存使用是否过大? 3 | 2. 应用多久执行一次GC? 4 | 3. 是否内存一直随着时间而增长? -------------------------------------------------------------------------------- /docs/images/chapter1/Crash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/Crash.png -------------------------------------------------------------------------------- /docs/images/chapter1/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/code.png -------------------------------------------------------------------------------- /docs/images/chapter2/leak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/leak.png -------------------------------------------------------------------------------- /docs/images/chapter1/badend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/badend.png -------------------------------------------------------------------------------- /docs/images/chapter1/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/devtools.png -------------------------------------------------------------------------------- /docs/images/chapter1/toCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/toCode.png -------------------------------------------------------------------------------- /docs/images/chapter2/bad code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/bad code.png -------------------------------------------------------------------------------- /docs/images/chapter1/memory status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/memory status.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory line.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory scope.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory stack.png -------------------------------------------------------------------------------- /docs/images/chapter1/chrome inspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter1/chrome inspect.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory inspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory inspect.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory line fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory line fixed.png -------------------------------------------------------------------------------- /docs/images/chapter2/allocation timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/allocation timeline.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory dependencies.png -------------------------------------------------------------------------------- /docs/images/chapter2/memory stack details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andycall/master-of-javascript-memory/HEAD/docs/images/chapter2/memory stack details.png -------------------------------------------------------------------------------- /docs/how to prevent leaks.md: -------------------------------------------------------------------------------- 1 | 1. 删除DOM节点的时候,同时删除DOM元素的引用 2 | 2. 避免循环对象引用 3 | 3. 使用何时的作用域 4 | 4. 避免在异步代码中访问外部变量 5 | 5. 如果不需要了,就解除事件绑定 6 | 6. 使用合适的方式来管理缓存(lru-cache) -------------------------------------------------------------------------------- /docs/how to debug memory with Chrome Devtools.md: -------------------------------------------------------------------------------- 1 | 1. Performance工具 2 | 2. Memory工具 3 | 1. Heap snapshot (内存快哉) 4 | 2. 采用时间线来查看内存分配 5 | 3. 采用代码视角来查看内存分配 6 | 3. Chrome performance API 7 | 4. 调试Node.js内存 8 | -------------------------------------------------------------------------------- /demo/simpleServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simpleserver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "request.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "lru-cache": "^5.1.1", 14 | "uuid": "^3.3.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/getting started.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 这是一本关于JavaScript内存管理的书,它所面向的读者是活跃在一线的中高级前端工程师。本书的内容将会是你分析内存泄露和优化页面性能的利器。 4 | 5 | 内存管理是每一位前端工程师在工作中都无法避开的一个话题,它既深不可测也十分强大。对于JavaScript而言,它既可以运行在浏览器上,也可以运行在服务器端,本书所包含的内容适合于任何运行JavaScript的平台,包括浏览器和Node.js。 6 | 7 | JavaScript是一门自带内存回收机制(Garbage Collection,英文简写GC)的编程语言,每一行JavaScript代码的背后都有编程语言默默为开发者完成变量内存的分配与回收。通常情况下,分配出去的内存都会被正确的回收回来。但是,当应用变得越来越复杂,代码量多到惊人的时候,代码中难免会出现一些问题,让分配出去的内存收不回来,如此反复下去,就会让应用所占用的内存不断增大,进而导致应用变慢,甚至是应用崩溃。 8 | 9 | 通过阅读本书,你将学会如何判定应用发生了内存泄露,以及如何在开发的过程中避免发生内存泄露,和使用适当的工具来找出问题的具体原因。 -------------------------------------------------------------------------------- /demo/simpleServer/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const uuid = require('uuid'); 3 | 4 | function readDataFromDataBase() { 5 | let cache = {}; 6 | return function(key) { 7 | if (cache[key]) { 8 | return cache[key]; 9 | } 10 | 11 | let data = new Array(10000).fill('xxxx'); 12 | cache[key] = data; 13 | return data; 14 | }; 15 | } 16 | 17 | const cachedDataBase = readDataFromDataBase(); 18 | 19 | const server = http.createServer((req, res) => { 20 | let key = uuid(); 21 | let data = cachedDataBase(key); 22 | res.end(JSON.stringify({ 23 | data: data 24 | })); 25 | }); 26 | 27 | server.listen(3000); 28 | console.log('Server listening to port 3000. Press Ctrl+C to stop it.'); -------------------------------------------------------------------------------- /demo/simpleServer/server-lru.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const uuid = require('uuid'); 3 | const LRU = require('lru-cache'); 4 | 5 | function readDataFromDataBase() { 6 | let cache = new LRU({ 7 | max: 50 8 | }); 9 | 10 | return function(key) { 11 | if (cache.has(key)) { 12 | return cache.get(key); 13 | } 14 | 15 | let data = new Array(10000).fill('xxxx'); 16 | cache.set(key, data); 17 | return data; 18 | }; 19 | } 20 | 21 | const cachedDataBase = readDataFromDataBase(); 22 | 23 | const server = http.createServer((req, res) => { 24 | let key = uuid(); 25 | let data = cachedDataBase(key); 26 | res.end(JSON.stringify({ 27 | data: data 28 | })); 29 | }); 30 | 31 | server.listen(3000); 32 | console.log('Server listening to port 3000. Press Ctrl+C to stop it.'); -------------------------------------------------------------------------------- /demo/simpleServer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simpleserver", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "lru-cache": { 8 | "version": "5.1.1", 9 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 10 | "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 11 | "requires": { 12 | "yallist": "^3.0.2" 13 | } 14 | }, 15 | "uuid": { 16 | "version": "3.3.2", 17 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 18 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 19 | }, 20 | "yallist": { 21 | "version": "3.0.3", 22 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", 23 | "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 内存调试技巧与泄露分析 2 | 3 | 内存管理机制是写出高性能应用程序的必备技能,本书将介绍内存对应用带来的影响以及理解内存背后的机制,让读者学会如何分析内存,调试内存,让内存不再是应用开发中的一个黑盒。 4 | 5 | ## 前言 6 | 这是一本关于JavaScript内存的书,它所面向的读者是活跃在一线的中高级前端工程师。本书的内容将会是你分析内存泄露和优化页面性能的利器。 7 | 8 | 9 | ## 目录 10 | 11 | 1. [前言](./docs/getting%20started.md) 12 | 2. [实例分析:一个内存泄露的demo](./docs/demo%20analyse.md) 13 | 3. [如何发现内存泄露](./docs/how%20to%20find%20memory%20leaks.md) 14 | 4. [如何定位内存泄露](./docs/how%20to%20identify%20leaks.md) 15 | 5. [如何从代码中找出引用关系](./docs/find%20references%20from%20code.md) 16 | 6. [避免内存泄露的技巧](./docs/how%20to%20prevent%20leaks.md) 17 | 7. [V8内存管理基础](./docs/the%20value%20type%20in%20V8.md) 18 | 8. [V8内存管理机制](./docs/the%20mechanism%20of%20memory%20management%20in%20V8.md) 19 | 9. [使用Chrome DevTools 来调试内存](./docs/how%20to%20debug%20memory%20with%20Chrome%20Devtools.md) 20 | 21 | ## 版权许可 22 | 本书采用“保持署名—非商用”创意共享4.0许可证。 23 | 24 | 只要保持原作者署名和非商用,您可以自由地阅读、分享、修改本书。 25 | 26 | 详细的法律条文请参见创意共享网站。 27 | 28 | ## 关于作者 29 | 30 | 我是董天成,是一名活跃在一线开发的前端工程师,目前就职于百度。除了写本书之外,我还在专注一款新式前端应用框架的研发——[RCRE](https://github.com/andycall/RCRE)。 31 | 32 | github: https://github.com/andycall 33 | 34 | zhihu: https://www.zhihu.com/people/dong-tian-cheng/activities -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # Creative Commons Attribution-NonCommercial 4.0 International License 3 | 4 | Disclaimer: This is a human-readable summary of (and not a substitute for) the [license](http://creativecommons.org/licenses/by-nc/4.0/legalcode). 5 | 6 | You are free to: 7 | 8 | - Share — copy and redistribute the material in any medium or format 9 | - Adapt — remix, transform, and build upon the material 10 | 11 | The licensor cannot revoke these freedoms as long as you follow the license terms. 12 | 13 | Under the following terms: 14 | 15 | - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 16 | - NonCommercial — You may not use the material for commercial purposes. 17 | - No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 18 | 19 | Notices: 20 | 21 | You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. 22 | 23 | No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. 24 | -------------------------------------------------------------------------------- /demo/vue leak/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Event Leak Demo 9 | 11 | 12 | 13 |
14 |

这个页面存在一个内存泄露,当反复切换路由之后,Vue实例无法得到释放

15 |

16 | Go to Foo 17 | Go to Bar 18 |

19 | 20 |
21 | 22 | 23 | 24 | 59 | -------------------------------------------------------------------------------- /demo/vue leak/index_fixed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Event Leak Demo 9 | 11 | 12 | 13 |
14 |

修复之后,Vue实例可以被回收

15 |

16 | Go to Foo 17 | Go to Bar 18 |

19 | 20 |
21 | 22 | 23 | 24 | 64 | -------------------------------------------------------------------------------- /docs/how to find memory leaks.md: -------------------------------------------------------------------------------- 1 | # 如何发现内存泄露 2 | 3 | 内存泄露是开发过程中难以发现的一种bug。它经常会变得扑朔迷离,让开发者抓狂。它并不像代码异常那样直接在控制台抛出来,而是静静的隐藏在你的代码中,当你写的代码被更多的人使用的时候给予你致命的一击。 4 | 5 | 那么问题来了,我在开发一个应用的时候,该如何确定应用中存在内存泄露? 6 | 7 | 有的人认为,应用占用内存很多就一定是内存泄露,这其实是一个错误的想法。软件越复杂,占用的内存也会越大,这是再也正常不过的事情了。任何系统的代码,变量,函数都需要占用内存。开发者有时为了加快软件运行速度,还会在应用中大量利用缓存技术。缓存的使用,就会占用一定量的内存,但是缓存和内存泄露的分界线是:缓存不会无限制的占用内存,而内存泄露会。 8 | 9 | 虽然大家也都在说该如何写代码才能避免内存泄露,但是等你真正写出内存泄露的时候,你根本毫无察觉。我线下测试好好的,不是吗? 10 | 11 | 本节将介绍如何使用Chrome的devtools来发现内存泄露问题,能够让你在完成产品功能之后,立刻找出代码中潜在的内存泄露。 12 | 13 | ## 难以发现的泄露 14 | 15 | > 本demo的代码都可以在github上进行获取:[获取地址](https://github.com/andycall/master-of-javascript-memory/blob/master/demo/vue%20leak/index.html) 16 | 17 | 假如现在有个基于Vue开发的SPA页面,页面有2个路由地址,可以自由进行切换。一个页面只显示一段文字,而另外一个页面会实时获取到浏览器框的宽和高。 18 | 19 | ![vue leak](./images/chapter2/leak.png) 20 | 21 | 在这个页面上,有一个潜在的内存泄露问题——如果连续在Foo和Bar之间切换,内存会以肉眼不可见的形式增长。这乍一看貌似问题不大,不过如果我尝试增大Foo页面所占用内存的大小的话,内存泄露的问题就会被放大。 22 | 23 | 救火最佳的时机就是在刚起火的时候来一盆水,那么问题来了,对于这种难以发现的场景,该如何去发现和定位问题呢? 24 | 25 | ### 基于时间线的内存调试工具 26 | 27 | Chrome devtools提供了一种可以基于时间线的内存调试工具——`Allocation instrumentation on timeline`。通过这样的工具,就能很清晰的观察当前内存的状态。 28 | 29 | ![timeline](./images/chapter2/allocation%20timeline.png) 30 | 31 | 打开devtools,选取Memory,然后再选中第二个选项,就可以进行基于时间线的录制了。点击Start按钮,进入录制模式。 32 | 33 | 这时候,使用鼠标在页面上反复切换`Go to Foo`和`Go to Bar`,就可以观察到调试工具不断画出了一些蓝色的线条。 34 | 35 | ![memory line](./images/chapter2/memory%20line.png) 36 | 37 | 如果重复以上步奏,使用[已经修复内存泄露的例子]()进行录制的话,可以得到下图的线条。 38 | 39 | ![memory line fixed](./images/chapter2/memory%20line%20fixed.png) 40 | 41 | 通过肉眼就能很明显的对比出来,在2s之后,第二张图几乎看不到任何蓝色的线条,大部分内容都是由灰色线条组成。而第一张图,每过一段时间就有一小截蓝色线条出现,蓝色和灰色线条各占一半。 42 | 43 | 在这样的图中,**蓝色的线条都代表当前进行了一些内存的分配,而灰色的线条代表GC已经成功将分配出去的内存回收了**。所以从第一张图中,我们就可以直接看出,每一次切换页面之后,都有将近一半的内存收不回来。而这些收不回来的内存就是潜在的泄露。 44 | 45 | ### 分析某一时段的内存 46 | 47 | 现在可以确认第一个例子中,在切换页面的过程中,内存发生了泄露,接下来,可以直接在图表上,选取某一个没有被回收的蓝色线条,来作为分析的依据。 48 | 49 | ![memory scope](./images/chapter2/memory%20scope.png) 50 | 51 | 选择之后,devtools会显示出在当前时间段内,被分配出去的对象列表。这时,我们可以注意到,这里有一个`VueComponent`对象,这说明在进行切换的时候,上一个页面的Vue实例并没有被释放。 52 | 53 | ![memory inspect](./images/chapter2/memory%20inspect.png) 54 | 55 | 点击 `VueComponent @2275067`, 就可以查看在内存中,其他对象和这个Vue对象之间的引用关系。 56 | 57 | ![memory stack](./images/chapter2/memory%20stack.png) 58 | 59 | 选中之后,上图的`Retainers`下面就列举出了所有引用这个`Vue Component`的对象。乍一看感觉挺复杂,这时候如果将这些对象都展开,就会在每一个对象的引用关系链中,找到一个来自于`index.html`的一个`context`。`context`在内存中一般是标识这是一个闭包,由一个函数来生成。 60 | 61 | ![memory stack details](./images/chapter2/memory%20stack%20details.png) 62 | 63 | 这样就能说明,虽然有这么多对象引用这个`Vue Component`,但是它们都被这个来自于`index.html`的一个函数创建的闭包所引用,这时候再顺着右边的链接跳转到代码,就能发现问题所在。 64 | 65 | ![bad code](./images/chapter2/bad%20code.png) 66 | 67 | ## 看似没问题的事件监听 68 | 69 | 确定了问题,再回来看页面的代码。Foo组件的代码很简短,除了渲染数据到页面之外,只有一个监听`Window`对象的事件监听函数,并实时将数据同步到页面上。 70 | 71 | ```javascript 72 | const Foo = { 73 | template: ` 74 |
75 |

the window width: {{width}}px

76 |

the window height: {{height}}px

77 |
`, 78 | data: function() { 79 | return { 80 | width: window.innerWidth, 81 | height: window.innerHeight 82 | } 83 | }, 84 | mounted() { 85 | window.addEventListener('resize', () => { 86 | this.width = window.innerWidth; 87 | this.height = window.innerHeight; 88 | }); 89 | } 90 | }; 91 | ``` 92 | 93 | 这乍一看确实没有什么问题,而且页面确实能够正常运行。不过问题恰好就出在这个事件监听上了,在JavaScript中,事件监听都会一直持有对监听函数的引用,而这个函数恰好是一个箭头函数,它的`this`指向正是Vue的实例,并且在箭头函数内部,就有两行调用`this`的代码。因此就会形成一个链式的引用关系,一直延续到全局的Window对象,导致整个链条上所有的对象都不会被GC所释放。从内存中也可以印证这一点: 94 | 95 | ![memory dependencies](./images/chapter2/memory%20dependencies.png) 96 | 97 | ## 不要忘记解除事件绑定 98 | 99 | 由于事件的绑定,让Vue实例和Window对象之间形成了一条引用关系,从而导致Vue实例无法被释放,因此解决这个问题的关键就是要解除这条引用关系。所以只需要在Vue实例即将要销毁的时候,清空已经绑定的事件,就可以解除Vue 实例和Window之间的关系,从而能够让Vue 实例可以正常被释放。 100 | 101 | ```javascript 102 | const Foo = { 103 | template: ` 104 |
105 |

the window width: {{width}}px

106 |

the window height: {{height}}px

107 |
`, 108 | data: function() { 109 | return { 110 | width: window.innerWidth, 111 | height: window.innerHeight 112 | } 113 | }, 114 | mounted() { 115 | this.resizeFunc = () => { 116 | this.width = window.innerWidth; 117 | this.height = window.innerHeight; 118 | }; 119 | window.addEventListener('resize', this.resizeFunc); 120 | }, 121 | beforeDestroy() { 122 | window.removeEventListener('resize', this.resizeFunc); 123 | this.resizeFunc = null; 124 | } 125 | }; 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /docs/demo analyse.md: -------------------------------------------------------------------------------- 1 | # 实例分析:一个内存泄露的demo 2 | 3 | 内存泄露这类问题的一大特点是它在开发的过程中难以发现。大部分内存泄露问题的发现都是在生产环境阶段发现的,因为内存泄露在通常情况下,并不会影响应用的功能,直到应用运行时间足够长,请求或者操作足够多的话,问题将会暴露,同时也会带来一些损失。而且让开发者更头疼的是,即使发现的应用存在内存泄露,由于缺乏充足的理论知识和调试方法,导致泄露的原因也很难定位。 4 | 5 | 本demo选取的是一个简单的node.js程序,来模拟一次服务器内存泄露导致应用崩溃的事件,并介绍在发现问题之后的一些基本的调试思路和实战方法。如果你对内存泄露的定位一点都不了解的话,本章内容将会给你建立一个基本的调试概念,以便利于后面对问题的详细分析。 6 | 7 | ## 一个内存泄露的HTTP服务器 8 | 9 | > 本demo的代码都可以在github上进行获取:[获取地址](https://github.com/andycall/master-of-javascript-memory/blob/master/demo/simpleServer/server.js) 10 | 11 | 小A在某互联网公司工作,负责一些线上运营活动的后端开发。这个运营活动的服务是一个简易的HTTP服务器,它在每次请求都会『读取』数据库来返回一段数据。 12 | 13 | ```javascript 14 | const http = require('http'); 15 | const uuid = require('uuid'); 16 | 17 | function readDataFromDataBase() { 18 | return new Array(10000).fill('xxxx'); 19 | } 20 | 21 | const server = http.createServer((req, res) => { 22 | let key = uuid(); 23 | let data = readDataFromDataBase(key); 24 | res.end(JSON.stringify({ 25 | data: data 26 | })); 27 | }); 28 | 29 | server.listen(3000); 30 | console.log('Server listening to port 3000. Press Ctrl+C to stop it.'); 31 | ``` 32 | 33 | 小A写完代码之后,在生产环境服务器上,使用下面的命令就把服务启动起来了。 34 | 35 | ```bash 36 | node server.js 37 | ``` 38 | 39 | 由于数据库的性能并不是特别好,所以整体服务的QPS并不是很高。直到有一天,老板找到小A,下周公司要做一个运营活动,用户量应该会增长不少,于是让他优化这个服务器,让它能够支撑更多的流量。小A于是通过一个简单的对象,给`readDataFromDataBase`函数添加了缓存功能。 40 | 41 | ```javascript 42 | const http = require('http'); 43 | const uuid = require('uuid'); 44 | 45 | function readDataFromDataBase() { 46 | let cache = {}; 47 | return function(key) { 48 | if (cache[key]) { 49 | return cache[key]; 50 | } 51 | 52 | let data = new Array(10000).fill('xxxx'); 53 | cache[key] = data; 54 | return data; 55 | }; 56 | } 57 | 58 | const database = readDataFromDataBase(); 59 | 60 | const server = http.createServer((req, res) => { 61 | let key = uuid(); 62 | let data = database(key); 63 | res.end(JSON.stringify({ 64 | data: data 65 | })); 66 | }); 67 | 68 | server.listen(3000); 69 | console.log('Server listening to port 3000. Press Ctrl+C to stop it.'); 70 | ``` 71 | 72 | 通过这样的改造,缓存生效了,老板也很开心,于是小A就把这段代码上到了生产环境,心里想着,等下周公司运营活动搞完,年终奖就有着落啦。 73 | 74 | 公司的运营活动获得了很大的成功,用户量一下子就增长了10倍。这时,小A突然接到用户反馈,说运营页面打不开了,小A紧忙登录到线上看报错日志,就发现node进程在打印出下图的错误之后就直接跪了。 75 | 76 | ![Crash](./images/chapter1/Crash.png) 77 | 78 | 老板很生气,让小A总结这次事故的原因,并后续给团队开展一次Case Study。 79 | 80 | ## 问题调查 81 | 82 | 小A在遇到问题之后一脸懵逼,我线下测试都是好好的,为什么一上线就跪了呢?抱有疑问的他跑去请教公司内的大佬,大佬看了小A的报错信息就说:这是内存泄露,来,我帮你看一下吧。 83 | 84 | ### 内存录制 85 | 86 | 大佬拿到他的代码之后,在启动的node命令后面,添加了一个特殊的参数`--inspect`。 87 | 88 | ```bash 89 | node --inspect server.js 90 | ``` 91 | 92 | 紧接着,大佬打开了Chrome浏览器,在地址栏内输入: `chrome://inspect`,发现刚才运行的server程序就在页面上列出来了。 93 | 94 | ![inspect](./images/chapter1/chrome%20inspect.png) 95 | 96 | 紧接着,大佬点击了下面的`inspect`按钮,一个Chrome Devtools就弹出来了。大佬选取了顶部Memory的tab,并在`Select profilling type`下面选择了`Allocation sampling`。点击下面的蓝色的`Start`按钮,然后录制就开始了。 97 | 98 | ![devtools](./images/chapter1/devtools.png) 99 | 100 | 大佬对小A说,现在内存录制搞好了,接下来就是构造一些请求来访问服务器了。 101 | 102 | ### 请求模拟 103 | 104 | 大佬打开小A电脑的命令行,输入了下面的命令: 105 | 106 | ```bash 107 | ab -n 1000000 -c 100 http://localhost:3000/ 108 | ``` 109 | 110 | "来,让我们再把它打挂吧",大佬边敲命令,边对小A说,我现在用ab这个压力测试工具,向你的服务器以100的并发发送了10W个请求,应该能模拟线上用户突增的场景。 111 | 112 | 大佬执行执行这个命令之后,立刻切换到devtools,发现`JavaScript VM Instance`显示的数字突增,不一会儿,就从不到10MB膨胀到了700MB。这时,大佬露出了满意的微笑,说到:"看,问题复现了",随后点击了页面上的`Stop`按钮,停止了内存的录制,并且退出了刚才执行的ab进程。 113 | 114 | ![badend](./images/chapter1/badend.png) 115 | 116 | ### 内存分析 117 | 118 | 这时,点击了Stop按钮之后,devtools显示出了下图的界面。 119 | 120 | ![memory status](./images/chapter1/memory%20status.png) 121 | 122 | 大佬对小A解释说,看,这个工具把每一行代码所占用的内存给你显示出来了,注意到第一行没有,那段代码占用了99.81%的内存! 123 | 124 | ![toCode](./images/chapter1/toCode.png) 125 | 126 | 大佬点击了最右边的`server.js`,devtools就自动跳转到代码界面了。devtools使用黄色的标识显示了占用内存最大的代码位置——就是小A写的那段缓存代码! 127 | 128 | ![code](./images/chapter1/code.png) 129 | 130 | ### 问题修复 131 | 132 | 大佬阅读了小A写的缓存代码,说道:你这样写肯定会泄露的!你把每次请求的数据都写入到cache这个对象中,那请求越来越多,cache肯定会越来越大嘛。小A说道:可是我需要缓存一些请求的数据,那现在我改怎么办? 133 | 134 | 大佬思考了一下,你可以使用LRU Cache这个数据结构,LRU Cache只会缓存最频繁访问的内容,那些不经常访问的内容都会被自动抛弃掉,这样的话,缓存的大小就不会无限制的增长了,而且还能保证最频繁的内容可以命中缓存。 135 | 136 | 说完,大佬就在命令行中执行下面的命令,安装了一个叫做`lru-cache`的npm包 137 | 138 | ```bash 139 | npm i lru-cache 140 | ``` 141 | 142 | 然后大佬通过这个包,替换到了小A代码中的缓存实现。 143 | 144 | ```javascript 145 | const http = require('http'); 146 | const uuid = require('uuid'); 147 | const LRU = require('lru-cache'); 148 | 149 | function readDataFromDataBase() { 150 | let cache = new LRU({ 151 | max: 50 152 | }); 153 | 154 | return function(key) { 155 | if (cache.has(key)) { 156 | return cache.get(key); 157 | } 158 | 159 | let data = new Array(10000).fill('xxxx'); 160 | cache.set(key, data); 161 | return data; 162 | }; 163 | } 164 | 165 | const cachedDataBase = readDataFromDataBase(); 166 | 167 | const server = http.createServer((req, res) => { 168 | let key = uuid(); 169 | let data = cachedDataBase(key); 170 | res.end(JSON.stringify({ 171 | data: data 172 | })); 173 | }); 174 | 175 | server.listen(3000); 176 | console.log('Server listening to port 3000. Press Ctrl+C to stop it.'); 177 | ``` 178 | 179 | 然后大佬再重新运行服务器,并使用ab工具来进行压力测试,发现整个服务器的内存会一直稳定在100MB以内。 180 | 181 | ## 总结 182 | 183 | 小A同学在开发过程中不注意缓存的大小限制,导致内存一直飙升直至服务崩溃。通过请教大佬,学会了如何通过Chrome devtools来发现内存问题,并采取LRU cache来作为应用缓存,避免了缓存过大的问题。 184 | --------------------------------------------------------------------------------