js的堆栈和垃圾回收机制(gc)

news/2024/11/14 20:59:22/

js的堆栈和垃圾回收机制(gc)

本文目录

    • js的堆栈和垃圾回收机制(gc)
      • 堆栈
      • 深拷贝和浅拷贝
        • 实现深拷贝
      • 垃圾回收机制
        • 栈溢出
        • 概念
        • 垃圾产生
        • 算法策略
        • V8引擎的优化
          • 新生代:Scavenge 算法
          • 老生代:标记-清除-整理 算法

堆栈

在js引擎中对变量的存储主要有两种位置

栈内存(stack)
堆内存(heap)

栈内存(stack):基本数据类型(Number、String 、Boolean、Null和Undefined)存储在栈中,按值访问,栈会自动分配内存空间,自动释放,存放简单类型,简单的数据段,占据固定大小的空间。
堆内存(heap):引用数据类型(Object 、Array 、Function等)的具体内容存储在堆中,其在堆内存中的引用地址(指针)存储在栈中,按引用访问(访问引用类型的数据时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据)动态分配的内存,大小不定也不会自动释放,存放引用类型

简单数据类型是在栈里直接开辟一个空间存放它的值
复杂数据类型则是在栈里开辟一个空间存放它内容的地址(十六进制),这个地址指向存放在堆里的内容

为什么基本数据类型存储在栈中,引用数据类型存储在堆中?
JavaScript引擎需要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都存放在栈空间里面,会影响到上下文切换的效率,进而影响整个程序的执行效率。

  • 基本类型
var a = 10; // 栈内存:a -> 10
var b = a; // 栈内存:b -> 10
b = 20; // 栈内存:b -> 20
// a仍然是10,b现在是20。// 函数里的形参如果是简单数据类型,它的值也是存储在栈里的
function fn(y){y++; // 栈内存(函数作用域内):y -> 11console.log(y); // 输出:11
}
var x = 10; // 栈内存:x -> 10
fn(x); // 将x的值复制给a,栈内存(函数作用域内):y -> 10
console.log(x); // 输出:10
// x仍然是10,因为函数内部修改y的值并不影响x的值。

  • 引用类型
var a = {name: '小红',age: 18
}
console.log(a)var b = a;
b.age = 20
console.log(a)
console.log(b)

深拷贝和浅拷贝

  • 浅拷贝:只是把存储在栈里的内容进行了赋值。
  • 深拷贝:就是复杂数据类型a给另一个复杂数据类型b赋值时,跳过栈,直接寻找a在堆里的内容进行拷贝,并在堆里为b开辟新的空间存储地址。

实现深拷贝

  • JSON.parse(JSON.stringify(a))
var b = JSON.parse(JSON.stringify(a));

对象的属性值不能是 undefined、symbol、函数、日期和正则

  • Object.assign(obj1, obj2)
let obj = {id: 1,name: '张三',age: 10,
}
let newObj = Object.assign({}, obj)

只有一级属性为深拷贝,二级属性后就是浅拷贝

二级属性例子

let obj = {id: 1,name: '张三',age: 10,friends: ['李四', '王五'],address: {street: '某某街',number: 100}
};
let newObj = Object.assign({}, obj);

在栈内存中,obj和newObj的friends和address属性并不直接存储这些对象的值,而是存储了指向堆内存中相应数据的引用地址(也就是Address3和Address4)

Address3和Address4是表示friends数组和address对象在堆内存中的地址。这两个属性是复杂数据类型(即它们是对象),因此他们的值被存储在堆内存中
这是因为JavaScript对于复杂数据类型(例如对象、数组)总是通过引用来处理的,而非直接存储其值。这就意味着,当你修改了obj的friends或address属性时,由于newObj的这些属性引用的是同一块堆内存,因此newObj中相应的属性值也会随之改变。这就是为什么Object.assign()方法只能实现一层深拷贝的原因

  • 扩展运算符
var obj = {a: 1,b: 2
}
var obj1 = {…obj}
// 数组
let newArr = [...arr]

只有一级属性为深拷贝,二级属性后就是浅拷贝

  • 数组使用数组方法进行深拷贝
var arr1 = [1, 2, 3, 4]
var arr2 = arr1.concat()
var arr3 = arr1.slice(1)

只有一级属性为深拷贝,二级属性后就是浅拷贝,如[1,2,3,[1,2,3]]

垃圾回收机制

栈溢出

(function foo() {foo()
})()

栈虽然很轻量,在使用时创建,使用结束后销毁,但是不是可以无限增长的,被分配的调用栈空间被占满时,就会引起”栈溢出“的错误

Maximum call stack size exceeded

概念

在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收

垃圾回收过程是不实时进行的,因为JavaScript是一门单线程的语言,每次执行垃圾回收,会使程序应用逻辑暂停,执行完垃圾后回收在执行应用逻辑,这种行为称为全停顿,所以一般垃圾回收会在cpu闲时进行

垃圾产生

程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃

先声明了一个Person变量,它引用了对象

{name: "江流",age: 20}

接着又将这个Person变量指向了另一个对象

{name: "心猿", age: 5000}

那么之前被引用的对象,现在就成了无用对象,也永远无法使用操作该对象,这种对象就是一个垃圾

算法策略

V8引擎的优化

  • 新生代内存是临时分配的内存,存活时间短,新生对象或只经过一次垃圾回收的对象
  • 老生代内存是常驻内存,存活时间长,经历过一次或多次垃圾回收的对象
新生代:Scavenge 算法

新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收,在 Scavenge 算法 的具体实现中,主要采用了一种复制式的方法即 Cheney 算法

Cheney算法 中将堆内存一分为二,一个是处于使用状态的空间 使用区(from),一个是处于闲置状态的空间 空闲区(to)

  • 新的对象会首先被分配到对象(from)空间,当对象区域快写满时,就需要执行一次垃圾清理操作。
  • 当进行垃圾收回时,先将 from 空间中存活的对象复制到空闲(to)空间进行保存,对未存活的空间进行回收。
  • 复制完成后,对象空间和空闲空间进行角色调换,空闲空间变成新的对象空间,原来的对象空间则变成空闲空间。
  • 这样就完成了垃圾对象的回收操作,同时这种角色调换的操作能让新生代中的这两块区域无限重复使用下去

缺点:堆使用效率低下

当一个对象在两次变换中还存在时,就会从 新生代区 晋升到 老生代区,这一过程被称为对象晋升策略

老生代:标记-清除-整理 算法

复制大对象所花费的时间长,执行效率并不高

  • 标记-清除(Mark-Sweep)

    分为标记和清除两个阶段。标记阶段会遍历堆中所有的对象,并对存活的对象进行标记,清除阶段则是对未标记的对象进行清除

  • 标记-整理(Mark-Compact)

    经过标记清除之后的内存空间会生产很多不连续的碎片空间,这种不连续的碎片空间中,在遇到较大的对象时可能会由于空间不足而导致无法存储。为了解决内存碎片的问题,需要使用另外一种算法 - 标记-整理(Mark-Compact)。标记整理对待未存活对象不是立即回收,而是将存活对象移动到一边,然后直接清掉端边界以外的内存

  • 增量标记

    为了避免垃圾回收时间过长影响其他程序的执行,V8将标记过程分成一个个小的子标记过程,同时让垃圾回收和JavaScript应用逻辑代码交替执行,直到标记阶段完成,这个过程为增量标记算法

    把垃圾回收这个大的任务分成一个个小任务,穿插在 JavaScript任务中间执行


http://www.ppmy.cn/news/82253.html

相关文章

【LeetCode】169. 多数元素

169. 多数元素(简单) 方法一:sort排序,时间复杂度为O(nlogn) 思路 我自己的写法用了最简单的方法,首先使用 sort() 对数组元素按照从小到大进行排序,然后依次遍历每个元素,如果该元素的出现次…

代码随想录算法训练营第四十八天 | 树形dp

198.打家劫舍 文档讲解:代码随想录 (programmercarl.com) 状态:看了“决定dp[i]的因素才做出来"。 思路 当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。 所以这里就更感觉到,当前状态和前面状态会有一种依赖关系&#xf…

C语言CRC-16 DNP格式校验函数

C语言CRC-16 DNP格式校验函数 CRC-16校验产生2个字节长度的数据校验码,通过计算得到的校验码和获得的校验码比较,用于验证获得的数据的正确性。基本的CRC-16校验算法实现,参考: C语言标准CRC-16校验函数。 不同应用规范通过对输…

622. 设计循环队列

622. 设计循环队列 Java实现循环队列设计 题目描述 设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。 循环队列的一个好处是我…

mysql执行计划explain

mysql 执行计划 explain 介绍 mysql8.0为例:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html EXPLAIN为语句中使用的每个表返回一行信息 SELECT。它按照 MySQL 在处理语句时读取它们的顺序列出输出中的表。这意味着 MySQL 从第一个表中读取一行&…

牛牛截图控件与利洽远程控制产品升级-支持证书自动升级

今天我们来聊一聊浏览器控件的一个痛点!看看我们是如何解决他的。 背景信息 目前市面上存在多种浏览器,IE、Chrome、Firefox、Edge以及一众国产浏览器,这些浏览器中,IE支持ActiveX,部分国产浏览器支持npapi&#xff…

前端架构师-week7-B端项目需求分析和架构设计

标题 B端项目需求分析 和 架构设计 将收获什么 做怎样的项目完成瓶颈期的突破 怎样从需求中寻找关键难点 怎样写技术解决方案 怎样进行基础的技术选型 关键词 挖掘难点 - 找到项目中的痛点 技术解决方案 - 以文档的形式创造可追溯的思考模型 业务组件库 - 多项目复用的业务组…

LIN-报文结构

文章目录 协议规范一、字节场二、报文头(HEADER FIELDS)同步间隔(synchronisation break)同步场(SYNCH FIELD)标识符场(IDENTIFIER FIELD) 三、数据场(DATE FIELDS)四、校…