内存概述
内存也是有生命周期的,不管什么程序语言,一般可以按顺序分为三个周期:
分配期
分配所需要的内存
使用期
使用分配到的内存(读、写)
释放期
不需要时将其释放和归还
定义变量自动分配内存
绝大部分情况下也不需要手动释放内存
而我们程序员大多只需关注使用内存
内存泄漏
当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用(或者由于设计错误)而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
而内存泄漏也是造成应用程序OOM(内存严重不足)的罪魁祸首之一
内存泄露的问题其困难在于
1.编译器不能发现这些问题。
2.运行时才能捕获到这些错误,这些错误没有明显的症状,时隐时现。
3.对于手机等终端开发用户来说,尤为困难。
解决方法:
避免创建全局变量,开启严格模式
不能滥用闭包
清除没有用的DOM元素引用 (
document.body.removeChild(DOM元素)
)定时器用完离开页面记得手动删除(
clearInterval()
)使用Vue的时候,在页面销毁时记得对事件解绑,对EventBus进行解绑
beforeDestory () { window.removeEventListener('事件名', 接收时的回调函数(参数)) } destroyed () { this.$bus.$off("事件名", 接收时的回调函数(参数)); }
在ES6 里可以使用
WeakMap
、WeakSet
JavaScript的垃圾回收机制(GC)
标记清除算法
设置一个根对象(root)(在Javascript里,根是全局对象)),然后垃圾回收器会定期从根(root)扫描内存中的对象,凡是能从根到达的对象,就是还需要用的,到达不了的进行标记,稍后回收
所有标记清除算法有两个阶段:
- 标记阶段
- 清除阶段
算法缺陷:无法从根对象查询到的对象都会被清除,垃圾收集完毕后会造成大量内存碎片,也就是造成内存空间不连续的问题(缺陷详情可以在下面的V8老生代垃圾回收机制查看)
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进
引用计数算法
古老的垃圾回收算法,没那么常用了
原理是跟踪记录每个值被引用的次数,被引用一次,则count + 1(除了弱引用类型WeakMap
、WeakSet
)
垃圾回收程序下次运行的时候就会释放引用数为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 降级
(降级:某些特殊情况下,在出现大量占用了一些稀缺服务资源,在紧急情况下可以对其整个降级,以达到丢卒保帅;降级的最终目的是保证核心服务可用,即使是有损的)