一看就懂的JS内存管理及垃圾回收机制介绍及解析

前言

最近在翻译外刊的时候,感觉很多好的文章翻译的时候不够地道,想为文章补充内容又担心准确度降低。莫得办法,最终我选择了以自己的大白话来总结一些翻译时的所学所想。

这篇文章将会根据我最近翻译的几篇node.js相关的垃圾回收机制,循序渐进 的进行一个JS相关的内存管理机制和垃圾回收的运行原理及介绍,文章没有特别深入,适合点杯奶茶☕搭配目录食用~

内存管理简介

众所周知,咱们开发的程序在跑的使用都是需要用到内存来存储一些数据的,但是要是 随便存,乱存,瞎xx存 那肯定存的少,因此在存的时候就会选择一些合适的数据结构,例如栈呀,堆呀,这里不展开讨论。

当然,存的多还不够,还得管的好。 那么就需要做内存管理 ,内存管理的目的是高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理功能提供了在程序请求时为 程序动态分配内存块 的方法,并在不再需要时 释放掉——这样内存就可以被重用。

JS中的内存管理

对于前端开发者而言,咱们的命根子是 js ,因此我们可能对内存管理这方面的认知弱于其他语言的开发者。咱们在日常开发的时候不会可以的去做这方面的考虑,这是为什么呢?

在应用程序级的内存管理中有两种方式,一种是手动的,一种是自动的。

以下是MDN的介绍:

像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()free()。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。

C语言的手动管理内存分配如下例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

  char name[20];
  char *description;

  strcpy(name, "RisingStack");

  // memory allocation
  description = malloc( 30 * sizeof(char) );
   
  if( description == NULL ) {
     fprintf(stderr, "Error - unable to allocate required memory\n");
  } else {
     strcpy( description, "Trace by RisingStack is an APM.");
  }
  
  printf("Company name = %s\n", name );
  printf("Description: %s\n", description );

  // release memory
  free(description);
}

手动内存管理中,开发者需要自己去控制内存的分配与释放,如果你没有管理好内存就容易导致以下几种情况:

  • 当使用的内存空间从未被释放时,导致 内存泄漏
  • 删除对象时会出现 野/空指针,但该指针被重用。当其他数据结构被覆盖或敏感信息被读取时,可能会导致严重的安全问题。

科普一下,空指针是 指向任何的内存地址的指针,野指针是 指向一个非法的或已销毁的内存 的指针,幸运的是,js是一门自动内存管理的语言,无需咱们手动管理内存分配。

那么我们主要了解的就是自动内存管理,它到底是如何管理的呢?

自动内存管理机制

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还

在JS中,我们在定义变量时会自动完成内存的分配,这一点应该很好理解,毕竟变量时咱们亲自创建的。但是光是创建可不行呀,咱们内存有限,有些变量要是没用了就得给他扬喽(销毁)。那js是如何销毁变量的呢?这里就涉及到了一个知识点——垃圾回收机制

垃圾回收机制

垃圾回收器简介

垃圾回收机制顾名思义就是将内存中没用的垃圾变量给回收了,但是关键点在于:怎么样的变量才是没用的变量捏?

高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

JS中使用了垃圾回收器来进行回收变量,垃圾回收器的主要工作原理就是通过判断一个对象是否被引用,如果一个对象已经没有被任何其他对象引用(对象有没有其他对象引用到它),那么它就是一个可以回收的变量。

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

以下是MDN的一个代码示例:

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属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

变量引用图示

用译文中的一些图片展示一下可能会更加清晰:

下图显示了如果你有相互引用的对象和一些没有任何引用的对象的情况。

垃圾回收器运行后,无法访问的对象将被删除,并释放内存空间。

垃圾收集器的好处

使用垃圾收集器进行变量回收有什么好处呢?

  • 可以防止 野/空指针 错误
  • 不会尝试释放已经释放的空间
  • 减少内存泄漏风险

垃圾收集器的弊端

当然,使用垃圾回收器并不能解决所有问题,也不是内存管理的银弹。我要了解垃圾回收器的弊端才能更好的利用它。

  • 性能影响 ——为了决定可以释放什么,GC会消耗计算能力
  • 不可预知的停顿 ——现代 GC 实现试图避免“stop-the-world”(暂停所有当前运行的线程)收集
  • 开发者不关心——由于垃圾会自动回收,在开发的过程中开发者可能不重视,导致内存泄漏

这里解释一下 stop-the-world , stop-the-world就是在垃圾回收的过程中整个应用程序线程都会被暂停,没有任何响应,如果时间长就像卡住一样。

总结一下主要的弊端就是会消耗性能进行垃圾回收的处理,底层为我们做了一些事情,那必然是要付出一定的代价滴~

js中变量的存储方式

大家应该知道在JS中有 基本数据类型,例如 number,string... 。还有一种 引用数据类型,例如array,object...

其中 基本数据类型的大小是固定的,因此直接存放在栈中。而 引用数据类型 的大小是会变化的,它的值存放在堆中,而在栈中存放它的索引地址。

新空间与旧空间

堆内存主要分为 新空间旧空间 两个部分。顾名思义,新空间是用于分配新变量的地方;在这里收集垃圾很快,大小约为 1-8MB。生活在新空间中的对象称为年轻一代(Young Generation)

在新空间的收集器中幸存下来的对象被提升到旧空间——它们被称为 老一代(Old Generation)。旧空间中的分配速度很快,但垃圾回收的成本很高。

Scavenge 和 Mark-Sweep

通常来说,只有约 20% 的年轻一代存活到老一代。旧空间的收集只有在用完后才会开始。为此,V8 引擎使用两种不同的收集算法。

为什么要分为两个部分呢,主要是为了提升垃圾变量的回收效率,在两个部分上应用不同的收集算法。

  • Scavenge(复制算法) 收集,速度快,在年轻一代上运行,
  • Mark-Sweep(标记-清除算法) 收集,速度较慢,在老一代运行。

新生代的变量由于一般生命周期比较端,而且数量相较于老一代多很多,所以使用了 复制算法 进行变量回收,这个算法的特点是速度很快,但是消耗的内存比较大。一般能活到老一代都是比较常驻的变量,因此使用的 标记-清除算法 特点是速度较慢,但是消耗的内存也对应小了很多。

虽说JS在执行时会自动的进行变量的销毁,但是不可避免的会有该销毁的变量没有销毁,导致越积越多的情况,导致系统崩溃,这种情况就叫内存泄漏。

内存泄漏

内存泄漏内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏原因

根据上面的介绍,JS已经会为我们自动的回收销毁变量了,那为啥还会导致内存泄漏嘞?以下是几个常见的内存泄漏原因

  • 定时器:定时器没有销毁的情况下,不断生成新变量或增大某个变量。

  • 全局变量:大部分时候我们可能是需要使用全局变量才主动定义,但是如果你无意中定义了一个全局变量,那么它将不会被回收,而会一直存留。

  • 闭包:一般来说,我们使用闭包就是在利用它不会被垃圾回收清理的特性进行一些操作,但是如果无意中定义了闭包可能就会导致内存泄漏。

  • 循环引用:由于垃圾收集器是检测对象有无被其他对象引用再进行删除,如果两个对象相互引用,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收

那为什么以上的行为都会产生内存泄漏,而很多前端开发者在开发的时候并没有关注这些点,写出来的代码在浏览器上跑的时候也没啥大问题呢?这主要是因为前端开发者开发的web网站一般使用场景都是 非长时间运行 的浏览器上,即使有一部分的内存泄漏,当关闭网站后也会释放,不会造成很严重的后果。

但是在 Node.js 出现后,js可以跑在服务端上面了,服务端上的应用可不像浏览器上的网页看一会就关了,要保障可以长时间的稳定运行才行,此时如果出现长时间持续内存泄漏的情况,用不了多久就很可能内存爆满,导致整个后端服务不可用。当然,一般情况下在部署服务时就应该设置好内存上限了。

当然优秀的开发者在什么运行环境都会注意防止内存泄漏的情况,那么我们如何判断自己的程序是否有内存泄漏的情况嘞?

内存泄漏调试

任务管理器

内存泄漏的调试的话,我们可以使用任务管理器做一个粗略的调试,在任务管理器中,我们可以通过点击浏览器应用查看各个网页的内存使用率,如果你的网站内存使用率持续上升,那么你就该检查一下你的代码了。

image.png

浏览器开发者工具

在谷歌浏览器中,我们可以使用开发者工具来进行内存使用情况查看,f12打开开发者工具,选择 memory 菜单。

如下图:

image.png

在左上角有一个垃圾桶按钮,点击就是主动执行一次 变量回收 工作。

image.png

点击 Take snapshot 按钮就是执行一个快照,记录当前网站的内存使用情况

image.png

每次执行快照后点击一下回收变量,再模拟一些用户操作后再执行一次快照,多次快照对比查看详细数据变化趋势来判断是否发生内存泄漏

代码调试

在Node.js中,我们还可以通过一些内置的api进行内存的打印,例如 process.memoryUsage(),通过 定时输出内存使用情况 的方式判断是否发生内存泄漏。

当然你也可以使用现成的包来帮助你判断是否发生内存泄漏,例如node-memwatch 它在检测到运行代码中的内存泄漏时触发一个事件,使用示例如下:

const memwatch = require("memwatch");

memwatch.on("leak", function (info) {
  // event emitted
  console.log(info.reason);
});

总结

这篇文章中,我们简单介绍了内存管理和垃圾回收的一些相关知识,并引出了内存泄漏这个问题的产生原因,最后讲了一下如何去调试它。如果文章对你有帮助的话欢迎收藏,最好再点个赞(因为收藏我看不到😢)。欢迎来评论区搞点硬核评论砸场子🐱‍👤

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值