思考:
1.JavaScript的内存是如何管理的?
2.Chrom浏览器是如何进行垃圾回收的?
我们带着以上两个思考来阅读文章。
一、JS内存管理
MDN介绍:“JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让 JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。”
内存管理大致分为3个部分:
1.分配你所需要的内存空间,在创建变量时zi'd
2.使用分配到的内存空间进行读写操作等
3.不需要使用内存时,将其空间释放或者归还
JavaScript大致将数据类型分为两个类别,基本类型数据和引用类型数据:
- 基本数据类型:在内存中占据固定的内存空间,值保存在栈空间中,直接可以通过值来访问这些。
- 引用类型:由于引用类型值大小不固定,栈内存中存放地址中的的内存对象,通过引用(地址)来访问的。
var a=123 //给数值变量分配栈内存空间
var etf = 'ARK' // 给字符串也是分配栈内存空间
// 给对象及其值分配堆内存
var obj = {
name:'TOM',
age:13
}
// 给数组及其值分配内存(和对象一样)
var a = [1, null]
// 给函数分配堆内存
function sum(a,b){
return a+b
}
栈内存中的基本数据类型可以通过操作系统直接处理,而堆内存中的引用数据类型,正是由于可以经常变化,大小不固定,因此需要JavaScript的引擎通过垃圾回收机制来处理。
二、垃圾回收
大多数内存管理问题都在这个都在这个阶段,如何判断哪些内存不再需要使用了要进行回收呢?
普通的理解方法参考MND:“垃圾回收机制有两种方式,一种是引用计数垃圾收集,一种是标记-清除算法”
1.引用
垃圾回收算法主要依赖于引用的概念。在内存环境中,一个对象如果具有访问另一个对象的权限就称为引用,分为显示显示引用和隐式引用。一个Javascript对象具有对它原型的引用叫做隐式引用,对它的属性的引用称为显示引用。
引用计数垃圾收集:
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收:
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2 变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个 o2 变量的引用了,“这个对象”的原始引用 o 已经没有
var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在,“这个对象”有两个引用了,一个是 o2,一个是 oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性 a 的对象还在被 oa 引用,所以还不能回收
oa = null; // a 属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
❌循环引用
引用计数垃圾收集算法有个限制:无法处理循环引用的示例,在下面的案例中。两个对象被创建并相互引用,形成了一个循环。它们被调用后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
容易造成内存泄漏的问题。
2.标记-清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法里假定设置一个(root)的对象(全局对象)垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
很好地解决了循环引用问题,在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。
// 可达
var name = 'Tom'
var obj = {
arr: [1, 2, 3]
}
console.log(window.name) // Tom
console.log(window.obj) // { arr: [1, 2, 3] }
console.log(window.obj.arr) // [1, 2, 3]
console.log(window.obj.arr[2]) // 3
function fn () {
var age = 22
}
// 不可达
console.log(window.age) // undefined
❌:那些无法从根对象查询到的对象都将被清除
尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制
以上这些理解往往是不够的,请继续往下学习:
三、V8垃圾回收机制
Javascript中的V8引擎被限制了内存使用((64位约1.4G/1464MB , 32位约0.7G/732MB)),根据不同的操作系统内存大小也会有所不同。
为什么限制V8内存大小呢?
- V8最开始是为浏览器而设计的引擎并未考虑占用过多的内存空间
- 由于被V8的垃圾回收机制所限制,这样会引起Javascript执行的线程被挂起会影响当前执行页面的性能
1. 分代回收
1.1 新生代内存回收
在64位操作系统下分配为32MB,因为新生代中变量的存活时间短,不太容易产生太大的内存压力,因此不够大也是可以理解的。
图中左边是正在使用的内存空间,右边是闲置的内存空间 。V8引擎进行垃圾回收时,会将左边空间中的对象进行检查,会将左边存活的活动对象复制到右边空间,并且将这些对象的内存有序地排列起来,然后将From-space中的非活动对象进行释放,完成之后将from-space和to-space进行互换,这样可以使得新生代中的这两块区域重复利用。如果是顺序放置的比较好处理。
图中橙色方便是存活的内存对象,白色是未分配的内存空间,接下来将介绍一种算法来解决这个问题,Scavenge算法,适用于内存比较小的情况,下图是经过算法处理后,内存空间的排布。
介绍Scavenge算法:
Scavenge算法是一种copy算法,copy存活的对象到to-space中
1.2 老生代内存回收
新生代中的变量如果经过回收后依然一直存在,那么就会直接被放入老生代内存中, 只要是已经经历过一次Scanvenge算法回收的就可以晋升未老生代内存中的对象。老生代中就不适合用Scavenge算法了,在老生代中采用Mark-Sweep(标记-清除)和Mark-Compact(标记-整理)的策略来进行老生代内存回收。
标记阶段和清除阶段:
首先它会遍历堆上的所有对象,分别对它们打上标记,然后在代码执行结束后,对使用过的变量取消标记。在清除阶段,就会把还有标记的进行整体清除,从而释放内存空间。
但是通过标记清除后,还是会存在内存碎片问题。内存碎片多了之后,如果要新来一个较大的内存对象需要存储,会造成影响对于通过标记清除产生的内存碎片,还是需要通过标记整理策略进行解决。
标记整理(Mark-Compact):
每次清理完非活动对象,就会把剩下的活动对象,整理到内存的一侧,整理完成后,直接回收掉边界上的内存。