likes
comments
collection
share

聊聊内存管理机制,get优化小技巧

作者站长头像
站长
· 阅读数 75

1 前言

在前端开发工作中,相信大家都听说过或者遇到过内存溢出这个问题,对于这个问题,从字面意思就可以知道,应该是程序的运行消耗了太多的内存从而导致内存不够溢出了。那么怎么避免或杜绝这种问题呢?为了更好的解决这个问题,提升代码的性能,接下来将带大家了解下JavaScript的内存管理机制,并基于内存管理机制,介绍些小技巧作为参考。

2 内存介绍

首先,我们先了解下JavaScript中的内存及分配,JavaScript的内存管理和其它语言还是有所区别的。比如像C语言这样的底层语言,一般都会提供内存管理的接口,如malloc()(分配内存方法)和free()(释放内存方法)。但是在JavaScript中,内存的分配和释放都是自动的,在创建变量(对象、字符串等)时会自动分配内存,并在不使用它们时自动释放,释放过程被称为垃圾回收。这种自动的管理方式可能会让很多开发者认为在开发过程中并不需要关心内存管理。

出于安全考虑,一般情况下分配给浏览器的内存会比较小,避免运行大量的网页耗尽内存而导致系统崩溃,将内存占用保持一个较小的值可以提高页面的性能,一般情况下,优化内存占用的最佳手段就是保证在代码执行的时候只保存需要的数据,及时释放不需要的数据。

2.1 内存生命周期

无论是哪种程序语言,内存的生命周期基本是一致的,一般都需要经历以下几个过程。

1、分配需要的内存

当我们在声明变量、初始化变量或者函数的调用,JavaScript的宿主环境就会自动的分配内存。

聊聊内存管理机制,get优化小技巧

2、使用分配的内存

使用内存其本质上就是内存的读写操作,比如对声明的变量进行运算、输出等操作。

聊聊内存管理机制,get优化小技巧

3、释放内存

当内存使用完毕后,由垃圾回收机制通过某种回收算法判断是否需要回收,从而回收掉不在需要使用的内存,释放内存。

聊聊内存管理机制,get优化小技巧

2.2 内存类型

为了更好的了解栈和堆,我们先了解下JavaScript中的数据类型,主要可以分为两类数据类型,基本类型和引用类型。

基本类型:Number、String、Boolean、Null、Undefined、Symbol,它们都是按值直接存放的,一般存放在栈内存中,可以直接访问。

引用类型:Function、Array、Object,这几种数据类型的实际数据一般存放在堆内存中,栈中一般会存放它们的地址指针,当我们需要访问数据时,先要从栈中获取到地址指针,然后通过地址指针从堆中找出对应的数据。

聊聊内存管理机制,get优化小技巧

1、栈内存

栈内存是一种有序的连续型结构,先进后出,主要存储基本类型数据或者引用类型数据的指针。给存储的变量分配的内存是固定的。

聊聊内存管理机制,get优化小技巧

2、堆内存

堆内存的存储结构不同于栈,虽然本质上都只是内存中的一片空间,但是堆是非连续型的树形结构,没有存储顺序及规律,只会用一块足够大的空间来存储变量,主要存储引用类型数据。

3 垃圾回收

JavaScript的垃圾回收由执行环境负责处理,基本思路其实很简单,就是确定哪个变量不再使用了,就释放它占用的内存。这个过程一般是周期性的,也就是每隔一段时间就会自动运行。那么怎么确定一个变量是否需要使用呢?

为了判断变量是否还需要使用,解决回收的问题,在浏览器的发展史上,主要采用标记的策略来实现,目前主要运用的两种方式为标记清理和引用计数。

3.1 回收算法

1、标记清理

JavaScript最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。 当变量离开上下文时,也会被加上离开上下文的标记。 给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。 垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。 到了2008年,IE、Firefox、Opera、Chrome和Safari都在自己的JavaScript 实现中采用标记清理(或其变体),只是在运行垃圾回收的频率上有所差异。

优点:可以处理循环引用的问题。

缺点:需要遍历所有对象寻找标记,效率低,清理出来的内存空间是不连续的,还需要进一步的维护整理。

2、引用计数

另一种没那么常用的垃圾回收策略是引用计数(reference counting)。 其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。类似地,如果保存对该值引用的变量被其他值给覆盖了, 那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。

优点:及时清理内存,减少程序暂停。

缺点:未解决循环引用问题。所谓循环引用,就是对象A有一个指针指向对象B,而 对象B也引用了对象A。

聊聊内存管理机制,get优化小技巧

3.2 V8内存回收分析

相信大家对V8都有或多或少的了解,V8是JavaScript引擎,是用来执行JavaScript的,可以编译成原生机器码,相比与其它的JavaScript引擎具有更高的性能。

简单了解V8后,接下来进入主题,了解下V8的内存,一般64位中内存为1.4G,32位中内存位0.7G,可能不同电脑浏览器会有所差异。在V8引擎中,垃圾回收是进行分代回收的,基于此,可分为新生代和老生代两种内存区域。

新生代:存活短的变量放在新生代,由from区域和to区域组成(如64位中新生代大小为32M,from和to各占16M)。

老生代:存货长的放在老生代(64位1.4G 32位0.7G)

新生代回收:

1、检查from区域中的存活对象,如果存活则拷贝到to区域,所有存活对象拷贝完后,释放from区域

2、from区域和to区域互换

3、以下情况对象将可能被移动到老生代

对象经过多次(一般5次以上)垃圾回收还存活

to的空间使用占比超过25%,或者是超大对象

聊聊内存管理机制,get优化小技巧

老生代回收:

1、标记清理:采用标记清理算法对内存进行回收清理

2、内存整理:由于清理后内存会出现不连续的情况,碎片内存不好使用,这是需要整理内存,将活着的对象左移。

聊聊内存管理机制,get优化小技巧

4 优化回收

1、 null

赋值null,可以解除引用,加快内存回收,解除对一个值的引用并不会自动导致相关内存被回收。解 除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃 圾回收时会被回收。

2、let和const声明改进垃圾回收过程

let、const都以块为作用域,相比于var,只用这两个关键字声明变量可以更早的让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,就有可能体现。

3、隐藏类和删除操作(Chrome)

Chrome的V8 JavaScript引擎在解释javaScript代码编译成实际的机器码时会利用隐藏类,将创建的对象与隐藏类关联起来,以跟踪他们的属性特征,能够共享相同隐藏类的对象性能会更好。

动态添加属性和删除(delete方法)属性会导致共用隐藏类失败,因此在构造函数中一次性声明所有属性,删除时采用赋值null替代,声明的该构造函数的实例将共用一个隐藏类,有助于提高性能

4、内存泄漏

变量的不合理使用,导致不能够被回收,比如全局变量,闭包的不合理使用。

5、静态分配与对象池

减少不必要的垃圾回收可以提高性能。浏览器决定频繁运行垃圾回收程序的一个标准就是对象更替的速度,比如下面的例子:

聊聊内存管理机制,get优化小技巧

调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值。假如这个矢量加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排垃圾回收。采用对象池以及静态分配内存的方式可以避免这种频繁的操作。比如将上面的例子做如下改造:

聊聊内存管理机制,get优化小技巧

转载自:https://juejin.cn/post/7242997304021205050
评论
请登录