JS内存管理生命周期和垃圾回收机制

简介: JS内存管理生命周期和垃圾回收机制

JS内存管理生命周期和垃圾回收机制


最近在看大神写的专栏,很精辟,笔者想通过总结的方式加深理解,不一定准确,只是笔者自己的想法,欢迎指正。

TL;DR

  • JS 的内存管理生命周期:分配、读写、释放
  • 垃圾回收机制的算法:引用计数法、标记清除法
  • 内存泄漏成因:闭包引起的共享父作用域、变量没声明、未清除定时器、删除不要的dom

JS 内存生命周期

内存管理又是是每一种编程语言都会具备的一种基本能力。

但有些语言暴露内存管理的方法,如 c,有些语言不暴露,如JS

所以,不暴露,就不容易看见和知道了。。。

JS 内存生命周期,三个:

  • “挖坑”—— 在内存空间的沃土里,划出自己的地,此举称为 “分配内存”。
  • “用坑”—— 往地里 “种菜” :填入你需要存储的信息。此后你可以读取它,也可以更改它,此举称为 “内存的读与写” 操作。
  • “还坑”—— 用坑一时爽,但作为好公民,咱用完这个地就得及时上交给村里。这个 “还回去” 的动作,就叫做内存的释放。

但挖坑的时候,得看你种啥,从而选择不同的土壤。

1.分配内存

内存分为两种:

  • 栈内存:线性表结构。适合存 基本类型:Sting、Number、Boolean、null、undefined、Symbol
  • 堆内存:树结构。适合存 引用类型:Object Array 等...

2.读写内存

栈内存和堆内存的结构不一样,所以读写的方式也不一样。

先看看以下是放在哪种内存里。

let a = 0;
let b = "Hello World";
let c = null;
let d = { name: "修言" };
let e = ["修言", "小明", "bear"];

网络异常,图片无法展示
|

在访问 a、b、c 三个变量时,过程非常简单:从栈中直接获取该变量的值。

而在访问 d 和 e 时,则需要分两步走:

  1. 从栈中获取变量对应对象的引用(即它在堆内存中的地址)
  2. 拿着 1 中获取到的地址,再去堆内存空间查询,才能拿到我们想要的数据

3.释放内存

JS 没有管理内存的方法,所以 JS 隔一段时间就巡查一次。

判断一个变量不再被需要之后,就会把个变量所占用的内存空间给释放掉。

垃圾回收: 巡查 => 判断 => 释放 的过程

垃圾回收的算法:怎么判断变量不再被需要

两种判断的法子:

  • 引用计数法(已被淘汰)
  • 标记清除法(正使用的)

引用计数法

“引用” 这个概念,其实可以认为它描述的是变量所处那块内存的内存地址。

用一个变量指向了一个值,那么就创建了一个针对这个值的 “引用”:

// 内存中数组这个值有一个引用,就是student这个变量
let students = ["修言", "小明", "bear"];

在引用计数法的机制下,内存中的每一个值都会对应一个引用计数。当垃圾收集器感知到某个值的引用计数 为 0 时,就判断它 “没用” 了,随即这块内存就会被释放。

// 如果将students改成null,则上面数组的引用数就变成0了,就会被回收
students = null;

引用计数法的缺陷

看个例子:

function badCycle() {
  var cycleObj1 = {};
  var cycleObj2 = {};
  cycleObj1.target = cycleObj2;
  cycleObj2.target = cycleObj1;
}
badCycle();

一般函数执行完,内部变量自动会被清除。 但是用引用计数法的话,cycleObj1 和 cycleObj2循环引用,其引用计数一直是 1,就不能被回收。

所以,引用计数法的缺陷是:循环引用的变量,很容易不能被回收。

标记清除法

标记清除法分为两个阶段:

  • 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里,根对象是 Global。从 根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为 “可抵达”。
  • 清除阶段: 没有被标记为 “可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除

“可抵达”:意味着可以被使用

重新看循环引用的代码:

function badCycle() {
  var cycleObj1 = {};
  var cycleObj2 = {};
  cycleObj1.target = cycleObj2;
  cycleObj2.target = cycleObj1;
}
badCycle();

badCycle 执行完毕后,从根对象 Window 出发,cycleObj1 和 cycleObj2 都会被识别为不可达的对象(不可被使用的对象),它们会按照 预期被清除掉。这样一来,循环引用的问题,就被标记清除干脆地解决掉了。

闭包和内存泄露

啥是内存泄露?

该释放的变量(内存垃圾)没有被释放,仍然霸占着原有的内存不松手,导致内存占用不断攀高,带来性能恶化、 系统崩溃等一系列问题,这种现象就叫内存泄漏。

先说一句,单纯由闭包导致的内存泄漏,极少极少(除非你的代码写得有问题)。

先看一段经典的闭包造成内存泄露的代码(写的有问题哈,改进就不会了)。

var theThing = null;
var replaceThing = function() {
  var originalThing = theThing;
  var unused = function() {
    if (originalThing)
      // 'originalThing'的引用
      console.log("嘿嘿嘿");
  };
  theThing = {
    longStr: new Array(1000000).join("*"),
    someMethod: function() {
      console.log("哈哈哈");
    }
  };
};
setInterval(replaceThing, 1000);

在 V8 中,一旦不同的作用域位于同一个 父级作用域下,那么它们会共享这个父级作用域。

在这段代码里, unused 是一个不会被使用的闭包,但和它共享同一个父级作用域的 someMethod,则是一个 “可抵达”的闭包。

unused 引用了 originalThing,这导致和它共享作用域的 someMethod 也 间接地引用了 originalThing。

结果就是 someMethod “被迫” 产生了对 originalThing 的持续引用,originalThing 虽然 没有任何意义和作用,却永远不会被回收。

不仅如此,originalThing 每次 setInterval 都会改变一次指向(指向最近 一次的 theThing 赋值结果),这导致无法被回收的无用 originalThing 越堆积越多,最终导致严重的内存泄漏。

内存泄露的原因

  • 被迫产生对父作用域变量的引用。上面的例子。避免方案:将某个闭包多裹一层,从而不共享父作用域
  • 在函数里,没有声明而直接赋值的变量。避免方案:变量必须声明

本身想在函数内部用,用完就回收,但是因为变成了全局变量,则不会被回收

function test() {
  me = "yan";
}
  • 忘记清除的 setInterval 和 setTimeout。避免方案:必须清定时器

在 轮询调用setInterval 和链式调用的 setTimeout 这两种场景下,定时器的工作可以说都是无穷无尽的。 当定时器囊括的函数逻辑不再被需要、而我们又忘记手动清除定时器时,它们就会永远保持对内存的占用。

  • 清除不当的 DOM。避免方案:手动置为null。

虽然删除了节点,但myDiv 这个变量对 这个 DOM 的引用仍然存在,它仍然是一块 “可抵达” 的内存。

const myDiv = document.getElementById('myDiv')
function handleMyDiv() {
// 一些与myDiv相关的逻辑
}
// 使用myDiv
handleMyDiv()
// 尝试”删除“ myDiv
document.body.removeChild(document.getElementById('myDiv'));


目录
相关文章
|
24天前
|
监控 Java
压力测试Jmeter的简单使用,性能监控-堆内存与垃圾回收 -jvisualvm的使用
这篇文章介绍了如何使用JMeter进行压力测试,包括测试前的配置、测试执行和结果查看。同时,还探讨了性能监控工具jconsole和jvisualvm的使用,特别是jvisualvm,它可以监控内存泄露、跟踪垃圾回收、执行时内存和CPU分析以及线程分析等,文章还提供了使用这些工具的详细步骤和说明。
压力测试Jmeter的简单使用,性能监控-堆内存与垃圾回收 -jvisualvm的使用
|
8天前
|
JavaScript 前端开发 Java
JavaScript基础知识-垃圾回收
关于JavaScript垃圾回收基础知识的介绍。
20 1
JavaScript基础知识-垃圾回收
|
12天前
|
Web App开发 存储 监控
Node.js中的内存泄漏
【8月更文挑战第31天】Node.js中的内存泄漏
30 1
|
14天前
|
存储 监控 算法
内存泄漏还是高性能?深度揭秘.NET垃圾回收机制
【8月更文挑战第28天】垃圾回收是.NET框架中自动化内存管理的关键机制,它通过分代收集算法自动清理不再使用的对象,简化了开发者的内存管理工作。本文深入解析了垃圾回收器的工作原理、对象内存分配策略及优化技巧,并介绍了多种监控工具,帮助提升.NET应用性能与稳定性。掌握这些知识将使开发者能够更高效地管理内存,提高应用程序的运行效率。
24 3
|
19天前
|
JavaScript 前端开发 算法
js 内存回收机制
【8月更文挑战第23天】js 内存回收机制
30 3
|
19天前
|
存储 JavaScript 前端开发
学习JavaScript 内存机制
【8月更文挑战第23天】学习JavaScript 内存机制
18 3
|
19天前
|
JavaScript 前端开发 Java
JavaScript内存泄露大揭秘!你的应用为何频频“爆内存”?点击解锁救星秘籍!
【8月更文挑战第23天】在Web前端开发中,JavaScript是构建动态网页的关键技术。然而,随着应用复杂度增加,内存管理变得至关重要。本文探讨了JavaScript中常见的内存泄露原因,包括意外的全局变量、不当使用的闭包、未清除的定时器、未清理的DOM元素引用及第三方库引发的内存泄露。通过了解这些问题并采取相应措施,开发者可以有效避免内存泄露,提高应用性能。
28 1
|
19天前
|
前端开发 JavaScript Java
揭开 JavaScript 垃圾回收的秘密——一场与内存泄漏的生死较量,让你的代码从此焕然一新!
【8月更文挑战第23天】本文通过多个实例深入探讨了JavaScript中的垃圾回收机制及其对应用性能的影响。首先介绍了基本的内存管理方式,随后分析了变量不再使用时的回收过程。接着,通过事件监听器未被移除及全局变量管理不当等场景展示了常见的内存泄漏问题。最后,文章介绍了使用`WeakRef`和`FinalizationRegistry`等现代API来有效避免内存泄漏的方法。理解并运用这些技术能显著提升Web应用的稳定性和效率。
39 0
|
22天前
|
JavaScript 前端开发 Java
|
11天前
|
JavaScript 前端开发