内容概要
- 内存管理
- 垃圾回收和常见的GC算法
- V8引擎的垃圾回收
- Performance 工具
- 代码优化实例
内存管理介绍
- 内存: 由可读写单元组成,表示一片可操作的空间
- 管理: 认为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 管理流程:申请——使用——释放
JavaScript中的内存管理
- 申请空间
- 使用空间
- 释放空间
JavaScript中的垃圾
- JavaScript中的内存管理是自动的
- 对象不再被引用时是垃圾
- 对象从根上访问不到时是垃圾
JavaScript中的可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JavaScript中的根就可以理解为全局变量对象
GC算法介绍
GC定义与作用
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收垃圾
GC里面的垃圾是什么
- 程序中不再需要使用的对象
function func () {
name = 'lagou'
return `${name} is a coder`
}
func()
- 程序中不能再访问到的对象
function func () {
name = 'lagou'
return `${name} is a coder`
}
func()
GC算法是什么
- GC是一种机制,垃圾回收器完成具体的工作
- 工作的内容就是查找垃圾释放空间、回收空间
- 算法就是工作时查找和回收所遵循的规则
常见的GC算法
-
引用计数算法 :
1.核心思想:设置引用数,判断当前索引数是否为0
2.引用计数器
3.引用关系改变时修改引用数字
4.引用数字为0时进行回收
优点:
(1):发现垃圾时立即回收
(2):最大程度减少程序暂停
缺点:
(1):无法回收循环引用的对象
(2):时间开销大 -
标记清除算法
1.核心思想:分标记和清除两个阶段
2.遍历所有对象并标记所有活动对象
3.遍历所有对象并清楚没有标记的对象
4.回收相应的空间
优点:
(1):可回收循环引用的对象
缺点:
(1):无法立即进行回收
(2):容易形成碎片空间,导致内存空间利用率不高 -
标记整理算法
(1):标记整理可以看作是标记清楚的增强
(2):标记阶段的操作和标记清楚一致
(3):清楚阶段会先执行整理,移动对象位置
优点:
(1):可回收循环引用对象
(2):减少 碎片化的空间
缺点:
(1):不会立即回收垃圾对象
(2):移动对象位置,回收效率较慢 -
分代回收算法
认识V8
- V8是一款主流的JavaScript执行引擎
- V8采用即时编译
- V8内存设有上限
垃圾回收策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8中常用的GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 增量标记
V8回收新生代对象
-
内存分配
(1):V8内存空间一分为二
(2):小空间用于存储新生代对象(32M|16M)
(3):新生代指的是存活时间较短的对象 -
新生代对象回收实现
(1):回收过程采用复制算法 + 标记整理
(2):新生代内存区分为两个等大的空间
(3):使用空间为Form,空闲空间为To
(4):活动对象存储在Form空间
(5):标记整理后将活动对象拷贝至To空间
(6):Form与To交换空间完成释放 -
回收细节说明
(1):拷贝过程中可能出现晋升
(2):晋升就是将新生代对象移至老生代
(3):一轮GC还存活的新生代需要晋升
(4):To空间的使用率超过25%后新生代需要晋升
V8回收老年代对象
-
老年代对象说明
(1):老年代对象存放在右侧老生代区域
(2):64位操作系统1.4G ,32位操作系统700M
(3):老年代对象就是存活时间较长的对象 -
老年代对象回收实现
(1):主要标记清楚、标记整理和标记增量算法
(2):首先使用标记清楚完成垃圾空间的回收
(3):采用标记整理进行空间优化
(4):采用增量标记进行效率优化
细节对比
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法
PerFormance 工具介绍
为什么使用PerFormance
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- PerFormance提供多种监控方式
PerFormance使用步骤
- 打开浏览器输入目标网址
- 进入开发人工具面板,选择性能
- 开启录制功能,访问具体界面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
内存问题的外在表现
- 页面出现延迟加载或经常性暂停
- 页面持续性出现糟糕的性能
- 页面的性能随时间延长越来越差
界定内存问题的标准
- 内存泄露:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题
- 频繁垃圾回收:通过内存变化图分析
内存监控的几种方式
- 浏览器任务管理器
- TimeLine时序图记录
- 堆快照查找分离DOM
- 判断是否存在频繁的垃圾回收
什么是分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点
- 分离状态的DOM节点
为什么确定频繁的垃圾回收
- GC工作时应用程序是停止的
- 频繁且过长的垃圾回收会导致引用假死
- 用户使用中感知应用卡顿
V8引擎执行工作流程
- Canner用来扫描
- Parser 解析器进行解析,生成AST(抽象语法树)文件
- Ignition 解释器进行解释,将AST文件转换成(ByteCode)字节码文件
- TurboFan 编译器进行编译,将字节码文件编译成浏览器中可执行的机器码
性能优化
事件委托:
循环添加事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>循环添加事件</title>
</head>
<body>
<button index='1'>按钮1</button>
<button index='2'>按钮2</button>
<button index='3'>按钮3</button>
<script src="./index.js"></script>
</body>
</html>
index.js
var aButtons = document.querySelectorAll('button')
// 基础
for (var i = 0; i < aButtons.length; i++) {
aButtons[i].onclick = function () {
console.log(`当前索引值为${i}`)
}
}
//事件委托
document.body.onclick = function (ev) {
var target = ev.target,
targetDom = target.tagName
if (targetDom === 'BUTTON') {
var index = target.getAttribute('index')
console.log(`当前点击的是第 ${index} 个`)
}
}
变量局部化:
- 这样可以提高代码的执行效率( 减少了数据访问时需要查找的路径 )
//普通
var i, str = ""
function packageDom() {
for (i = 0; i < 1000; i++) {
str += i
}
}
packageDom()
//变量局部化
function packageDom() {
let str = ''
for (let i = 0; i < 1000; i++) {
str += i
}
}
packageDom()
减少层级访问:
- 访问数据时,层级越深执行速度越慢
缓存数据:
- 减少声明和语句数(词法 语法)
- 缓存数据(作用域链查找变快)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>缓存数据</title>
</head>
<body>
<div id="skip" class="skip"></div>
<script>
// 缓存数据:对于需要多次使用的数据进行提前保存,后续进行使用
var oBox = document.getElementById('skip')
// 假设在当前的函数体当中需要对 className 的值进行多次使用,那么我们就可以将它提前缓存起来
function hasClassName(ele, cls) {
console.log(ele.className)
return ele.className == cls
}
console.log(hasClassName(oBox, 'skip'))
function hasClassName(ele, cls) {
var clsName = ele.className
console.log(clsName)
return clsName == cls
}
console.log(hasClassName(oBox, 'skip'))
/*
01 减少声明和语句数(词法 语法)
02 缓存数据(作用域链查找变快)
*/
</script>
</body>
</html>
防抖:
- 在一些高频率事件触发的场景下我们不希望对应的事件处理函数多次执行
- 对于这个高频的操作来说,我们只希望识别一次点击,可以人为是第一次或者是最后一次
实现代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖函数实现</title>
</head>
<body>
<button id="btn">点击</button>
<script>
var oBtn = document.getElementById('btn')
// oBtn.onclick = function () {
// console.log('点击了')
// }
/**
* handle 最终需要执行的事件监听
* wait 事件触发之后多久开始执行
* immediate 控制执行第一次还是最后一次,false 执行最后一次
*/
function myDebounce(handle, wait, immediate) {
// 参数类型判断及默认值处理
if (typeof handle !== 'function') throw new Error('handle must be an 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(ev) {
console.log('点击了1111', this, ev)
}
// 当我们执行了按钮点击之后就会执行...返回的 proxy
oBtn.onclick = myDebounce(btnClick, 200, false)
// oBtn.onclick = btnClick() // this ev
</script>
</body>
</html>
节流:
- 对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按着我们定
义的频率减少触发的次数
实现代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>节流函数实现</title>
<style>
body {
height: 5000px;
}
</style>
</head>
<body>
<script>
// 节流:我们这里的节流指的就是在自定义的一段时间内让事件进行触发
function myThrottle(handle, wait) {
if (typeof handle !== 'function') throw new Error('handle must be an function')
if (typeof wait === 'undefined') wait = 400
let previous = 0 // 定义变量记录上一次执行时的时间
let timer = null // 用它来管理定时器
return function proxy(...args) {
let now = new Date() // 定义变量记录当前次执行的时刻时间点
let self = this
let interval = wait - (now - previous)
if (interval <= 0) {
// 此时就说明是一个非高频次操作,可以执行 handle
clearTimeout(timer)
timer = null
handle.call(self, ...args)
previous = new Date()
} else if (!timer) {
// 当我们发现当前系统中有一个定时器了,就意味着我们不需要再开启定时器
// 此时就说明这次的操作发生在了我们定义的频次时间范围内,那就不应该执行 handle
// 这个时候我们就可以自定义一个定时器,让 handle 在 interval 之后去执行
timer = setTimeout(() => {
clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是 timer 中的值还在
timer = null
handle.call(self, ...args)
previous = new Date()
}, interval)
}
}
}
// 定义滚动事件监听
function scrollFn() {
console.log('滚动了')
}
// window.onscroll = scrollFn
window.onscroll = myThrottle(scrollFn, 600)
</script>
</body>
</html>
网友评论