内存管理
javascript内存管理
内存变化持续升高、内存泄露。
// 申请空间
let obj = {}
// 使用
obj.name = 'lg'
// 释放
obj = null;
javascript垃圾回收
可达对象,从根上出发,可以访问到的,就是可达
let obj = {name : 'a'}
let ali = obj;
obj = null;
// ali还在引用,name还是可达
delete obj.o1
delete obj.o2.prev
o1不可达了,变成了辣鸡
垃圾回收与常见的GC算法
1、name作用域全局,但是没有引用了
2、name局部作用域
引用计数算法
引用计数算法优缺点
引用数为0,就立即释放 时刻监控引用为0的情况,所以可以一直释放,保证了当前内存不会占满
1、obj1,obj2局部作用于,在根查找不到了。但是由于两者在作用域范围内,相互引用了,所以引用计数不为0,此时就造成了内存浪费;
function fn() {
const obj1 = {};
const obj2 = {};
obj1.name = obj2;
obj2.name = obj1;
return '111'
}
fn();
2、需要维护数值变化,所以要一直查找数值是否为0,查找的数值越多,开销就越大
标记清楚算法
标记清楚算法优缺点
1、对可达对象进行标记。
2、没有标记的,清除; 抹除可达对象的标记。
3、回收。
标记阶段:递归的方式去遍历可达对象,进行标记。
清除阶段:GC后续开始工作的时候,就会把没有标记的对象进行回收,并清除已标记的对象的标记。
回收的空间会放在空闲列表中,方便程序直接在列表中申请空间使用
标记算法优缺点
相对于引用计数,引用计数无法清除相互引用的对象。
而标记清除可以,如果局部函数中的相互引用对象,不可达,则认为可以回收。
缺点:
释放完的空间分散的,地址不连续。导致空间碎片化。无法最大化使用空间。
比如两个域,但是刚好数据是1.5个域,那么左侧空间过大,右侧空间过小。
标记整理算法实现原理
整理过程:
将对象进行移动,形成连续的空间
常见的GC算法
1、引用计数:通过引用计数器,维护每个对象的引用数值,通过数值来判断是否需要回收 2、标记清除:遍历所有对象,对当前所有活动对象进行标记,对没有标记的对象进行回收,释放空间 3、标记整理:在清除上的前置操作,会整理当前的地址空间 4、分代回收:V8
各自优缺点
1、 优点:即时回收垃圾对象,只要发现计数器为0,就回收。最大程度减少程序卡顿。 缺点:无法回收存在的循环引用的对象。 资源消耗大,需要频繁操作、维护计数器。
2、 优:回收循环引用的对象 缺:容易产生碎片空间。 不会立即回收垃圾对象,需要等到遍历完成之后,才开始清除,清除的时候,程序会停止工作。
3、 优:减少空间碎片化。 缺:不能立即回收垃圾对象。
V8
主流的avascript执行引擎(优秀的内存管理机制 即时编译(可以把源码直接翻译成可执行,以往是需要先将源码转成字节码 内存设限,64位系统 1.5G,32位系统 800M
官方测试,当垃圾内存达到1.5G的时候,进行分量标记算法只需要消耗50ms;采用非分量算法,需要1s; 所以以1.5G为界;
V8引擎的辣鸡回收
策略:
分代回收思想
内存分为新生代、老生代
针对不同对象采用不同GC算法
内存有上限,基于此,需要分代回收思路,不同代采用更适合的GC算法
V8内存分配:
V8内存空间一分为二,上图。
小空间用于存储新生代对象(32M | 16M)(64bit | 32bit)
新生代指的是存活时间较短的对象(局部作用域变量,执行完肯定要回收。不像全局变量,需要程序退出,才会回收。)
新生代 —— 实现回收:
回收过过程采用复制算法 + 标记整理
新生代内存区分为两个等大小空间
from是使用空间,to是空闲空间
活动对象存储于from空间 (当from空间满了
标记整理后将活动对象拷贝至to空间,完全释放from空间,(活动对象就是还有在使用的对象,完全释放from空间,因为活动对象已经复制到to了
from空间与to空间交换 (复制之后,to空间都是当前的活动对象,直接交换即可。
回收细节
拷贝过程,发现新生代在老生代也出现了,那么这个对象,就要晋升,
晋升就是将新生代对象,移动到老生代;
判断标准:
一轮GC还存活的新生代,那么就要晋升。
拷贝过程,To空间的使用率超过25%。(在未来回收操作的时候,是要把To跟from进行交换,那么就是剩余少于80%了,新对象可能会存储不进来。
老生代 —— 实现回收:
存储在右侧空间
内存大小(1.4G | 700M) (64bit | 32bit)
老生代指的就是存货时间长 (全局作用域对象,闭包
主要 标记清除,标记整理,增量标记算法
首先标记清除完成垃圾空间回收(会有空间碎片化出现
采用标记整理进行空间优化 (当新生代需要移动到老生代,出现空间不足,此时会触发标记整理,优化空间
采用增量标记进行效率优化
细节对比
(与新生代新生代区域垃圾回收,使用空间换时间 (采用复制算法,每时每刻,内部都会有空闲空间存在;因为新生代本身空间小,空间的浪费,换来的时间效益,是微不足道的。
老生代垃圾回收不适合复制算法,(如果跟新生代一样,采用一分为二,那么就有很多空闲空间浪费;老年代存储的对象也对,复制算法耗时也多。
增量算法
垃圾回收工作的时候,是会阻塞javascript运行, 程序执行之后,空档期进行垃圾回收。
增量算法,就是把整个垃圾回收工作,分为几个小部分,分批完成。而不是以往的整个工作完成。合理分配时间消耗。利用每一个空档期。
先遍历找到第一层可达对象,然后就继续程序正常执行,然后再查找二层,再正常进行,知道完成标记工作。 开始清除,清除之后执行。
非增量总耗时不超过1s,这里采用增量,时间耗时就更少,所以程序多次阻塞,实际耗时不多。
总结
V8是主流的javascript执行引擎
V8内存设置上限(过多,用户感知明显
基于分代回收思想实现垃圾回收
内存分新生代、老生代
垃圾回收常见的GC算法(新生代:复制+标记整理; 老生代:标记清除+整理+增量标记)
Performace工具
内存问题的体现
页面延迟加载、经常性暂停 (内存泄露: 内存使用持续升高
持续性出现槽糕的性能 (内存膨胀,内存满了的时候会去申请扩大内存,但是仍然不够满足
页面性能随时间越来越差 (频繁的内存回收
浏览器的任务管理器
timeline时序图记录
对快照查找分离DOM
判断是否存在频繁的垃圾回收
浏览器任务管理器
shift + esc
内存,表示原生内存,DOM节点占据的内存;不断增大,DOM增加越多
JavaScript内存,js的堆,括号中表示界面中可达对象的数量;
无法定位具体问题;
Timeline记录内存
堆快照查找分离DOM 工作原理:
分离dom:脱离dom树,在界面不显示。
Memory快照
频繁的垃圾回收,会阻塞程序运行。
Performance
如何精准测试 JavaScript 性能
代码优化实例
慎用全局变量
全局变量跟局部变量的性能差别
将使用中无法避免的全局变量,缓存到局部 document缓存到obj中
通过原型新增方法
在原型对象,新增实例对象需要的方法
避开闭包陷阱
闭包特点: 外部具有指向内部的引用 在外部作用域访问内部作用域的数据
跨作用域使用对象,闭包。
el被引用,无法回收。 此处,按钮对象, 被document引用、被el引用; 如果按钮以后被移除了,document被移除了,但是el还应用,内存不回收。
需要手动清除。
避免属性访问方法使用
ops/s 指的是每秒可完成多少个这样的operations请求。
For循环优化
采用最优循环方式
forEach > for > forin
节点添加优化
节点操作必然伴随着回流,重绘;
利用文档碎片
克隆优化节点操作
直接量替换Object操作
JSBench jsbench.me/
运行速度快,不一定就是性能好。 速度只是性能中的一点,应该从空间、时间一起考虑。
减少判断层级,巧用return
明确判断条件,用case switch,易于维护
减少作用域链查找层级
查找层级多,但是空间消耗小。
name = '1';
function foo() {
name = '2';
console.log(name)
}
查找层级少,但是空间消耗大。
name = '1';
function foo() {
var name = '2';
console.log(name)
}
减少数据读取次数
每一次都要在ele查询
var a = {b: 1}
function foo(ele, cls) {
return ele.b === cls
}
提前缓存,但是占用空间
var a = {b: 1}
function foo(ele, cls) {
var name = ele.b;
return name === cls
}
字面量与构造式
构造式,相当于函数调用
obj = new Object();
obj.name = 1;
obj.age = 12;
str = new String('1')
字面量,直接开辟新空间存储数据
obj = {
name: 1,
age: 12
};
str1 = '1'
减少循环体活动
不变的数据,在循环体外部执行,相当于数据缓存
for ---- while
for是从前,往后找,所以会多了一些前置条件判断 while是从后往前找
减少声明以及语句数 使用的时候再声明,而不是提前缓存。
与之前的优化,减少访问层级,有冲突。
1、语句数过多,编译阶段有所消耗
2、不会每次都使用的,可以直接使用,也减少空间
语句数少了,但是书写的时候,应该用第一种,可读性高。
采用事件委托
var list = document.querySelectorAll('li);
var showText = (e) => {console.log(e.target.innerHTML)}
for(let item of list) {
item.onclick = showText;
}
var ul = document.querySelectorAll('ui);
var showText = (e) => {
var obj = e.target;
if (obj.nodeName.toLowerCase() === 'li') {
console.log(e.target.innerHTML)
}
}
ul.addEventListener('click', showText, true);
事件监听太多占用内容,但是添加事件委托,也要考虑查找子元素的时候