JavaScript怎么管理内存

103 阅读9分钟

为什么要管理内存

减少浏览器负担: 内存占用过大会让浏览器压力过大,导致浏览器卡顿

node端: 内存如果不够,服务就会中断,而nodejs开启的服务如果不管理内存,就会中断

内存如何存放数据

栈内存(stack) 主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,以及对象变量的指针,基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。 栈内存也是用来存储执行上下文的调用栈

堆内存(heap) :动态分配的内存,大小不定也不会自动释放,存放引用类型,但是堆内存存储变量时没有什么规律可言,堆内存存储的对象类型数据对于大小这方面,一般都是未知的,它只会用一块足够大的空间来存储变量

image.png

为什么数据不能全部放在栈内存中?

因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率;

通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

V8如何回收内存

V8有多大

64位下是1.4G

32位下700MB

但是根据浏览器不同,有些许扩容。node情况下会有一些C++内存扩容

栈内存如何回收

function foo() {
         var a = 1;
         var b = { name: "CR7" };
         function bar() {
              var c = "ball";
              var d = { name: "Messi" };
          }
         bar();
        }
  foo();

上面代码先编译,并创建执行上下文,然后再按照顺序执行代码

foo函数执行上下文中:保存a变量和c变量在堆内存中的引用地址,bar函数执行上下文中:保存c变量和d变量在堆内存中的引用地址,所以a、c保存在栈内存中,b、d保存在堆内存中

当函数执行完,函数执行上下文就按照后进先出(LIFO)原则从调用栈中弹出,函数执行上下文中保存的基本数类型,如a、c就等待被回收,同时指向堆内存中数据的引用也没有了,堆内存中的引用类型数据也会被堆内存回收

堆内存如何回收

在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象 对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收:

  • 副垃圾回收器,主要负责新生代的垃圾回收。
  • 主垃圾回收器,主要负责老生代的垃圾回收。

垃圾回收器的工作流程

V8 把堆分成两个区域——新生代和老生代,并分别使用两个不同的垃圾回收器。其实不论什么类型的垃圾回收器,它们都有一套共同的执行流程。

第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。

第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。

第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如副垃圾回收器回收新生代内存

新生代内存:存放短时间存活的新变量,内存量极小,64位下大概32MB

image.png

新生代回收算法:

把新生代空间对半划分为两个区域,一半是对象区域(就叫from吧),一半是空闲区域(就叫to吧)

如果a变量不用了,下次回收把b、c变量复制到to空间,然后清空from空间,然后对调from和to空间,由于复制需要时间成本,所以新生代内存不能太大,否则影响执行效率。典型的牺牲空间换时间

image.png

老生代内存:生存时间比较长的变量,几乎占据所有内存,64位下大概是1400MB

image.png

老生代回收分三步:标记已死变量,清除已死变量,整理磁盘

图中灰色代表已死变量,清除后,那两块灰色的内存不连续了,像数组需要连续的内存来存储, 所以需要整理磁盘,会花费比较多时间

新生代和老生代变量转化

新生代内存不大,容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

什么时候触发内存回收呢

执行完一次主线程代码

const a = "CR7"
const b = "Messi"
console.log(a)
setTimeout(()=>{
  a = "kaka"
  console.log(b)
  // 把回调从宏任务队列取出来,放到主线程执行完后,回收一次
},2000)
// 主线程执行完,回收一次

内存不够的时候,怎么回收变量

全局变量直到程序执行完毕,才会回收

const size = 30 * 1024 * 1024

function testMemory() {
    //node方法,获取目前使用的堆内存
    const memory = process.memoryUsage().heapUsed
    console.log(memory / 1024 / 1024 + 'MB');
}

const arr1 = new Array(size)
testMemory()
const arr2 = new Array(size)
testMemory()
const arr3 = new Array(size)
testMemory()
const arr4 = new Array(size)
testMemory()
const arr5 = new Array(size)
testMemory()
const arr6 = new Array(size)

用node执行上面代码,指定最大内存为1000,发现定义第五个变量内存不够了,但是程序没有执行完,又没有可以回收的变量,就报错了

image.png

普通变量,就是当他们失去引用

修改一下代码,定义局部变量

const size = 30 * 1024 * 1024

function testMemory() {
    //node方法,获取目前使用的堆内存
    const memory = process.memoryUsage().heapUsed
    console.log(memory / 1024 / 1024 + 'MB');
}

const arr1 = new Array(size)
testMemory();

(function () {
    const arr2 = new Array(size)
    testMemory()
    const arr3 = new Array(size)
    testMemory()
})()

const arr4 = new Array(size)
testMemory()
const arr5 = new Array(size)
testMemory()
const arr6 = new Array(size)

在定义arr5时,内存不够了,arr3和arr2没有外部引用,就被回收了,arr5和arr6可以继续定义

image.png

那么如何优化内存呢

尽量不要定义全局变量,定义了及时手动释放

注意闭包

            function foo() {
                var myName = "CR7";
                let test1 = 1;
                const test2 = 2;
                var innerBar = {
                    setName: function (newName) {
                        myName = newName;
                    },
                    getName: function () {
                        console.log(test1);
                        return myName;
                    },
                };
                return innerBar;
            }
            debugger;
            var bar = foo();
            bar.setName("Messi");
            bar.getName();
            console.log(bar.getName());

image.png

站在内存的角度来分析一下上面的代码:

当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文,

在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo - 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。

接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。

由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中

当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(foo)”对象,所以即使 foo 函数退出了,“clourse(foo)”依然被其内部的 getName 和 setName 方法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。

产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。

补充:node在内存管理中的特殊点

node可以手动触发垃圾回收:global.gc();

node端可以设置内存:node --max-old-space-size=1000 文件名 ,限制 V8 使用的内存量;

Node 提供的process.memoryUsage方法来查看内存: process.memoryUsage()返回一个对象

  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

补充:为什么V8要设计为1.4G

1.4G 对浏览器脚本已经够用

回收的时候是阻塞式的,垃圾回收需要时间,垃圾回收的时候会中断代码执行,所以为了减少阻断时间,设置为1.4G

参考:

栈空间和堆空间:数据是如何存储的

JavaScript 内存泄漏教程