内存管理
- 开发者主动申请空间,使用空间,释放空间
- 由于es中没有提供相应的内存管理API,所以JS不能像C,C++一样由开发者主动调用API完成空间管理
//申请空间
let obj = {}
//使用
obj.name = ‘yyy’
//释放
obj = null
GC算法
垃圾回收机制的简写
GC可以找到内存中的垃圾,并释放和回收
- 引用计数
- 标记清除
- 标记整理
- 分代回收
引用计数
设置引用数,判断当前引用数是否为0
引用计数器:GC通过判断引用计数器是否为0判断是否需要内存回收
优缺:发现垃圾立即回收,最大限度减少程序的暂停
缺点:无法回收循环引用对象,时间开销大
function fn2(){
const name = 'aa'
return name
}
fn2() //执行完后,函数内name变量的引用计数变成0,fn2所占的内存被回收释放
//obj1和obj2循环引用
function objGroup(obj1,obj2){
obj1.next = obj2
obj2.prev = obj1
return {
o1:obj1,
o2:obj2
}
}
标记清除
将垃圾回收操作分成两个阶段 标记阶段和清除阶段
标记阶段:遍历所有对象(递归查找)给活动对象标记
清除阶段:遍历所有对象清除没有标记的对象,同时清除掉标记
优点:解决循环引用不能回收问题
缺点:会产生垃圾碎片化问题,不能使空间最大化使用(当释放的空间内存地址不连续,导致分散在空间角落,导致下次再使用内存不太适合再使用)
标记整理
标记整理可以看作是标记清除的增强
标记阶段和标记清除算法一致
整理阶段:会在清除之前,先执行整理,移动对象,使释放的地址连续,再执行清除操作
优点:减少碎片化空间
缺点:不会立即回收垃圾对象
v8
- v8是一款主流的js执行引擎
- v8采用即时编译
- 内存设限 64 * 1.5G | 32 * 800M
- v8垃圾回收策略
- 采用分代回收思想
- 内存分为新生代和老生代
- 针对不同对象采用不同算法
内存管理,性能优化
v8常用GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
v8内存分配
- v8内部把空间一分为二,新生代对象和老生代对象
- 左边的小空间用于存储新生代对象(32M | 16M)
- 新生代对象指的是存活时间较短的对象
新生代对象回收实现
- 回收过程采用复制算法+标记整理
- 新生代内存区分为二个等大小空间
- 使用空间为From,空闲空间为To
- 活动对象存储于From空间
- 触发GC机制后,标记整理From内的活动对象,后将活动对象拷贝至To
- From与To交换空间完成释放
回收细节说明
- 拷贝过程中可能存在晋升(某个活动对象再老生代内存中也会出现)
- 晋升就是将新生代对象移动至老生代
- 一轮GC之后还存活的新生代需要晋升
- To空间的使用率超过25%需要晋升
V8如何回收老生代对象
- 老生代对象存放在(上图)右侧老生代区域
- 64位1.4G,32位700M
- 老生代对象就是指存活时间较长的对象(闭包,全局变量)
老年代对象回收实现
- 主要采用标记清除,标记整理,增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 当新生代内存向老生代区域移动的时候,且老生代存储空间又不足以存放新生代存储区移过来的内容,采用标记整理进行空间优化
- 采用增量标记进行效率提升
新老生代对比
- 新生代区域垃圾回收使用空间换时间
- 它采用复制算法,每时每刻都有空闲空间存在,但是由于新生代空间本身就很小,分出来的空间就更小,所以空间上的浪费相对于时间上的提升是微不足道的。
- 老生代区域垃圾回收不适合复制算法
- 因为老生代区域空间大,如果一分为二会有几百M的空间是浪费不用的
- 老生代区域存放的数据比较多,复制的时间消耗长
- 增量标记
- 垃圾回收会阻塞js的执行
- 让垃圾回收和程序交替执行,而不是一次性执行垃圾回收
内存管理,性能优化 - 总结
- v8主流js执行引擎
- v8内存设置上限
- v8采用基于分代回收思想实现垃圾回收
- v8内存分为新生代和老生代
- v8垃圾回收常用的GC算法(新生代 复制+标记整理 老生代 标记清除,标记整理,增量标记)
界定内存问题的标准
- 内存泄漏: 内存使用持续升高
- 内存膨胀: 程序在多种不同设备上都存在性能问题
- 频繁垃圾回收: 通过内存变化图
监控内存的几种方式
- 浏览器任务管理器
- timeline时序图记录
- 堆快照查找分离dom
- 判断是否存在频繁的垃圾回收
shift+esc打开浏览器任务管理器:
第一列:原生内存,表示当前页面所有dom所占用的内存。
第二列:表示js堆,需要关注小括号中的大小,表示界面当中所有可达对象正在使用的内存大小
Timeline记录
内存管理,性能优化
分离dom
- 界面上的dom节点都应该存在于dom树上
- dom节点只是从dom树上脱离了,但是js代码中还被引用着,这种dom叫做分离dom(界面上没有,但是占用内存)
堆栈
- js执行环境(v8)
- 执行环境栈(在计算机申请内存空间,专门用于执行js代码)
- 执行上下文(管理不同区域,全局和局部等,区分不同区域代码执行)
- VO(G),全局变量对象
- 1.基本数据类型按值进行操作
- 2.基本数据类型是存放在栈区的
- 3.GO:全局对象 window
- 它并不是VO(G)但是它也是一个对象,因此它也会有一个内存的空间地址。
- 因为有地址就可以对其进行访问,js会在VO(G)当中准备一个变量叫window
JSBench
- jsbench.me ops值越大越好
变量局部化提升执行速度
var i,str = ''
function package(){
for(i = 0;i<1000;i++){
str += i
}
}
package()
// 函数内部的变量每次循环都需要到全局查找变量,每循环一次查找一次
function package(){
let str = '';
for(let i = 0;i<1000;i++){
str += i
}
}
package()
// 当前自己的作用域就有str和i
缓存数据
减少访问层级
防抖节流
- 在一些高频率事件触发的场景下我们不希望对应的事件处理函数多次执行
- 场景:
- 滚动事件
- 输入的模糊查询
- 轮播图切换
- 点击操作…
- 浏览器默认情况下都会有自己的监听事件间隔(4-6ms),如果监测到多次事件的监听执行,那么就会造成不必要的浪费
- 前置场景:界面上有一个按钮,我们可以连续多次点击
- 防抖:对于这个高频操作来说,我们只希望识别一次点击,可以是第一次或者是最后一次
- 节流:对于高频操作,我们可以自己设置频率,让本来很多次的事件触发,按照我们定义的频率减少触发次数
//防抖实现
var oBtn = document.getElementById('btn')
// handle是最终要执行的事件
// wait 事件触发之后多久开始执行
// immediate 控制执行第一次还是最后一次,false执行最后一次
function myDebonce(handle,wait,immediate ){
// 参数类型判断
if(typeof handle !== 'function') throw new Error('handle must be function')
if(typeof wait === 'undefined') wait = 300
if(typeof wait === 'boolean'){
immediate = wait
wait = 300
}
if(typeof immediate !== 'boolean')immediate = false
// 所谓的防抖效果我们想要实现的就是有一个'人'可以管理handle的执行次数
//如果我们想要执行最后一次,那就意味着无论我们当前点击了多少次,前面的n-1次都没用
let timer = null
return function proxy(...args){
let self = this,
init = immediate && !timer
clearTimeout(timer)
timer = setTimeout(()=>{
timer = null
!immediate ? handle.call(self,...args) : null
},wait)
// 如果当前传递的是true 就表示需要立即执行
//如果想要实现只在第一次执行,那么就可以添加上timer为null作为判断
//因为只要timer为null就意味着没有第二次点击
init ? handle.call(self,...args) : null
}
}
function btnClick(e){
console.log('点击了',this,e)
}
//当我们执行了按钮点击之后就会执行...返回的proxy
// false 最后一次执行
// true 第一次执行
oBtn.onclick = myDebonce(btnClick,300,false)
//节流的实现:
// 自定义的一段时间内让事件进行触发
function myThrottle(handle,wait){
if(typeof handle !== 'function') throw new Error('handle must be function')
if(typeof wait == 'undefined')wait = 400
let prev = 0 //定义变量记录上一次执行的时间
let timer = null //
return function proxy(...args){
let now = new Date() //定义变量记录当前执行的时间点
let self = this
let interval = wait - (now - prev)
if(interval <= 0){
clearTimeout(timer)
timer = null
//此时说明是一个非高频次的操作,可以执行handle
handle.call(self,...args)
prev = new Date()
}else if(!timer) {
// 当我们发现当前系统中有一个定时器了,就意味着我们不需要再开启定时器
// 此时说明这次的操作发生在我们定义的频率范围内,那就不应该执行handle
//这个时候我们就可以定义一个定时器,让handle在interval之后去执行
timer = setTimeout(() => {
clearTimeout(timer) //这个操作只是将系统的定时器清除了,但是timer中的值还在
timer = null
handle.call(self,...args)
prev = new Date()
},interval)
}
}
}
function scrollFn(){
console.log('滚动了 ')
}
//window.onscroll = scrollFn
window.onscroll = myThrottle(scrollFn,600)
减少判断层级
function doSomething(part,chapter){
const parts = ['ES2016','工程化','Vue','React']
if(part){
if(parts.includes(part)){
console.log('属于当前课程');
if(chapter > 5){
console.log('您需要提供VIP身份');
}
}
}else{
console.log('请确认模块信息')
}
}
doSomething('ES2016',6)
//属于当前课程
//您需要提供VIP身份
// 优化
function doSomething(part,chapter){
const parts = ['ES2016','工程化','Vue','React']
if(!part){
console.log('请确认模块信息');
return
}
if(!parts.includes(part))return
console.log('属于当前课程');
if(chapter > 5){
console.log('您需要提供VIP身份');
}
}
doSomething('ES2016',6)
//属于当前课程
//您需要提供VIP身份
减少循环体活动
var test = () => {
var arr = ['aa','22','我为前端而活']
var i
for(i= 0;i<arr.length;i++){
console.log(arr[i]);
}
}
test()
var test = () => {
var arr = ['aa','22','我为前端而活']
var len = arr.length
for(i = 0;i < len;i++){
console.log(arr[i]);
}
}
test()
var test = () => {
var arr = ['aa','22','我为前端而活']
var len = arr.length
while(len--){
console.log(arr[len]);
}
}
test()
字面量与构造式
let test = () => {
let obj = new Object()
obj.name = 'aa'
obj.age = 22
obj.slogan = '我为前端而活'
return obj
}
// 字面量优于构造式
let test = () => {
let obj = {
name: 'aa',
age: 22,
slogan: '我为前端而活'
}
return obj
}