js的内存管理和垃圾回收

419 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

认识内存管理

不管什么样的编程语言,在代码的执行过程中都会需要为其分配内存。不同的是有些编程语言内存分配是需要手动分配的,有些编程语言是自动分配的。内存的管理主要分为以下三个部分:

  1. 申请分配内存(申请)
  2. 使用分配到的内存(释放)
  3. 回收无用的内存(回收)

对于第一步和第三步内存的分配和回收,一些编程语言是需要手动申请和回收的,比如c语言的malloc函数和free函数。还有一些函数是自动分配和回收的,如javascript就是自动分配和回收的。

js的内存管理

javascript会在定义变量时为我们分配内存。但是对于基本类型和引用类型分配方式是不同的。对于js的基本类型(null,undefined,boolean,string,number),是在栈上为其分配空间的,对于引用类型(object)是在堆上为其进行空间分配的,同时会在栈上存在有该对象的引用,比如定义了·以下对象,

let obj={
    name:'echo',
    age:18
}

其内存表示为下 image.png

js的垃圾回收

垃圾回收:Garbage Collect。简称js。

自动回收

可以把垃圾回收分为两类,栈中的垃圾回收和堆中的垃圾回收。

栈中的垃圾回收

比如函数调用结束之后需要将该函数的执行上下文从调用栈中弹出。一起来看一下这个过程吧。 比如下述代码:

function foo(){
    //代码
}
foo()

全局环境下调用了foo函数,其在调用栈中的存储状态如下:

image.png

其中有一个栈顶指针ESP指向栈顶元素。当foo函数调用结束之后,ESP指针会向下移动,在该场景下,ESP向下移动指向了全局执行上下文,foo函数执行上下文仍存在于栈中,但已经是一块无效内存了,当调用了新的函数新函数的执行上下文会压入栈中覆盖foo函数的执行上下文。

堆中的垃圾回收

堆中分为了新生代和老生代两个区域,新生代用来存放一些比较"新"的对象,老生代用来存储一些比较"老"的对象。为什么要分为新生代和老生代两个区域呢,这是因为js中的很多对象存活周期很短,一经分配后很快就会变得不可访问。而有一种说法是“不死的对象活得更久”,可以把这些对象存放在老生代中。而对于新生代和老生代中的垃圾回收机制是不同的,当新生代中的对象经过了三次垃圾回收仍然存活就会将这个对象放在老生代中。

新生代中的垃圾回收

新生代中分为了对象区域和空闲区域。新定义的变量会被放到对象区域,当执行垃圾回收时,会对对象区域中的对象进行遍历,若该对象仍需使用,则将其复制到空闲区域。遍历结束后就会将对象区域清空,然后将对象区域和空闲区域的角色翻转,即原本的对象区域变为了空闲区域,原本的空闲区域变为了对象区域。若一个对象经历了三次垃圾回收仍存在于新生代中,就会将其放在老生代中。

老生代中的垃圾回收

垃圾回收器会从栈的根元素进行遍历,来确定老生代中的对象是否是仍在使用的(这是因为堆中仍在使用的对象一定是被其他变量引用着的,如果遍历栈的时候可以到达该对象就说明这个对象仍在使用,若该对象在栈中无对应的引用就可以将该对象回收掉了)。垃圾回收器会将所有仍在使用的对象移动到老生代的同一端,然后将边界外的对象都清除掉。

手动回收

js红宝书“事实上,在有的浏览器中可以触发垃圾收集过程,但我们不建议读者这样做。在IE中,调用window.CollectGarbage()方法会立即执行垃圾收集,在Opera7及更高版本中,调用window.opera.collect()也会启动垃圾收集例程”。 当我们不再使用某个对象时,可以将这个对象置为null。但是将一个对象置为null并非手动进行了一次垃圾回收过程,而是接触了其引用(引用计数法),接触引用可以让该对象脱离引用环境,方便垃圾回收器下次运行时将其回收。

垃圾回收方法
标记清除法

当一个变量进入执行环境就标记该对象进入了环境,如在一个函数中声明了该对象,当一个对象离开了环境就标记为其离开了环境,如函数调用结束执行上下文被销毁。标记为离开环境的变量会在垃圾回收的过程中被垃圾回收器回收。

引用计数法

一个对象被赋值给了一个变量则可以说这个对象被这个变量引用了,该对象的引用数+1,比如let obj={name:"echo",age:18};如果obj又被赋予了另外的值,则该对象的引用数-1,当该对象的引用数为0时则说明这个对象没有被任何变量引用,就可以在执行垃圾回收的时候将其回收掉了。