内存的生命周期
js环境中分配的内存一般有如下的生命周期:
1、内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
2、内存使用:即读写内存,也就是使用变量、函数等
3、内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
举例:
1 | var a = 20; // 在内存中给数值变量分配空间 |
第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。
现在想想,从内存来看 null
和 undefined
本质的区别是什么?
答:一个变量的原始值就是undefined,而null是一个空值。
为什么typeof(null) //object
而typeof(undefined) //undefined
?
答:typeof(null)为obj的原因是,null为一个变量,他的指针为空
对了,ES6
语法中的 const
声明一个只读的常量。一旦声明,常量的值就不能改变。但是下面的代码可以改变 const
的值,这是为什么?
1 | const foo = {}; |
答:foo 为对象,栈中存的是{}的地址,即使改变了{},其地址没有改变,满足其定义
内存回收机制
JavaScript有自动垃圾收集机制,即找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
在JavaScript中最常用标记清除算法来找到哪些对象是不再继续使用的并给予删除,因此 a = null 其实只是做了一个释放引用的操作,让a原本对应的值失去引用,脱离执行环境, 这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
以Google的V8引擎为例,在V8引擎中所有的JAVASCRIPT对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。
另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。新生代:新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等; 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。
可以通过下面的例子来分析一下垃圾回收机制
1 | function fun1() { |
在上述代码中,当执行var f1 = fun1();
的时候,执行环境会创建一个{name:'csa', age:24}
这个对象,当执行var f2 = fun2();
的时候,执行环境会创建一个{name:'coder', age=2}
这个对象,然后在下一次垃圾回收来临的时候,会释放{name:'csa', age:24}
这个对象的内存,但并不会释放{name:'coder', age:2}
这个对象的内存。这就是因为在fun2()
函数中将{name:'coder, age:2'}
这个对象返回,并且将其引用赋值给了f2
变量,又由于f2
这个对象属于全局变量,所以在页面没有卸载的情况下,f2
所指向的对象{name:'coder', age:2}
是不会被回收的。
垃圾回收算法
对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用了。
引用计数算法
熟悉或者用C语言搞过事的同学的都明白,引用无非就是指向某一物体的指针。对不熟悉这个语言的同学来说,可简单将引用视为一个对象访问另一个对象的路径。(这里的对象是一个宽泛的概念,泛指JS环境中的实体)。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需了。
缺点:循环引用导致内存泄漏: 如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。 正是因为有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了,因为还占有大量市场的IE老祖宗们使用的正是这一算法。在需要照顾兼容性的时候,某些看起来非常普通的写法也可能造成意想不到的问题: 例:
标记清除算法
上面说过,现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
这种方式就会很好的规避了引用计数的循环引用造成的内存泄露的问题。
- 本文作者: gtt
- 本文链接: https://gtt011029.github.io/posts/39252/