网站免费申请建站,网站建设 物流,创新驱动发展战略的意义,宿松网站建设设计前言
计算机基本组成#xff1a; 我们编写的软件首先读取到内存#xff0c;用于提供给 CPU 进行运算处理。 内存的读取和释放#xff0c;决定了程序性能。 冯诺依曼结构
解释和编译
这两个概念怎么理解呢。
编译相当于事先已经完成了可以直接用。好比去饭店吃饭点完上…前言
计算机基本组成 我们编写的软件首先读取到内存用于提供给 CPU 进行运算处理。 内存的读取和释放决定了程序性能。 冯·诺依曼结构
解释和编译
这两个概念怎么理解呢。
编译相当于事先已经完成了可以直接用。好比去饭店吃饭点完上菜就可以吃不需要自己做解释呢就好比你要吃饭就得煮火锅一边煮一边吃
JavaScript 属于解释型语言它需要在代码执行时将代码编译为机器语言。 astabstract struct tree抽象语法树
Interpreter 逐行读取代码并立即执行。Compiler 读取您的整个代码进行一些优化然后生成优化后的代码。
function add(a, b) { return ab}
for(let i 0; i 1000; i) { add(1 1)}上面的示例循环调用了 add 函数1000次该函数将两个数字相加并返回总和。
Interpreter 接收上面的代码后它将逐行读取并立即执行代码直到循环结束。 它的工作仅仅是实时地将代码转换为我们的计算机可以理解的内容。如果这段代码受者是 Compiler它会先完整地读取整个程序对我们要执行的代码进行分析并生成电脑可以读懂的机器语言。过程如同获取 X我们的JS文件并生成 Y机器语言一样。如果我们使用 Interpreter 执行 Y则会获得与执行 X 相同的结果。 从上图中可以看出ByteCode 只是中间码计算机仍需要对其进行翻译才能执行。 但是 Interpreter 和 Compiler 都将源代码转换为机器语言它们唯一的区别在于转换的过程不尽相同。
Interpreter 逐行将源代码转换为等效的机器代码。Compiler 在一开始就将所有源代码转换为机器代码。
JavaScript 引擎
JavaScript 其实有众多引擎只不过 v8 是我们最为熟知的。
V8 (Google)用 C编写开放源代码由 Google 丹麦开发是 Google Chrome 的一部分也用于 Node.js。JavaScriptCore (Apple)开放源代码用于 webkit 型浏览器如 Safari 2008 年实现了编译器和字节码解释器升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。Rhino由 Mozilla 基金会管理开放源代码完全以 Java 编写用于 HTMLUnitSpiderMonkey (Mozilla)第一款 JavaScript 引擎早期用于 Netscape Navigator现时用于 Mozilla Firefox。
NodeJs 大体架构 谷歌的 Chrome 使用 V8Safari 使用 JavaScriptCoreFirefox 使用 SpiderMonkey。 简单看一下 V8 的处理过程。
始于从网络中获取 JavaScript 代码。V8 解析源代码并将其转化为抽象语法树AST。基于该 ASTIgnition 解释器可以开始做它的事情并产生字节码。在这一点上引擎开始运行代码并收集类型反馈。为了使它运行得更快字节码可以和反馈数据一起被发送到优化编译器。优化编译器在此基础上做出某些假设然后产生高度优化的机器代码。如果在某些时候其中一个假设被证明是不正确的优化编译器就会取消优化并回到解释器中。
traverse(ast, {FunctionDeclaration: function(path) {path.node.id.name x;},VariableDeclaration: function(path) {// 匹配map 映射}
});垃圾回收 GC 即 Garbage Collection 程序工作过程中会产生很多 垃圾这些垃圾是程序不用的内存或者是之前用过了以后不会再用的内存空间而 GC 就是负责回收垃圾的因为他工作在引擎内部所以对于我们前端来说GC 过程是相对比较无感的这一套引擎执行而对我们又相对无感的操作也就是常说的 垃圾回收机制 了 当然也不是所有语言都有 GC一般的高级语言里面会自带 GC比如 Java、Python、JavaScript 等也有无 GC 的语言比如 C、C 等那这种就需要我们程序员手动管理内存了相对比较麻烦
上面提到了内容存储的方式变量会在栈中存储对象在堆中存储例如
let a {name: heyi};
a [1, 2, 3, 4, 5];我们知道写代码时创建一个基本类型、对象、函数……都是需要占用内存的但是我们并不关注这些因为这是引擎为我们分配的我们不需要显式手动的去分配内存那么 JavaScript 引擎是如何发现并清理垃圾的呢
可达性分析算法 - 引用计数法
相信这个算法大家都很熟悉也经常听说。 它的策略是跟踪记录每个变量值被使用的次数
当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1如果同一个值又被赋给另一个变量那么引用数加 1如果该变量的值被其他的值覆盖了则引用次数减 1当这个值的引用次数变为 0 的时候说明没有变量在使用这个值没法被访问了回收空间垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存 这个算法最怕的就是循环应用还有比如 JavaScript 中不恰当的闭包写法。
function test(){let A new Object() 1 1 2 -1 1let B new Object() 1 1 2 -1 1A.b BB.a AA nullB null
}优点 引用计数算法的优点我们对比标记清除来看就会清晰很多首先引用计数在引用值为 0 时也就是在变成垃圾的那一刻就会被回收所以它可以立即回收垃圾 而标记清除算法需要每隔一段时间进行一次那在应用程序JS脚本运行过程中线程就必须要暂停去执行一段时间的 GC另外标记清除算法需要遍历堆里的活动以及非活动对象来清除而引用计数则只需要在引用时计数就可以了
缺点 引用计数的缺点想必大家也都很明朗了首先它需要一个计数器而此计数器需要占很大的位置因为我们也不知道被引用数量的上限还有就是无法解决循环引用无法回收的问题这也是最严重的
标记清除mark-sweep算法
标记清除Mark-Sweep目前在 JavaScript引擎 里这种算法是最常用的到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法各大浏览器厂商还对此算法进行了优化加工且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。 此算法分为 标记 和 清除 两个阶段标记阶段即为所有活动对象做上标记清除阶段则把没有标记也就是非活动对象销毁 此算法分为 标记 和 清除 两个阶段标记阶段即为所有活动对象做上标记清除阶段则把没有标记也就是非活动对象销毁
当变量进入执行环境时反转某一位通过一个二进制字符来表示标记又或者可以维护进入环境变量和离开环境变量这样两个列表可以自由的把变量从一个列表转移到另一个列表。 引擎在执行 GC使用标记清除算法时需要从出发点去遍历内存中所有的对象去打标记而这个出发点有很多我们称之为一组 根 对象而所谓的根对象其实在浏览器环境中包括又不止于 全局Window对象、文档DOM树
整个标记清除算法大致过程就像下面这样
垃圾收集器在运行时会给内存中的所有变量都加上一个标记假设内存中所有对象都是垃圾全标记为0然后从各个根对象开始遍历把不是垃圾的节点改成1清理所有标记为0的垃圾销毁并回收它们所占用的内存空间最后把所有内存中对象标记修改为0等待下一轮垃圾回收
优点 标记清除算法的优点只有一个那就是实现比较简单打标记也无非打与不打两种情况这使得一位二进制位0和1就可以为其标记非常简单
缺点 标记清除算法有一个很大的缺点就是在清除之后剩余的对象内存位置是不变的也会导致空闲内存空间是不连续的出现了 内存碎片如下图并且由于剩余空闲内存不是一整块它是由不同大小内存组成的内存列表这就牵扯出了内存分配的问题 假设我们新建对象分配内存时需要大小为 size由于空闲内存是间断的、不连续的则需要对空闲内存列表进行一次单向遍历找出大于等于 size 的块才能为其分配如下图 那如何找到合适的块呢我们可以采取下面三种分配策略
First-fit找到大于等于 size 的块立即返回Best-fit遍历整个空闲列表返回大于等于 size 的最小分块Worst-fit遍历整个空闲列表找到最大的分块然后切成两部分一部分 size 大小并将该部分返回 这三种策略里面 Worst-fit 的空间利用率看起来是最合理但实际上切分之后会造成更多的小块形成内存碎片所以不推荐使用对于 First-fit 和 Best-fit 来说考虑到分配的速度和效率 First-fit 是更为明智的选择 综上所述标记清除算法或者说策略就有两个很明显的缺点内存碎片化空闲内存块是不连续的容易出现很多空闲内存块还可能会出现分配所需内存过大的对象时找不到合适的块分配速度慢因为即便是使用 First-fit 策略其操作仍是一个 O(n) 的操作最坏情况是每次都要遍历到最后同时因为碎片化大对象的分配效率会更慢
归根结底标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续所以只要解决这一点两个缺点都可以完美解决了
而 标记整理Mark-Compact算法 就可以有效地解决它的标记阶段和标记清除算法没有什么不同只是标记结束后标记整理算法会将活着的对象即不需要清理的对象向内存的一端移动最后清理掉边界的内存
内存管理
以上给大家介绍了常用的一些垃圾回收算法在不同语言中对于垃圾回收的处理略有差异。 V8 引擎对内存这块做了深度优化我们接下来详细介绍。 V8 的垃圾回收策略主要基于分代式垃圾回收机制V8 中将堆内存分为新生代和老生代两区域采用不同的垃圾回收器也就是不同的策略管理垃圾回收 新生代 当新加入对象时它们会被存储在使用区。然而当使用区快要被写满时垃圾清理操作就需要执行。在开始垃圾回收之前新生代垃圾回收器会对使用区中的活动对象进行标记。标记完成后活动对象将会被复制到空闲区并进行排序。然后垃圾清理阶段开始即将非活动对象占用的空间清理掉。最后进行角色互换将原来的使用区变成空闲区将原来的空闲区变成使用区。
如果一个对象经过多次复制后依然存活那么它将被认为是生命周期较长的对象且会被移动到老生代中进行管理。除此之外还有一种情况如果复制一个对象到空闲区时空闲区的空间占用超过了25%那么这个对象会被直接晋升到老生代空间中。25%比例的设置是为了避免影响后续内存分配因为当按照 Scavenge 算法回收完成后空闲区将翻转成使用区继续进行对象内存分配。 这是 V8 引擎存储对象Object和动态数据Dynamic Data的地方。这也是程序对于内存区域中最大的一块地方同时垃圾回收 GC 也发生在这里。并不是整个 Heap 堆内存都进行垃圾回收只有新空间New Space和旧空间Old Space由垃圾回收管理。 新空间New Space是新对象存活的地方这些对象的生命周期都很短。这个空间很小由两个 Semi-Space 组成类似 JVM 中的 S0 和 S1。 我们将会在后面的内容看到它。新空间New Space的大小是由两个 V8 中的标志位来控制 min_semi_space_size(Initial) 和 max_semi_space_size(Max) 。 旧空间Old Space在新空间New Space中存活了两个 minor GC 周期的对象会被迁移到这里。 这个空间由 Major GC(Mark-Sweep Mark-Compact) 管理。我们也会在后面内容中看到它。旧空间Old Space的大小也是由两个 V8 中的标志位来控制nitial_old_space_size(Initial) 和 max_old_space_size(Max) 。 旧空间Old Space被分成两个部分 旧指针空间Old pointer space这些存活下来的的对象都包含了指向其他对象的指针。旧数据空间Old data space这些对象只包含数据没有指向其他对象的指针。在新空间New Space中存活两个 minor GC 周期后String已经装箱的数字未装箱的双精度数组会被迁移到这里。 大型对象空间Large object space大于其他空间大小限制的对象存放在这里。每个对象都有自己的内存区域这里的对象不会被垃圾回收器移动。 代码空间Code-space这是即时编译器JIT存储已经编译的代码块的地方。这是唯一可执行内存的空间尽管代码可能被分配到大型对象空间Large object space那也是可以执行的。 单元空间Cell Space属性单元空间Property Cell Space和映射空间Map Space这些空间分别存放 CellPropertyCell 和 Map。这些空间包含的对象大小相同并且对对象类型有些限制可以简化回收工作。 每个空间除了大型对象空间Large object space都由一组 Page 组成。一个 page 是由操作系统分配的一个连续内存块大小为 1MB。
老生代
不同于新生代老生代中存储的内容是相对使用频繁并且短时间无需清理回收的内容。这部分我们可以使用标记整理进行处理。 从一组根元素开始递归遍历这组根元素遍历过程中能到达的元素称为活动对象没有到达的元素就可以判断为非活动对象 清除阶段老生代垃圾回收器会直接将非活动对象进行清除。
并行回收
为了减少主线程阻塞我们在进行 GC 处理时使用辅助进程。
全停顿标记
这个概念看字眼好像不好理解其实如果用前端开发的术语来解释就是阻塞。 虽然我们的 GC 操作被放到了主进程与子进程中去处理但最终的结果还是主进程被较长时间占用。
切片
增量就是将一次 GC 标记的过程分成了很多小步每执行完一小步就让应用逻辑执行一会儿这样交替多次后完成一轮 GC 标记如下图
三色标记
三色白灰黑。相信大家都知道灰色地带这个词吧 我们这里的会表示的是一个中间状态为什么会有这个中间状态呢
白色指的是未被标记的对象灰色指自身被标记成员变量该对象的引用对象未被标记黑色指自身和成员变量皆被标记
写屏障(增量中修改引用)
这一机制用于处理在增量标记进行时修改引用的处理可自行修改为灰色
惰性清理
增量标记只是用于标记活动对象和非活动对象真正的清理释放内存则 V8 采用的是惰性清理(Lazy Sweeping)方案。 在增量标记完成后进行清理。当增量标记完成后假如当前的可用内存足以让我们快速的执行代码其实我们是没必要立即清理内存的可以将清理过程稍微延迟一下让 JavaScript 脚本代码先执行也无需一次性清理完所有非活动对象内存可以按需逐一进行清理直到所有的非活动对象内存都清理完毕。
并发回收
还记得 react 中的 Concurrent 吗 我们想想 React 演进过程是不是就会觉得从并行到并发的演进变得很合了呢 并发挥收其实是更进一步的切片几乎完全不阻塞主进程。
总结
分代式机制把一些新、小、存活时间短的对象作为新生代采用一小块内存频率较高的快速清理而一些大、老、存活时间长的对象作为老生代使其很少接受检查新老生代的回收机制及频率是不同的可以说此机制的出现很大程度提高了垃圾回收机制的效率
怎么理解内存泄漏
怎么解决内存泄漏代码层面如何优化
减少查找
var i, str
function packageDomGlobal() {for(i 0; i 1000; i) {str i}
}// 第二种情况。我们采用局部变量来保存相关数据
function packageDomLocal() {let str for(let i 0; i 1000; i) {str i}
}减少变量声明
// 第一种情况循环体中没有抽离出值不变的数据
var test () {let arr [czs, 25, I love FrontEnd];for(let i 0; i arr.length; i){console.log(arr[i]);}
}// 第二种情况循环体中抽离出值不变的数据
var test () {let arr [czs, 25, I love FrontEnd];const length arr.length;for(let i 0; i length; i){console.log(arr[i]);}
}使用 Performance Memory 分析内存与性能setTimeout、setInterval 需要及时清除事件绑定的清除
运行机制
浏览器主进程
协调控制其他子进程创建、销毁浏览器界面显示用户交互前进、后退、收藏将渲染进程得到的内存中的Bitmap绘制到用户界面上存储功能等 第三方插件进程每种类型的插件对应一个进程仅当使用该插件时才创建 GPU进程用于3D绘制等 渲染进程就是我们说的浏览器内核排版引擎Blink和JavaScript引擎V8都是运行在该进程中将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页负责页面渲染脚本执行事件处理等每个tab页一个渲染进程出于安全考虑渲染进程都是运行在沙箱模式下 网络进程负责页面的网络资源加载之前作为一个模块运行在浏览器主进程里面最近才独立成为一个单独的进程
浏览器事件循环 宏任务
可以将每次执行栈执行的代码当做是一个宏任务
I/OsetTimeoutsetIntervalsetImmediaterequestAnimationFrame
微任务
当宏任务执行完会在渲染前将执行期间所产生的所有微任务都执行完
process.nextTickMutationObserverPromise.then catch finally
整体流程
执行一个宏任务栈中没有就从事件队列中获取执行过程中如果遇到微任务就将它添加到微任务的任务队列中宏任务执行完毕后立即执行当前微任务队列中的所有微任务依次执行当前宏任务执行完毕开始检查渲染然后GUI线程接管渲染渲染完毕后JS线程继续接管开始下一个宏任务从事件队列中获取
浏览器事件循环
console.log(1);queueMicrotask(() {console.log(2)});Promise.resolve().then(() console.log(3));setTimeout(() {console.log(4)})问上面的打印顺序是怎么样的 首先任务js 主进程的内容先执行js 常规的代码 1, 2 为微任务3 微任务4 宏任务
执行同步代码。执行一个宏任务执行栈中没有就从任务队列中获取。执行过程中如果遇到微任务就将它添加到微任务的任务队列中。宏任务执行完毕后立即执行当前微任务队列中的所有微任务依次执行。当前宏任务执行完毕开始检查渲染然后渲染线程接管进行渲染。渲染完毕后JavaScript 线程继续接管开始下一个循环。 宏任务-微任务-渲染如果需要渲染比如到了时间或有更改 dom而不是又执行宏任务 call stack vs task queue
console.log(1);setTimeout(() console.log(2));Promise.resolve().then(() console.log(3));Promise.resolve().then(() setTimeout(() console.log(4)));Promise.resolve().then(() console.log(5));setTimeout(() console.log(6));console.log(7);// 结果
/*
1 7 3 5 2 6 4
*/再来一道
Promise.resolve().then(() {// 微任务1console.log(Promise1)setTimeout(() {// 宏任务2console.log(setTimeout2)}, 0)
})
setTimeout(() {// 宏任务1console.log(setTimeout1)Promise.resolve().then(() {// 微任务2console.log(Promise2)})
}, 0)// p1 s1 p2 s2具体执行流程分析 主线程执行
首先Promise.resolve() 立即解决然后调用 .then() 方法将回调函数微任务1放入微任务队列。接着执行 setTimeout宏任务1将回调函数放入宏任务队列设置延迟时间为 0。主线程执行完毕后开始执行微任务队列中的微任务1执行 .then() 方法的回调函数输出 Promise1。微任务1执行完毕后主线程再次清空此时宏任务队列中有两个任务宏任务1和宏任务2宏任务队列是先进先出FIFO的所以首先执行宏任务1。在宏任务1中输出 setTimeout1然后立即将一个新的 - -Promise.resolve().then() 的回调函数微任务2放入微任务队列。宏任务1执行完毕后主线程再次清空此时微任务队列中有一个任务微任务2执行微任务2输出 Promise2。微任务2执行完毕后主线程再次清空此时宏任务队列中只剩下宏任务2执行宏任务2输出 setTimeout2。
来一道更复杂些的题目
console.log(stack [1]);
setTimeout(() console.log(macro [2]), 0);
setTimeout(() console.log(macro [3]), 1);const p Promise.resolve();
for(let i 0; i 3; i) p.then(() {setTimeout(() {console.log(stack [4])setTimeout(() console.log(macro [5]), 0);p.then(() console.log(micro [6]));}, 0);console.log(stack [7]);
});console.log(macro [8]);// 请说出答案
/* Result:
stack [1]
macro [8]stack [7], stack [7], stack [7]macro [2]
macro [3]stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]macro [5], macro [5], macro [5]
--------------------
but in node in versions 11 (older versions) you will get something differentstack [1]
macro [8]stack [7], stack [7], stack [7]macro [2]
macro [3]stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]macro [5], macro [5], macro [5]more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/**const $inner document.querySelector(#inner)
const $outer document.querySelector(#outer)function handler () {console.log(click) // 直接输出Promise.resolve().then(_ console.log(promise)) // 注册微任务setTimeout(() console.log(timeout)) // 注册宏任务requestAnimationFrame(_ console.log(animationFrame)) // 注册宏任务$outer.setAttribute(data-random, Math.random()) // DOM属性修改触发微任务
}new MutationObserver(_ {console.log(observer)
}).observe($outer, {attributes: true
})$inner.addEventListener(click, handler)
$outer.addEventListener(click, handler)**以上执行的顺序click - promise - observer - click - promise - observer - animationFrame - animationFrame - timeout - timeout
Node事件循环机制 伪代码运行
while (true){// 1. Get one macrotask (oldest) task itemtask macroTaskQueue.pop(); execute(task);// 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration)while (microtaskQueue.hasTasks()){const microTask microtaskQueue.pop();execute(microTask);}// 3. If 16ms have elapsed since last time this condition was trueif (isPaintTime()){// 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration) const animationTasks animationQueue.getTasks();for (task in animationTasks){execute(task);}repaint(); // render the page changes (via the render pipeline)}
}