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


目录
相关文章
|
2天前
|
监控 算法 Java
Python内存管理与垃圾回收机制
【5月更文挑战第12天】了解Python内存管理与垃圾回收对编写高效稳定程序至关重要。Python自动管理内存,使用`malloc()`和`free()`分配和释放。引用计数跟踪对象引用,当引用计数为零时对象销毁。垃圾回收处理循环引用,采用分代回收算法。优化技巧包括避免循环引用、显式释放对象、使用生成器和迭代器。理解这些机制有助于避免内存泄漏,提高性能。通过示例代码,学习如何在实践中应用内存管理最佳实践和高级优化技巧,以及如何调试和诊断内存问题。在并发和异步编程中,需注意线程安全和异步内存管理。掌握这些知识能提升Python编程的效率和质量。
10 3
|
3天前
|
JavaScript 前端开发 算法
JavaScript的垃圾回收机制通过标记-清除算法自动管理内存
【5月更文挑战第11天】JavaScript的垃圾回收机制通过标记-清除算法自动管理内存,免除开发者处理内存泄漏问题。它从根对象开始遍历,标记活动对象,未标记的对象被视为垃圾并释放内存。优化技术包括分代收集和增量收集,以提升性能。然而,开发者仍需谨慎处理全局变量、闭包、定时器和DOM引用,防止内存泄漏,保证程序稳定性和性能。
9 0
|
3天前
|
JavaScript 前端开发 算法
JavaScript 的垃圾回收机制有一些潜在的缺点
【5月更文挑战第11天】JavaScript 的垃圾回收机制虽自动化管理内存,降低泄漏风险,但也存在性能开销、无法精确控制内存释放、全局变量和闭包可能导致内存泄漏、弱引用及循环引用问题。开发者需注意优化代码,避免这些问题,以充分利用垃圾回收机制并提升应用性能。
6 0
|
3天前
|
JavaScript 前端开发 算法
了解JavaScript的垃圾回收机制
JavaScript的垃圾回收机制自动回收不再使用的内存,主要通过标记清除和引用计数算法。它从根对象开始遍历引用链,标记活跃对象并清理未标记的。引用计数算法在对象引用为零时回收,但循环引用会导致问题。现代垃圾回收器能处理循环引用,防止内存泄漏。示例代码展示了当大数组不再被引用时,垃圾回收机制自动清理内存。
|
5天前
|
存储 算法 Java
了解Java内存管理与垃圾回收机制
了解Java内存管理与垃圾回收机制
7 0
|
12天前
3.默认值不一样【重点】 局部变量:没有默认值,如果要想使用,必须手动进行赋值 成员变量:如果没有赋值,会有默认值,规则和数组一样 4.内存的位置不一样(了解) 局部变量:位于栈内存 成员变量:位于堆内存 5生命周期不一样(了解)
3.默认值不一样【重点】 局部变量:没有默认值,如果要想使用,必须手动进行赋值 成员变量:如果没有赋值,会有默认值,规则和数组一样 4.内存的位置不一样(了解) 局部变量:位于栈内存 成员变量:位于堆内存 5生命周期不一样(了解)
17 0
|
13天前
|
JavaScript 前端开发 算法
【JavaScript技术专栏】深入理解JavaScript垃圾回收机制
【4月更文挑战第30天】本文深入解析JavaScript的垃圾回收机制,旨在帮助开发者理解其工作原理。内容涵盖垃圾回收的概念、标记阶段、清除阶段,以及优化策略如增量回收、分代回收和并行回收。此外,还介绍了引用计数、标记-清除等常见垃圾回收算法,并讨论了内存泄漏的原因及解决方法,强调理解垃圾回收对编写高效代码的重要性。
|
18天前
|
Linux
Linux rsyslog占用内存CPU过高解决办法
该文档描述了`rsyslog`占用内存过高的问题及其解决方案。
41 4
|
1月前
|
移动开发 运维 监控
掌握Linux运维利器:查看CPU和内存占用,轻松解决性能问题!
掌握Linux运维利器:查看CPU和内存占用,轻松解决性能问题!
|
1月前
|
监控 Python
【python】实现cpu/内存监控的功能(非常简单)
【python】实现cpu/内存监控的功能(非常简单)