Browser garbage collection with Vue project memory leak analysis

Introduction

Javascript in your browser with automatic garbage collection (GC: Garbage Collecation), that is, procedure code execution environment will be responsible for managing the use of memory. The principle is: garbage collector periodically (periodic) to identify those variables do not continue to use, and then release its memory . However, this process is not real-time, and because of its relatively large overhead GC stops operation in response to the other, so that the garbage collector periodically performed at regular intervals.

Variable that is the end of the life cycle of the variable is no longer used, of course, can only be local variables, global variables lifecycle until unloading browser page will end. Local variables exist only during the execution of the function, and in the process will be for local variables allocated on the heap or on the stack a respective space to store their value, then use these variables in the function, until the function is completed, and the closed package due to internal function, external function and can not be considered an end.

Or the Code illustrate it:

function Fn1 () { 
 var obj = {name: 'hanzichi', Age: 10}; 
} 
function Fn2 () { 
 var obj = {name: 'hanzichi', Age: 10}; 
 return obj; 
} 
var A = Fn1 ( ); 
var Fn2 B = (); 
duplicated code

We look at how the code is executed. First statement two functions, and Fn2 are called fn1, when invoked fn1, fn1 into the environment, will open a memory object stored {name: 'hanzichi', age: 10}, and when the call ends, a fn1 environment, then the memory block is automatically released JS engine garbage collector; fn2 in the called procedure, the object is returned to global variable b points, so the memory block will not be released.

Here the question arises: in the end which variable is of no use? So the garbage collector must keep track of which variables useless in the end, is no longer useful for variables marked mark, for future to recover its memory. Strategy for futility mark of variables may differ due to realized and there usually implemented in two ways: marking Clear and reference counting . Less common reference count, the more commonly used marker clear.

2. Mark Clear

js the most commonly used form of garbage collection is marked cleared. When the variable into the environment, e.g., in the function declaration of a variable, the variable will be marked as "into the environment." Logically, it never is released into the environment variable memory occupied, as long as the execution flow into the corresponding environment, you might use them. When leaving the environment variable, it is marked as "leave the environment."

Test function () { 
var A = 10; // marked, into the environment  
var b = 20; // marked, into the environment 
} 
Test (); // finished after a, b has been marked to leave the environment, is recovered . 
Copy the code

All variables garbage collector running time will be stored in memory are marked (Of course, you can use any marked way). Then, it will remove the tag and environment variables referenced variable environment variables (closures). And then after that variable is marked will be considered ready to delete the variable, because the environment variables have been unable to access these variables. Finally, the garbage collector memory to complete the cleanup, the destruction of the value of those tagged and recovery of memory space they occupy. So far, IE9 +, Firefox, Opera, Chrome, Safari's JS implementations are signs used garbage collection policy remove or similar strategies, but garbage collection time interval different from each other.

3. The reference count

Meaning the number of times each of the reference count value is referenced track record. When declaring a variable and a reference citations when the value is assigned to the variable type, then this value is 1. If a value the same reference number has been assigned to another variable, the value is incremented. Reference Conversely, if the value comprises a reference variable has made another value, this value is decremented by one. When this number becomes the reference value of 0, then no way to access this value, and thus can be recycled back to the memory space occupied. In this way, when the next time the garbage collector runs, it will release that memory reference number value of 0 occupied.

Test function () { 
 var A = {}; // A number of references to the objects 1 
 var A = B; // A reference to the object number plus 1, is 2 
 var C = A; // A pointing object reference number plus 1 as. 3 
 var B = {}; // a reference to an object number minus 1, 2 
} 
copy the code

Netscape Navigator3 the first to use reference counting strategy browser, but it soon encountered a serious problem: the circular reference . Refers to a circular reference object A contains a pointer pointing to the object B, the object B also contains a pointer to object A reference.

Fn function () { 
 var A = {}; 
 var B = {}; 
 a.pro = B; 
 b.pro = A; 
} 
Fn (); 
duplicated code

The above code number of citations a and b are 2, fn finished, the two objects have left the environment, under the mark clear way is no problem, but in reference counting strategy, because a and b are not citations 0, so it will not be garbage collected memory, if a large number fn function is called, it will cause a memory leak. On IE7 and IE8, memory straight up.

We know, IE is not part of the object there is a native JS objects. For example, its memory leak in the DOM and BOM object is to use C ++ implementation in the form of COM objects, and garbage collection mechanism is COM object using reference counting strategy. Therefore, even if IE's engine uses js mark clear strategy to achieve, but JS access to the COM object is still based on reference counting strategy of. In other words, as far as the COM object in IE, it will present circular references.

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;
复制代码

This example between a DOM element element with a native js Object myObject create a circular reference. Wherein e has a property variable myObject pointing element objects; and the variable o element has a property anaphora myObject. Because of this circular reference, even if the case is removed from the page's DOM, it will never be recovered.

Browser garbage collection with Vue project memory leak analysis


  • Orange refers directly referenced js variables in memory

  • Red means js indirectly referenced variables, as shown above, was refB refA indirect reference variable can cause even refB is emptied, it will not be recovered

  • As the child element refB indirect reference parentNode, as long as it is not deleted, all of its parent element (the red part of the figure) will be deleted

another example:

window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){};
};
复制代码

This code looks okay, but obj quoted document.getElementById ( 'element'), while document.getElementById ( 'element') the onclick method is referenced in the external environment variable, naturally including obj, it is not very subtle what. (In newer browsers when removing the Node already on it will remove the event, but in older browsers, in particular, have this bug on IE)

Solution:

The easiest way is to lift their own hand circular reference, such as this can just function

myObject.element = null;
element.o = null;
window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){};
 obj=null;
};
复制代码

将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。

要注意的是,IE9+ 并不存在循环引用导致 DOM 内存泄露问题,可能是微软做了优化,或者 DOM 的回收方式已经改变。

4. 内存管理

4.1 什么时候触发垃圾回收?

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k 的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。

微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和 IE6 相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多

4.2 合理的GC方案

1. 基础方案

Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),即:

  1. 遍历所有可访问的对象。

  2. 回收已不可访问的对象。

2. GC的缺陷

和其他语言一样,JS 的 GC 策略也无法避免一个问题:GC 时,停止响应其他操作,这是为了安全考虑。而 Javascript 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于 JS 游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免GC造成的长时间停止响应。

3. GC优化策略

David 大叔主要介绍了2个优化方案,而这也是最主要的2个优化方案了:

  1. 分代回收(Generation GC) 这个和Java回收策略思想是一致的,也是V8所主要采用的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。如图:

Browser garbage collection with Vue project memory leak analysis


  1. 这里需要补充的是:对于 tenured generation 对象,有额外的开销:把它从 young generation 迁移到 tenured generation,另外,如果被引用了,那引用的指向也需要修改。 这里主要内容可以参考深入浅出Node中关于内存的介绍,很详细~

  2. 增量GC 这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”。如图:

Browser garbage collection with Vue project memory leak analysis


这种方案,虽然耗时短,但中断较多,带来了上下文切换频繁的问题。因为每种方案都其适用场景和缺点,因此在实际应用中,会根据实际情况选择方案。

比如:低 (对象/s) 比率时,中断执行GC的频率,simple GC更低些;如果大量对象都是长期“存活”,则分代处理优势也不大。

5. Vue 中的内存泄漏问题

JS 程序的内存溢出后,会使某一段函数体永远失效(取决于当时的 JS 代码运行到哪一个函数),通常表现为程序突然卡死或程序出现异常。

这时我们就要对该 JS 程序进行内存泄漏的排查,找出哪些对象所占用的内存没有释放。这些对象通常都是开发者以为释放掉了,但事实上仍被某个闭包引用着,或者放在某个数组里面。

5.1 泄漏点

  1. DOM/BOM 对象泄漏;

  2. script 中存在对 DOM/BOM 对象的引用导致;

  3. JS 对象泄漏;

  4. 通常由闭包导致,比如事件处理回调,导致 DOM 对象和脚本中对象双向引用,这个是常见的泄漏原因;

5.2 代码关注点

主要关注的就是各种事件绑定场景,比如:

  1. DOM 中的 addEventLisner 函数及派生的事件监听,比如 Jquery 中的 on 函数,Vue 组件实例的 $on 函数;

  2. 其它 BOM 对象的事件监听, 比如 websocket 实例的 on 函数;

  3. 避免不必要的函数引用;

  4. 如果使用 render 函数,避免在 HTML 标签中绑定 DOM/BOM 事件;

5.3 如何处理

  1. 如果在 mounted/created 钩子中使用 JS 绑定了 DOM/BOM 对象中的事件,需要在 beforeDestroy 中做对应解绑处理;

  2. 如果在 mounted/created 钩子中使用了第三方库初始化,需要在 beforeDestroy 中做对应销毁处理(一般用不到,因为很多时候都是直接全局 Vue.use);

  3. 如果组件中使用了 setInterval,需要在 beforeDestroy 中做对应销毁处理;

5.4 在 vue 组件中处理 addEventListener

调用 addEventListener 添加事件监听后在 beforeDestroy 中调用 removeEventListener 移除对应的事件监听。为了准确移除监听,尽量不要使用匿名函数或者已有的函数的绑定来直接作为事件监听函数。

mounted() {
 const box = document.getElementById('time-line')
 this.width = box.offsetWidth
 this.resizefun = () => {
 this.width = box.offsetWidth
 }
 window.addEventListener('resize', this.resizefun)
 },
 beforeDestroy() {
 window.removeEventListener('resize', this.resizefun)
 this.resizefun = null
 }
复制代码

5.5 观察者模式引起的内存泄漏

在 spa 应用中使用观察者模式的时候如果给观察者注册了被观察的方法,而没有在离开组件的时候及时移除,可能造成重复注册而内存泄漏;

举个栗子: 进入组件的时候 ob.addListener("enter", _func),如果离开组件 beforeDestroy 的时候没有 ob.removeListener("enter", _func),就会导致内存泄漏

5.6 上下文绑定引起的内存泄漏

有时候使用 bind/apply/call 上下文绑定方法的时候,会有内存泄漏的隐患。

var ClassA = function(name) {
 this.name = name
 this.func = null
}
var a = new ClassA("a")
var b = new ClassA("b")
b.func = bind(function() {
 console.log("I am " + this.name)
}, a)
b.func() // 输出: I am a
a = null // 释放a
//b = null; // 释放b
//b.func = null; // 释放b.func
function bind(func, self) { // 模拟上下文绑定
 return function() {
 return func.apply(self)
 }
}
复制代码

使用 chrome dev tool > memory > profiles 查看内存中 ClassA 的实例数,发现有两个实例,a 和 b。虽然 a 设置成 null 了,但是 b 的方法中 bind 的闭包上下文 self 绑定了 a,因此虽然 a 释放,但是 b/b.func 没有释放,闭包的 self 一直存在并保持对 a 的引用。


网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

Browser garbage collection with Vue project memory leak analysis


Guess you like

Origin blog.51cto.com/14516511/2438243