Skip to content

Commit

Permalink
doc(chapter2) 完成第二章节内容
Browse files Browse the repository at this point in the history
  • Loading branch information
andycall committed Mar 29, 2019
1 parent 4ff0df3 commit 197f45a
Show file tree
Hide file tree
Showing 21 changed files with 122 additions and 9 deletions.
2 changes: 1 addition & 1 deletion demo/vue leak/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h1>这个页面存在一个内存泄露,当反复切换路由之后,Vue实
<router-view></router-view>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.8/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.2/vue-router.js"></script>
<script>
const Foo = {
Expand Down
2 changes: 1 addition & 1 deletion demo/vue leak/index_fixed.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h1>修复之后,Vue实例可以被回收</h1>
<router-view></router-view>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.8/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.2/vue-router.js"></script>
<script>
const Foo = {
Expand Down
14 changes: 7 additions & 7 deletions docs/demo analyse.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ console.log('Server listening to port 3000. Press Ctrl+C to stop it.');

公司的运营活动获得了很大的成功,用户量一下子就增长了10倍。这时,小A突然接到用户反馈,说运营页面打不开了,小A紧忙登录到线上看报错日志,就发现node进程在打印出下图的错误之后就直接跪了。

![Crash](./images/Crash.png)
![Crash](./images/chapter1/Crash.png)

老板很生气,让小A总结这次事故的原因,并后续给团队开展一次Case Study。

Expand All @@ -91,11 +91,11 @@ node --inspect server.js

紧接着,大佬打开了Chrome浏览器,在地址栏内输入: `chrome://inspect`,发现刚才运行的server程序就在页面上列出来了。

![inspect](./images/chrome%20inspect.png)
![inspect](./images/chapter1/chrome%20inspect.png)

紧接着,大佬点击了下面的`inspect`按钮,一个Chrome Devtools就弹出来了。大佬选取了顶部Memory的tab,并在`Select profilling type`下面选择了`Allocation sampling`。点击下面的蓝色的`Start`按钮,然后录制就开始了。

![devtools](./images/devtools.png)
![devtools](./images/chapter1/devtools.png)

大佬对小A说,现在内存录制搞好了,接下来就是构造一些请求来访问服务器了。

Expand All @@ -111,21 +111,21 @@ node --inspect server.js

大佬执行执行这个命令之后,立刻切换到devtools,发现`JavaScript VM Instance`显示的数字突增,不一会儿,就从不到10MB膨胀到了700MB。这时,大佬露出了满意的微笑,说到:"看,问题复现了",随后点击了页面上的`Stop`按钮,停止了内存的录制,并且退出了刚才执行的ab进程。

![badend](./images/badend.png)
![badend](./images/chapter1/badend.png)

### 内存分析

这时,点击了Stop按钮之后,devtools显示出了下图的界面。

![memory status](./images/memory%20status.png)
![memory status](./images/chapter1/memory%20status.png)

大佬对小A解释说,看,这个工具把每一行代码所占用的内存给你显示出来了,注意到第一行没有,那段代码占用了99.81%的内存!

![toCode](./images/toCode.png)
![toCode](./images/chapter1/toCode.png)

大佬点击了最右边的`server.js`,devtools就自动跳转到代码界面了。devtools使用黄色的标识显示了占用内存最大的代码位置——就是小A写的那段缓存代码!

![code](./images/code.png)
![code](./images/chapter1/code.png)

### 问题修复

Expand Down
113 changes: 113 additions & 0 deletions docs/how to find memory leaks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,116 @@

## 难以发现的泄露

> 本demo的代码都可以在github上进行获取:[获取地址](https://github.com/andycall/master-of-javascript-memory/blob/master/demo/vue%20leak/index.html)
假如现在有个基于Vue开发的SPA页面,页面有2个路由地址,可以自由进行切换。一个页面只显示一段文字,而另外一个页面会实时获取到浏览器框的宽和高。

![vue leak](./images/chapter2/leak.png)

在这个页面上,有一个潜在的内存泄露问题——如果连续在Foo和Bar之间切换,内存会以肉眼不可见的形式增长。这乍一看貌似问题不大,不过如果我尝试增大Foo页面所占用内存的大小的话,内存泄露的问题就会被放大。

救火最佳的时机就是在刚起火的时候来一盆水,那么问题来了,对于这种难以发现的场景,该如何去发现和定位问题呢?

### 基于时间线的内存调试工具

Chrome devtools提供了一种可以基于时间线的内存调试工具——`Allocation instrumentation on timeline`。通过这样的工具,就能很清晰的观察当前内存的状态。

![timeline](./images/chapter2/allocation%20timeline.png)

打开devtools,选取Memory,然后再选中第二个选项,就可以进行基于时间线的录制了。点击Start按钮,进入录制模式。

这时候,使用鼠标在页面上反复切换`Go to Foo``Go to Bar`,就可以观察到调试工具不断画出了一些蓝色的线条。

![memory line](./images/chapter2/memory%20line.png)

如果重复以上步奏,使用[已经修复内存泄露的例子](<https://github.com/andycall/master-of-javascript-memory/blob/master/demo/vue%20leak/index_fixed.html>)进行录制的话,可以得到下图的线条。

![memory line fixed](./images/chapter2/memory%20line%20fixed.png)

通过肉眼就能很明显的对比出来,在2s之后,第二张图几乎看不到任何蓝色的线条,大部分内容都是由灰色线条组成。而第一张图,每过一段时间就有一小截蓝色线条出现,蓝色和灰色线条各占一半。

在这样的图中,**蓝色的线条都代表当前进行了一些内存的分配,而灰色的线条代表GC已经成功将分配出去的内存回收了**。所以从第一张图中,我们就可以直接看出,每一次切换页面之后,都有将近一半的内存收不回来。而这些收不回来的内存就是潜在的泄露。

### 分析某一时段的内存

现在可以确认第一个例子中,在切换页面的过程中,内存发生了泄露,接下来,可以直接在图表上,选取某一个没有被回收的蓝色线条,来作为分析的依据。

![memory scope](./images/chapter2/memory%20scope.png)

选择之后,devtools会显示出在当前时间段内,被分配出去的对象列表。这时,我们可以注意到,这里有一个`VueComponent`对象,这说明在进行切换的时候,上一个页面的Vue实例并没有被释放。

![memory inspect](./images/chapter2/memory%20inspect.png)

点击 `VueComponent @2275067`, 就可以查看在内存中,其他对象和这个Vue对象之间的引用关系。

![memory stack](./images/chapter2/memory%20stack.png)

选中之后,上图的`Retainers`下面就列举出了所有引用这个`Vue Component`的对象。乍一看感觉挺复杂,这时候如果将这些对象都展开,就会在每一个对象的引用关系链中,找到一个来自于`index.html`的一个`context``context`在内存中一般是标识这是一个闭包,由一个函数来生成。

![memory stack details](./images/chapter2/memory%20stack%20details.png)

这样就能说明,虽然有这么多对象引用这个`Vue Component`,但是它们都被这个来自于`index.html`的一个函数创建的闭包所引用,这时候再顺着右边的链接跳转到代码,就能发现问题所在。

![bad code](./images/chapter2/bad%20code.png)

## 看似没问题的事件监听

确定了问题,再回来看页面的代码。Foo组件的代码很简短,除了渲染数据到页面之外,只有一个监听`Window`对象的事件监听函数,并实时将数据同步到页面上。

```javascript
const Foo = {
template: `
<div>
<p>the window width: {{width}}px</p>
<p>the window height: {{height}}px</p>
</div>`,
data: function() {
return {
width: window.innerWidth,
height: window.innerHeight
}
},
mounted() {
window.addEventListener('resize', () => {
this.width = window.innerWidth;
this.height = window.innerHeight;
});
}
};
```

这乍一看确实没有什么问题,而且页面确实能够正常运行。不过问题恰好就出在这个事件监听上了,在JavaScript中,事件监听都会一直持有对监听函数的引用,而这个函数恰好是一个箭头函数,它的`this`指向正是Vue的实例,并且在箭头函数内部,就有两行调用`this`的代码。因此就会形成一个链式的引用关系,一直延续到全局的Window对象,导致整个链条上所有的对象都不会被GC所释放。从内存中也可以印证这一点:

![memory dependencies](./images/chapter2/memory%20dependencies.png)

## 不要忘记解除事件绑定

由于事件的绑定,让Vue实例和Window对象之间形成了一条引用关系,从而导致Vue实例无法被释放,因此解决这个问题的关键就是要解除这条引用关系。所以只需要在Vue实例即将要销毁的时候,清空已经绑定的事件,就可以解除Vue 实例和Window之间的关系,从而能够让Vue 实例可以正常被释放。

```javascript
const Foo = {
template: `
<div>
<p>the window width: {{width}}px</p>
<p>the window height: {{height}}px</p>
</div>`,
data: function() {
return {
width: window.innerWidth,
height: window.innerHeight
}
},
mounted() {
this.resizeFunc = () => {
this.width = window.innerWidth;
this.height = window.innerHeight;
};
window.addEventListener('resize', this.resizeFunc);
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeFunc);
this.resizeFunc = null;
}
};
```

File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
Binary file added docs/images/chapter2/allocation timeline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/bad code.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/leak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory dependencies.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory inspect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory line fixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory line.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory scope.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory stack details.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chapter2/memory stack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 197f45a

Please sign in to comment.