前言
不管什么程序语言,内存生命周期基本是一致的:首先,分配需要的内存,然后使用分配到的内存;最后,释放内存。而对于第三个步骤,何时释放内存及释放哪些变量的内存,则需要说那个垃圾回收机制,本文详细介绍javascript中的内存管理和垃圾回收机制。
分配内存
为了不让程序员费心分配内存,javascript在定义变量时就完成了内存分配
有些函数调用结果是分配对象内存
有些函数分配新变量或者新对象
【存储方式】
因为原始值占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中。
由于复杂值的大小会改变,所以不能将其存储在栈中,否则会降低变量的查询速度,因此其存储在堆中,存储在变量处的值是一个指针,指向存储对象的内存地址。
使用内存
使用值的过程实际上是对分配内存进行读取与写入的操作,读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数
释放内存
大多数内存管理的问题都在这个阶段。在这里碎艰难的任务是找到‘所分配的内存确实已经不再需要’
javascript内嵌了垃圾回收器,用来跟踪内存的分配和使用,以便当分配的内存不再使用的时候,自动释放它。垃圾收集器会按照固定时间间隔,或者代码执行中预定的收集时间,周期性的执行这一操作
局部变量只在函数执行的过程中存在,而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储他们的值。然后在函数中使用这些变量,直到函数执行结束。此时,局部变量就没有存在的必要了。因此可以释放他们的内存以供将来使用。在这种情况下,很容易判断变量是否还有存在的必要;但是并非所有的情况下都这么容易就能得到结论。
垃圾回收器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以便将来收回其所占用的内存。用于标记无用变量的策略通常有标记清楚和应用技术两种
引用计数
引用计数是最简单的垃圾收集算法,这种算法把‘对象是否需要’简化定义为“对象有没有其他对象引用它”。如果没有引用指向该对象(零引用),对象将被回收机制回收
下面代码中,量个对象a,b,被创建,一个作为另一个的属性被引用,另一个被分配给变量o
o2引用了o
'这个对象'的原始引用o被o2替换了
现在“这个对象”有两个引用了,一个是o2一个是oa
最初的对象现在已经是零引用了,然而它的属性a的对象还在被oa引用,所以还不能回收
a属性的那个对象现在也是零引用了,它可以被回收机制回收了
【循环引用】
Netscape Navigator3.0是最早使用引用计数策略的浏览器,但是很快它就遇到了一个严重的问题——循环引用
引用计数算法有两个限制:无法处理循环引用,在下面的例子中,两个对象被创建,并互相引用,形成一个循环。他们被调用之后不会离开函数作用域,所以他们已经没有用了,可以被回收了。然而引用计算法考虑到它们互相都有至少一次引用,所以他们不会被回收
【IE低版本】
IE8-浏览器中,有一部分对象并不是原生javascript对象,例如,其BOM和DOM中的对象就是使用c++以COM(component Object Model 组件对象模型)对象的形式实现,而COM对象的垃圾回收机制采用的就是引用计数策略。该方式常常造成对象被循环引用时内存发生泄漏
这个例子在一个DOM元素(element)与一个原生javascript对象(myObject)之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象,而变量element也有一个属性名为someObject的属性指向myObject。由于存在这个循环引用,即使将例子中的DOM从页面中移除,它也永远不会被回收
为了避免类似这样的循环引用,最好是在不使用它们的时候手工断开原生javascript和DOM元素之间的连接
将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存
为了解决此问题,IE9把BOM和DOM对象都转换成了真正的javascript对象
清楚标记
javascript中最常用的垃圾收集算法是标记清除(mark-and-sweep),这个算法把‘对象是否需要’简化定义为‘对象是否可以到达’。如果对象不可以到达,对象将被垃圾回收机制回收。
大多数浏览器实现使用的都是标记清除式的垃圾收集策略,只不过垃圾收集的时间互有不同
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以到达的对象和所有不能到达的对象
该算法称为标记清除,是因为分为标记(mark)和清除(sweep)两个阶段
在标记阶段,垃圾回收器会从根对象开始遍历,每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象
在清除阶段,垃圾回收器会对内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作
在标记阶段,从根对象1可以访问到B,从B又可以访问到E,那么B和E都是可到达对象,同样的道理,F、G、J和K都是可到达对象。在回收阶段,所有未标记为可到达的对象都会被垃圾回收器回收
【循环引用】
使用标记清除算法,循环引用不再是问题,上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收
性能问题
垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集时间间隔是一个非常重要的问题
IE的垃圾收集器是根据内存分配量运行的。具体一点说,就是256个变量,4096个对象(或数组)字面量和数组元素(slot)或者64kb的字符串。达到上述任何一个临界值,垃圾收集器就会运行
这种实现方式的问题在于,如果一个脚本中包含那么多变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量。而这样一来,垃圾收集器就不得不频繁地运行。结果,由此引发的严重性能问题促使IE7重写了其垃圾收集例程
IE7的javascript引擎的垃圾收集例程改变了工作方式:触发垃圾收集的变量分配、字面量和数组元素的临界值被调整为动态修正。IE7中的各项临界值在初始时与IE6相等。如果垃圾收集例程回收的内存分配量低于15%,则变量、字面量和数组元素的临界值就会加倍。如果例程回收了85%的内存分配量,则将各种临界值重置回默认值。这样,极大地提升了IE在运行包含大量javascript的页面时的性能
事实上,在有的浏览器中可以触发垃圾收集过程。在IE中,调用window.CollectGarbage()方法会立即执行垃圾收集
优化内存占用
使用具备垃圾收集机制的javascript的主要问题在于:分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少,目的是防止运行javascript的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量
因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式是:为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用,这种做法叫解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时自动被解除引用
不过,要注意的是,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收