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'));


目录
相关文章
|
17天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
17天前
|
JavaScript 前端开发 Java
避免 JavaScript 中的内存泄漏
【10月更文挑战第30天】避免JavaScript中的内存泄漏问题需要开发者对变量引用、事件监听器管理、DOM元素操作以及异步操作等方面有深入的理解和注意。通过遵循良好的编程实践和及时清理不再使用的资源,可以有效地减少内存泄漏的风险,提高JavaScript应用程序的性能和稳定性。
|
17天前
|
JavaScript 前端开发 算法
JS垃圾回收
【10月更文挑战第30天】JavaScript 的垃圾回收机制是保证程序稳定运行的重要组成部分。了解垃圾回收的原理和算法,以及注意避免内存泄漏的问题,可以帮助开发者更好地利用 JavaScript 进行高效的开发
|
17天前
|
存储 JavaScript 前端开发
JavaScript的垃圾回收机制
【10月更文挑战第29天】JavaScript的垃圾回收机制是确保程序高效运行的重要保障,了解其工作原理和相关注意事项,有助于开发者更好地编写高性能、稳定的JavaScript代码。
|
13天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
35 6
|
22天前
|
监控 JavaScript 前端开发
如何检测和解决 JavaScript 中内存泄漏问题
【10月更文挑战第25天】解决内存泄漏问题需要对代码有深入的理解和细致的排查。同时,不断优化和改进代码的结构和逻辑也是预防内存泄漏的重要措施。
35 6
|
22天前
|
JavaScript 前端开发 Java
JavaScript 中内存泄漏的几种常见情况
【10月更文挑战第25天】实际上还有许多其他的情况可能导致内存泄漏。为了避免内存泄漏,我们需要在开发过程中注意及时清理不再需要的资源,合理使用内存,并且定期检查内存使用情况,以确保程序的性能和稳定性
29 2
|
26天前
|
存储 JavaScript 前端开发
JavaScript垃圾回收机制与优化
【10月更文挑战第21】JavaScript垃圾回收机制与优化
25 5
|
25天前
|
存储 JavaScript 前端开发
js 中有哪几种内存泄露的情况
JavaScript 中常见的内存泄漏情况包括:1) 全局变量未被释放;2) 意外的全局变量引用;3) 被遗忘的计时器或回调函数;4) 事件监听器未被移除;5) 子元素存在时删除父元素;6) 循环引用。
|
6月前
|
前端开发 JavaScript 算法
JavaScript 内存管理的秘密武器:垃圾回收(下)
JavaScript 内存管理的秘密武器:垃圾回收(下)
JavaScript 内存管理的秘密武器:垃圾回收(下)