JS内存管理——前端之JavaScript高级之三【Day36-Day42】

257 阅读10分钟

挑战坚持学习1024天——前端之JavaScript高级 认知和方向很重要,认知决定能获利多少,方向不对再怎么努力都白搭。

js基础部分可到我文章专栏去看 ---点击这里

Day36【2022年8月29日】

学习重点: JS 内存管理机制(上)

1.JS内存生命周期

不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存:

JS 的内存生命周期,和大多数程序语言一样,分为三个阶段:

第一步分配内存:分配申请你需要的内存(申请);

第二步内存读写:使用分配的内存(存放一些东西,比如对象等);

第三步内存释放:不需要使用时,对其进行释放;内存的释放。

不同的编程语言对于第一步和第三步会有不同的实现:

手动管理内存: 比如C、C++,包括早期的OC,都是需要手动来管理内存的申请和释放的(malloc和free函数);

自动管理内存: 比如Java、JavaScript、Python、Swift、Dart等,它们有自动帮助我们管理内存;

对于开发者来说,JavaScript 的内存管理是自动的、无形的。

我们创建的原始值、对象、函数……这一切都会占用内存;但是我们并不需要手动来对它们进行管理,JavaScript引擎会帮助我们处理好它;

2.栈内存与堆内存

JavaScript会在定义数据时为我们分配内存。 JS对于原始数据类型内存的分配会在执行时,直接在栈空间进行分配;JS对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用。

JS 中的数据类型,整体上来说只有两类:基本类型和引用类型。

其中基本类型包括:Sting、Number、Boolean、null、undefined、Symbol、Bigint。这类型的数据最明显的特征是大小固定、体积轻量、相对简单,它们被放在 JS 的栈内存里存储。

而排除掉基本类型,剩下的数据类型就是引用类型,比如 Object、Array、Function 等。这类数据比较复杂、占用空间较大、且大小不定,它们被放在 JS 的堆内存里存储。

2.1 对堆栈的理解

堆和栈分别是不同的数据结构。栈是线性表的一种,而堆则是树形结构。

实例

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

以上几种详细存储方式

a —— Number类型 —— 基本类型 —— 栈内存
b —— String类型 —— 基本类型 —— 栈内存
c —— null —— 基本类型 —— 栈内存
d —— Object —— 引用类型 —— 堆内存
e —— Array —— 引用类型 —— 堆内存

具体访问机制及其区别: 77250c2264058f48d293994929f99fb.png 从以上图可以看出:abc 直接从栈内存中取到值 而d和e具体如下:

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

Day37【2022年8月30日】

学习重点: JS 内存管理机制(下)

1.垃圾回收机制

因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数:

  • 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率;

  • 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露;

所以大部分现代的编程语言都是有自己的垃圾回收机制:

  • 垃圾回收的英文是Garbage Collection,简称GC;

  • 对于那些不再使用的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;

  • 而我们的语言运行环境,比如Java的运行环境JVM,JavaScript的运行环境js引擎都会内存 垃圾回收器;

  • 垃圾回收器我们也会简称为GC,所以在很多地方你看到GC其实指的是垃圾回收器;

但是这里又出现了另外一个很关键的问题:GC怎么知道哪些对象是不再使用的呢?

-这里就要用到GC的实现以及对应的算法的垃圾回收算法有两种 —— 引用计数法和标记清除法。

1.1引用计数法

当一个对象有一个引用指向它时,那么这个对象的引用就+1;当一个对象的引用为0时,这个对象就可以被销毁掉;这是最初级的垃圾回收算法,它在现代浏览器里几乎已经被淘汰。但是在面试中还会被问到。

这个算法有一个很大的弊端就是会产生循环引用;

1.1.1引用计数原理详解

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

const students = ['修言', '小明', 'bear']

如图,大家知道赋值表达式是从右向左读的。这行代码首先是开辟了一块内存,把右侧这个数组塞了进去,此时这个数组就占据了一块内存。随后 students 变量指向它,这就是创建了一个指向该数组的 “引用”。此时数组的引用计数就是 1(如下图)。

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

比如我们此时如果把 students 指向一个 null :

students = null

那么 [‘xiuyan’, ‘xiaoming’, ‘bear’] 这个数组所具备的引用计数就会跟着变成 0(如下图),它就变成了一块没用的内存,即将面临着作为 “垃圾” 被回收的命运。

image.png

1.2引用计数弊端循环引用详解

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

在代码第 8 行,我们执行了 badCycle 这个函数。大家知道,函数作用域的生命非常短暂,当函数执行完之后,作用域内的变量也会全部被视作 “垃圾” 进而移除。

但如果咱们用了引用计数法,那么即使 badCycle 执行完毕,cycleObj1 和 cycleObj2 还是会活得好好的 —— 因为 cycleObj2 的引用计数为 1(cycleObj1.target),而 cycleObj1 的引用计数也为 1 (cycleObj2.target)(如下图)。

image.png 引用计数法无法识别循环引用场景下的 “垃圾”如果任由 cycleObj1、cycleObj2 这样的变量肆虐内存,那么会产生内存泄漏。

1.2标记清除法

自 2012 年起,所有浏览器都使用了标记清除算法。可以说,标记清除法是现代浏览器的标准垃圾回收算法。 标记清除的核心思路是可达性(Reachability)这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些有引用到的对象,就认为是不可用的对象; 这个算法有两个阶段,分别是标记阶段和清除阶段:

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

这个算法可以很好的解决循环引用的问题;

实例

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

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

1.3其他算法优化补充

JS引擎比较广泛的采用的就是可达性中的标记清除算法,当然类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会

结合一些其他的算法。

标记整理(Mark-Compact) 和“标记-清除”相似;

  • 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化;

分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。

  • 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理;

  • 那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少;

增量收集(Incremental collection)

  • 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。

  • 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的

延迟;

闲时收集(Idle-time collection)

  • 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

V8引擎详细的内存图堆,V8引擎内存进行了详细的划分 image.png

2.今日精进

人生是一场不断蜕变旅途,没有终点。每天持续充电,找寻机会发展副业。

Day38【2022年8月31日】

今日精进

多跟同事沟通避免不必要的错误,做事还要更细心一点,多实践多练习,认真对待每一件事。

Day39【2022年9月1日】

今日精进

多听多思考,多跟别人沟通,有时候换个角度思考问题会有不一样的感悟。

Day40【2022年9月2日】

今日精进

走出舒适圈,每天都要给自己一点压力,不能过的太安逸,有压力才有动力。

Day41【2022年9月3日】

今日精进

人只有觉得后悔的时候说明留给自己努力的时候不多了,趁现在还有机会,全力以赴不给未来留遗憾。

Day42【2022年8月4日】

今日精进

莫为眼前蝇头小利,放弃未来,格局打开,目光发长远,会发现不一样的风景。

参考资料

  • JavaScript高级程序设计(第4版)
  • MDN
  • coderwhy大神资料参考
  • 解锁前端面试体系核心攻略

结语

志同道合的小伙伴可以加我,一起交流进步,我们坚持每日精进(互相监督思考学习,如果坚持不下来我可以监督你)。我们一起努力鸭! ——>点击这里

备注

按照时间顺序倒叙排列,完结后按时间顺序正序排列方便查看知识点,工作日更新。