说明
图解 Google V8 学习笔记
几种内存问题
内存问题可以定义为三类:
- 内存泄漏 (Memory leak):导致页面的性能越来越差;
- 内存膨胀 (Memory bloat):导致页面的性能会一直很差;
- 频繁垃圾回收:导致页面出现延迟或者经常暂停。
内存泄漏
内存泄漏:当进程不再需要某些内存的时候,这些不再被需要的内存依然没有被进程回收。
例子1:使用未定义的变量
function foo() { temp_array = new Array(200000) }
当执行这段代码时,由于函数体内的对象没有被 var、let、const
这些关键字声明,那么 V8 就会使用 this.temp_array
替换 temp_array
。
function foo() { this.temp_array = new Array(200000) }
这里的 this 指向常驻内存 的 window 对象,即便 foo 函数退出了,依然被 window 对象引用,这就造成了 temp_array 的泄漏。
为了解决这个问题,可以在 JavaScript 文件头部加上 use strict;,使用严格模式避免意外的全局变量。
没有加 use strict;,this 指向 window 对象。
加上 use strict;
,this 指向 undefined。
例子2:使用闭包
因为闭包会引用父级函数中定义的变量,如果引用了不被需要的变量,那么也会造成内存泄漏。
function foo(){ var temp_object = new Object() temp_object.x = 1 temp_object.y = 2 temp_object.array = new Array(200000) return function(){ console.log(temp_object.x); } }
那么当调用完 foo 函数之后,由于返回的匿名函数引用了 foo 函数中的 temp_object.x
,这会造成 temp_object 无法被销毁,即便只是引用了 temp_object.x
,也会造成整个 temp_object 对象依然保留在内存中。
从上图可以看出,我们仅仅是需要 temp_object.x
的值,V8 却保留了整个 temp_object 对象。
怎么解决这个问题?
我们可以根据实际情况,来判断闭包中返回的函数到底需要引用什么数据,不需要引用的数据就绝不引用。
可以改成下面的方式:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> function foo(){ var temp_object = new Object() temp_object.x = 1 temp_object.y = 2 temp_object.array = new Array(200000) let kaimo313 = temp_object.x; return function(){ console.log(kaimo313); debugger } } foo()(); </script> </body> </html>
可以看到闭包引用的仅仅是一个 kaimo313 的变量。
例子3:DOM 内存泄漏
如果某个节点已从 DOM 树移除,但 JavaScript 仍然引用它,我们称此节点为 detached。detached 节点是 DOM 内存泄漏的常见原因。
let detachedTree; function create() { var ul = document.createElement('ul'); for (var i = 0; i < 100; i++) { var li = document.createElement('li'); ul.appendChild(li); } detachedTree = ul; } create()
由于 JavaScript 代码中保留了这些元素的引用,导致这些 DOM 元素依然会呆在内存中,这些 DOM 元素从 DOM 上被移除后,它们并不会立即销毁。
内存膨胀
内存膨胀和内存泄漏有一些差异,内存膨胀主要表现在程序员对内存管理的不科学,额外使用过多的内存有可能是没有充分地利用好缓存,也有可能加载了一些不必要的资源。通常表现为内存在某一段时间内快速增长,然后达到一个平稳的峰值继续运行。
比如:只需要 50M 内存就可以搞定的,有些程序员却花费了 500M 内存。
内存膨胀和内存泄漏的关系图:
解决方案:合理规划项目,充分利用缓存等技术来减轻项目中不必要的内存占用。
频繁的垃圾回收
频繁使用大的临时变量,导致了新生代空间很快被装满,从而频繁触发垃圾回收。频繁的垃圾回收操作会让你感觉到页面卡顿。
例子:
function strToArray(str) { let i = 0; const len = str.length; let arr = new Uint16Array(str.length); for (; i < len; ++i) { arr[i] = str.charCodeAt(i); } return arr; } function foo() { let i = 0; let str = "test V8 GC"; while (i++ < 1e5) { strToArray(str); } } foo();
上面这段代码就会频繁创建临时变量,这种方式很快就会造成新生代内存内装满,从而频繁触发垃圾回收。
优化策略:考虑将这些临时变量设置为全局变量。
其他场景的内存问题
来自 sugar
网友:
介绍一个场景:Node.js v4.x ,BFF 层服务端在 js 代码中写了一个 lib 模块 做 lfu、lru 的缓存,用于针对后端返回的数据进行缓存。把内存当缓存用的时候,由于线上 qps 较大的时候,缓存模块被频繁调用,造成了明显的 gc stw 现象,外部表现就是 node 对上游 http 返回逐渐变慢。由于当时上游是 nginx,且 nginx 设置了 timeout retry,因此这个内存 gc 问题当 node 返回时间超出 nginx timeout 阈值时 进而引起了 nginx 大量 retry,迅速形成雪崩效应。后来不再使用这样的当时,改为使用 node 服务器端本地文件 + redis/memcache 的缓存方案,node 做 bff 层时 确实不适合做内存当缓存这种事。
来自 Lorin 网友:
运行场景:K线行情列表
技术方案:websocket 推送二进制数据(2次/秒) -> 转换为 utf-8 格式 -> 检查数据是否相同 -> 渲染到 dom 中
出现问题:页面长时间运行后出现卡顿的现象
问题分析:将二进制数据转换为 utf-8 时,频繁触发了垃圾回收机制
解决方案:后端推送采取增量推送形式
来自 sheeeeep 网友:
介绍一下最近遇到的内存问题,非常粗暴就是 webview 页面内存占用了400多M,加上 app 本身、系统的内存占用,1G内存的移动设备直接白屏。其中部分原因是用 webaudio 加载了十多个音乐文件,用 canvas 加载了几十张小图片。图片直接改成 url 用到的时候再加载到 webgl 中,声音文件按需加载,有了很大的缓解。