深入JavaScript内存


内存概述

内存也是有生命周期的,不管什么程序语言,一般可以按顺序分为三个周期:

  • 分配期

    分配所需要的内存

  • 使用期

    使用分配到的内存(读、写)

  • 释放期

    不需要时将其释放和归还

定义变量自动分配内存

绝大部分情况下也不需要手动释放内存

而我们程序员大多只需关注使用内存

内存泄漏

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用(或者由于设计错误)而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

而内存泄漏也是造成应用程序OOM(内存严重不足)的罪魁祸首之一

内存泄露的问题其困难在于

1.编译器不能发现这些问题。

2.运行时才能捕获到这些错误,这些错误没有明显的症状,时隐时现。

3.对于手机等终端开发用户来说,尤为困难。

解决方法:

  • 避免创建全局变量,开启严格模式

  • 不能滥用闭包

  • 清除没有用的DOM元素引用 (document.body.removeChild(DOM元素))

  • 定时器用完离开页面记得手动删除(clearInterval()

  • 使用Vue的时候,在页面销毁时记得对事件解绑,对EventBus进行解绑

    beforeDestory () {
        window.removeEventListener('事件名', 接收时的回调函数(参数))
    }
    destroyed () {
        this.$bus.$off("事件名", 接收时的回调函数(参数));
    }
  • 在ES6 里可以使用 WeakMapWeakSet

JavaScript的垃圾回收机制(GC)

标记清除算法

设置一个根对象(root)(在Javascript里,根是全局对象)),然后垃圾回收器会定期从根(root)扫描内存中的对象,凡是能从根到达的对象,就是还需要用的,到达不了的进行标记,稍后回收

所有标记清除算法有两个阶段:

  • 标记阶段
  • 清除阶段

算法缺陷:无法从根对象查询到的对象都会被清除,垃圾收集完毕后会造成大量内存碎片,也就是造成内存空间不连续的问题(缺陷详情可以在下面的V8老生代垃圾回收机制查看)

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进

引用计数算法

古老的垃圾回收算法,没那么常用了

原理是跟踪记录每个值被引用的次数,被引用一次,则count + 1(除了弱引用类型WeakMapWeakSet

垃圾回收程序下次运行的时候就会释放引用数为0的内存

它有很多计数问题,比如引用循环,对象A有一个指针指向对象B,而对象B也引用了A,他们的引用书永远不会变成0

V8的内存分代和回收算法

Chrome 浏览器所使用的 V8 引擎就是采用的分代回收策略。这个和 Java 回收策略思想是一致的。目的是通过区分「临时」与「持久」对象;多回收「临时对象区」(新生代young generation),少回收「持久对象区」(老生代 tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。

新生代中的对象存活时间较短的对象,老生代中的对象存活时间较长,或常驻内存的对象。

新生代

新生代中的对象主要通过Scavenge算法进行垃圾回收

它将堆内存一分为二切开,让每一个空间成为一个semispace,而这两个空间中,一个处于使用中(From空间),另一个处于闲置状态(To空间)。由于新生代生命周期短,所以比较适合这个频繁对换 + 释放的算法

此时垃圾回收开始:

1.检查From空间中存活对象,然后复制到To空间中,而非存活对象空间会被释放掉,From空间和To空间的身份对换。

2.当一个对象多次复制之后依然存活,说明人家比较长寿,就会被迁移至老生代之中

3.To闲置空间内存占用超过25%,也会被迁移至老生代之中

老生代

老生代使用的是标记清除法,活对象在新生代中只占叫小部分,死对象在老生代中只占较小部分,这是为什么采用标记清除算法的原因。

从上文我们也可以清楚标记清除法的缺陷问题(内存碎片问题,内存空间不连续)

  • 此时如果需要分配一个大对象,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
  • 为了解决碎片问题,标记整理被提出来。就是在对象被标记死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

参考链接https://juejin.cn/post/6844903951742025736

减少垃圾回收对性能的影响:1.让垃圾回收尽量少地进行 2.避免内存泄露

提升性能

避免JavaScript的先创建再补充的动态属性赋值,而是一次性声明所有属性,让实例们共享一个隐藏类

//错误示范❌
function Article(){
    this.title = 'my name is title';
}
let a1 = new Article();
let a2 = new Article();
a1.author = 'Allen';
//正确示范√
function Article(author){
    this.title = 'my name is title';
    this.name = author;
}
let a1 = new Article('Allen');
let a2 = new Article();

由于JavaScript数组大小是动态可变的,,引擎会删除大小为100的数组,在创建一个新的大小为200的数组,垃圾回收程序看到这个删除操作,说不定看你对象更替速度那么快,就加快对你这里垃圾回收的频率,从而降低性能。要避免这种动态分配的操作,可以在初始化时就创建一个大小够用的数组,从而避免上述先删除再创建的操作,不过,你必须事先想好这个数组有多大。

(实际上静态分配是优化的一种极端方式,如果你的应用程序被垃圾回收严重拖了后腿,可以利用它来提升性能,但这种情况并不多见,大多情况下,这都属于过早优化。)

V8性能优化

在 V8 引擎下,又引入了 TurboFan 编译器,他会在特定的情况下进行优化,将代码编译成执行效率更高的 Machine Code

然而什么情况下会转换成为machine code?

1.

function test(x) {
  return x + x
}
test(1)
test(2)
test(3)
test(4)

以上函数被多次调用并且参数一直传入 number 类型,那么 V8 就会认为该段代码可以编译为 Machine Code,因为你固定了类型,不需要再执行很多判断逻辑了。

所以我们要尽可能保证传入类型一致

这也给我们带来了一个思考,这是不是也是使用 TypeScript 能够带来的好处之一

2.

另外,编译器还有个骚操作 Lazy-Compile,当函数没有被执行的时候,会对函数进行一次预解析,直到代码被执行以后才会被解析编译。然而有时候,我们的函数只需要被预解析一次,然后在调用的时候再被解析编译。但是对于这种函数马上就被调用的情况来说,预解析这个过程其实是多余的,那么有什么办法能够让代码不被预解析呢?

立即执行函数

(function test(obj) {
  return x + x
})()

但是不可能我们为了性能优化,给所有的函数都去套上括号,并且也不是所有函数都需要这样做。

缓存雪崩

缓存雪崩就是指缓存由于某些原因(比如 宕机、cache服务挂了或者不响应),导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。

其实也可以解释为cache crash之后,牵一发而动全身,导致后端的各大区域接而无法进行服务而崩溃,全部拖死

前情提要:redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。

类似于:

1、redis集群彻底崩溃

2、缓存服务大量对redis的请求hang住,占用资源

3、缓存服务大量的请求打到源头服务去查询mysql,直接打死mysql

4、源头服务因为mysql被打死也崩溃,对源服务的请求也hang住,占用资源

5、缓存服务大量的资源全部耗费在访问redis和源服务无果,最后自己被拖死,无法提供服务

6、nginx无法访问缓存服务,redis和源服务,只能基于本地缓存提供服务,但是缓存过期后,没有数据提供

7、网站崩溃

产生原因

1、例如 “缓存并发”,“缓存穿透”,“缓存颠簸” 等问题,这些问题也可能会被恶意攻击者所利用。

  • 缓存穿透

    指查询一个缓存和数据库都没有的数据,例如我们数据库的id都是从1开始自增的,如果传入的参数为-1或者特别大不存在的数据,就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存

  • 缓存击穿

    指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库

2、例如 某个时间点内,系统预加载的缓存周期性集中失效了,例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期。

解决方法:可以通过设置不同的过期时间,来错开缓存过期,从而避免缓存集中失效。

预防和解决

  • 保证缓存层服务高可用性,如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。

    • 部署方式一:双机房部署,一套Redis Cluster,部分机器在一个机房,另一部分机器在另外一个机房。

    • 部署方式二:双机房部署,两套Redis Cluster,两套Redis Cluster之间做一个数据同步。

  • Redis数据备份和恢复、快速缓存预热

  • 对源服务访问进行 限流、资源隔离(熔断)、Stubbed 降级。

  • 对缓存访问进行 资源隔离(熔断)、Fail Silent 降级

    (降级:某些特殊情况下,在出现大量占用了一些稀缺服务资源,在紧急情况下可以对其整个降级,以达到丢卒保帅;降级的最终目的是保证核心服务可用,即使是有损的)


文章作者: Hello
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hello !
 上一篇
Vue(下) Vue(下)
7.其他事件总线事件总线和vuex的作用很像,只不过vuex用于管理状态(变量),而事件总线用于管理事件,利用事件传参(感觉vuex有点面向受控组件,而eventbus面向非受控组件) 1.在main.js 文件中 创建一个新的Vue实例,
2021-03-22
下一篇 
安全问题汇总 安全问题汇总
前言Egg的网络安全介绍 1.XSS什么是XSSCross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击,通过注入恶意脚本,,使之在用户浏览器上运行,然后利用这些恶意脚本,攻击者可以获取用户的敏感信息Cook
2021-03-15
  目录