js運行機制、js內存、v8回收機制

js運行機制

進程與線程

進程是cpu資源分配的最小單位,進程可以包含多個線程。 瀏覽器就是多進程的,每打開的一個瀏覽器窗口就是一個進程。

線程是cpu調度的最小單位,同一進程下的各個線程之間共享程序的內存空間。

可以把進程看做一個倉庫,線程是可以運輸的貨車,每個倉庫有屬於自己的多輛貨車爲倉庫服務(運貨),每個倉庫可以同時由多輛車同時拉貨,但是每輛車同一時間只能幹一件事,就是運輸本次的貨物。

渲染進程

瀏覽器包括4個進程:

  • 主進程(Browser進程),瀏覽器只有一個主進程,負責資源下載,界面展示等主要基礎功能
  • GPU進程,負責3D圖示繪製
  • 第三方插件進程,負責第三方插件處理
  • 渲染進程(Renderer進程),負責js執行,頁面渲染等功能,渲染進程主要包括GUI渲染線程、Js引擎線程、事件循環線程、定時器線程、http異步線程。
GUI渲染線程

先看看瀏覽器得到一個網站資源後幹了哪些事:

  • 首先瀏覽器會解析html代碼(實際上html代碼本質是字符串)轉化爲瀏覽器認識的節點,生成DOM樹,也就是DOM Tree
  • 然後解析css,生成CSSOM(CSS規則樹)
  • 把DOM Tree 和CSSOM結合,生成Rendering Tree(渲染樹)

GUI就是來幹這個事情的,如果修改了一些元素的顏色或者背景色,頁面就會重繪(Repaint),如果修改元素的尺寸,頁面就會迴流(Reflow),當頁面需要Repaing和Reflow時GUI多會執行,進行頁面繪製。

這裏提示一點:Reflow比Repaint的成本更高

JS引擎線程

js引擎線程就是js內核,負責解析與執行js代碼,也稱爲主線程。瀏覽器同時只能有一個JS引擎線程在運行JS程序,所以js是單線程運行的。

需要注意的是,js引擎線程和GUI渲染線程同時只能有一個工作,js引擎線程會阻塞GUI渲染線程,在瀏覽器渲染的時候遇到script標籤,就會停止GUI的渲染,然後js引擎線程開始工作,執行裏面的js代碼,等js執行完畢,js引擎線程停止工作,GUI繼續渲染下面的內容。所以如果js執行時間太長就會造成頁面卡頓的情況

事件循環線程

事件循環線程用來管理控制事件循環,並且管理着一個事件隊列(task queue),當js執行碰到事件綁定和一些異步操作時,會把對應的事件添加到對應的線程中(比如定時器操作,便把定時器事件添加到定時器線程),等異步事件有了結果,便把他們的回調操作添加到事件隊列,等待js引擎線程空閒時來處理。

定時器線程

由於js是單線程運行,所以不能抽出時間來計時,只能另開闢一個線程來處理定時器任務,等計時完成,把定時器要執行的操作添加到事件任務隊列,等待js引擎線程來處理。這個線程就是定時器線程。

異步請求線程

當執行到一個http異步請求時,便把異步請求事件添加到異步請求線程,等收到響應(準確來說應該是http狀態變化),把回調函數添加到事件隊列,等待js引擎線程來執行。

Event Loop

上面介紹了渲染進程中的5個主要的線程,下面就從Event Loop的角度來聊一聊他們之間是怎樣合作的。

  • 當javascript代碼執行的時候會將不同的變量存於內存中的不同位置:堆(heap)和棧(stack)中來加以區分。其中,堆裏存放着一些對象。而棧中則存放着一些基礎類型變量以及對象的指針。 但是我們這裏說的執行棧和上面這個棧的意義卻有些不同。

  • 我們知道,當每次調用一個方法的時候,js會生成一個與這個方法對應的執行環境(context),又叫執行上下文這個執行環境中存在着這個方法的私有作用域,上層作用域的指向,方法的參數,這個作用域中定義的變量以及這個作用域的this對象。 而當一系列方法被依次調用的時候,因爲js是單線程的,同一時間只能執行一個方法,於是這些方法被排隊在一個單獨的地方。這個地方被稱爲執行棧
    在這裏插入圖片描述

  • 當一個腳本第一次執行的時候,js引擎會解析這段代碼,並將其中的同步代碼按照執行順序加入執行棧中,然後從頭開始執行。如果當前執行的是一個方法,那麼js會向執行棧中添加這個方法的執行環境,然後進入這個執行環境繼續執行其中的代碼。當這個執行環境中的代碼 執行完畢並返回結果後,js會退出這個執行環境並把這個執行環境銷燬,相應的棧內存中的內存也會銷燬,回到上一個方法的執行環境。這個過程反覆進行,直到執行棧中的代碼全部執行完畢。

  • 一個方法執行會向執行棧中加入這個方法的執行環境,在這個執行環境中還可以調用其他方法,甚至是自己,其結果不過是在執行棧中再添加一個執行環境。這個過程可以是無限進行下去的,除非發生了棧溢出,即超過了所能使用內存的最大值。

以上的過程說的都是同步代碼的執行。那麼當一個異步代碼(如發送ajax請求數據)執行後會如何呢?js的特點是非阻塞,實現這一點的關鍵在於下面要說的這項機制——事件隊列(Task Queue)。

  • js引擎遇到一個異步事件後並不會一直等待其返回結果,而是會將這個事件掛起(比如:定時操作在定時器線程上;http請求則在異步請求線程上處理),繼續執行執行棧中的其他任務。當一個異步事件返回結果後,js會將這個事件加入與當前執行棧不同的另一個隊列,我們稱之爲事件隊列。被放入事件隊列不會立刻執行其回調,而是等待當前執行棧中的所有任務都執行完畢, 主線程處於閒置狀態時,主線程會去查找事件隊列是否有任務。如果有,那麼主線程會從中取出排在第一位的事件,並把這個事件對應的回調放入執行棧中,然後執行其中的同步代碼…,如此反覆,這樣就形成了一個無限的循環。這就是這個過程被稱爲事件循環(Event Loop)
    在這裏插入圖片描述
    圖中的stack表示我們所說的執行棧,web apis則是代表異步事件,callback queue即事件隊列。
console.log(1)
setTimeout(()=>{
	console.log(4)
},2000)
setTimeout(()=>{
	console.log(3)
},1000)
console.log(2)
// 1 2   1秒後打印3   再等1秒打印4
  • 以上的事件循環過程是一個宏觀的表述,實際上因爲異步任務之間並不相同,因此他們的執行優先級也有區別。不同的異步任務被分爲兩類:微任務(micro task)和宏任務(macro task)。

宏任務(macrotask)::

script(整體代碼)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 環境)

微任務(microtask):

Promise、 MutaionObserver、process.nextTick(Node.js環境)、MutationObserver(html5新特性)

  • 前面我們介紹過,在一個事件循環中,異步事件返回結果後會被放到一個任務隊列中。然而,根據這個異步事件的類型,這個事件會被放在對應的宏任務隊列或者微任務隊列中去。並且在執行棧爲空的時候,主線程會先查看微任務隊列是否有事件存在。如果不存在,那麼再去宏任務隊列中取出一個事件並把對應的回到加入當前執行棧;如果存在,則會依次執行隊列中事件對應的回調,直到微任務隊列爲空,然後去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧…如此反覆,進入循環。
console.log(1)
setTimeout(()=>{
	console.log(5)
	new Promise(function(resolve,reject){
	    console.log(6)
	    resolve()
	}).then(function(){
	    console.log(7);
	})
})
new Promise(function(resolve,reject){
    console.log(2)
    resolve()
}).then(function(){
    console.log(4);
})
setTimeout(()=>{
	console.log(8)
})
console.log(3)
//瀏覽器的結果 1 2 3 4 5 6 7 8 
//node中的結果 1 2 3 4 5 6 8 7

js內存

  • 全局執行上下文:只有一個,瀏覽器中的全局對象就是 window 對象,this 指向這個全局對象。

  • 函數執行上下文:存在無數個,只有在函數被調用的時候纔會被創建,每次調用函數都會創建一個新的執行上下文。

  • Eval 函數執行上下文: 指的是運行在 eval 函數中的代碼,很少用而且不建議使用。

棧數據結構(stack):存放基本類型與引用類型的地址。

可以通過類比乒乓球盒子來分析,處於盒子中最頂層的乒乓球,它一定是最後被放進去,但可以最先被使用。而我們想要使用底層的乒乓球,就必須將上面的乒乓球取出來,讓底層的乒乓球處於盒子頂層。這就是棧空間先進後出,後進先出的特點,棧底永遠都是全局上下文,而棧頂就是當前正在執行的上下文。

堆數據結構(heap):引用類型

堆數據結構是一種樹狀結構。它的存取數據的方式,則與書架與書非常相似。書雖然也整齊的存放在書架上,但是我們只要知道書的名字,就可以很方便的取出我們想要的書,好比在JSON格式的數據中,我們存儲的key-value是可以無序的,因爲順序的不同並不影響我們的使用,我們只需要關心書的名字。

隊列數據結構

隊列是一種先進先出(FIFO)的數據結構。正如排隊過安檢一樣,排在隊伍前面的人一定是最先過檢的人

棧與堆存放數據區別

棧內存:Undefined、不是new出來的布爾、數字和字符串,它們都是直接按值存儲在棧中的,每種類型的數據佔用的內存空間的大小是確定的,並由系統自動分配和自動釋放。這樣帶來的好處就是,內存可以及時得到回收,相對於堆來說,更加容易管理內存空間。

堆內存:引用類型的數據,如對象、數組、函數、Null等(typeof null 爲 object),它們是通過拷貝和new出來的,這樣數據的地址指針是存儲於棧中的,當我們想要訪問引用類型的值的時候,需要先從棧中獲得對象的地址指針,然後,在通過地址指針找到堆中的所需要的數據。

在這裏插入圖片描述


var a = {n: 1};
var b = a;
a.x = a = {n: 2};

a.x 	// 這時 a.x 的值是undefined
b.x 	// 這時 b.x 的值是{n: 2}

在這裏插入圖片描述

回收機制

JavaScript擁有自動的垃圾回收機制,關於垃圾回收機制,有一個重要的行爲,那就是,當一個值,在內存中失去引用時,js通過特定的算法來找到哪些對象是不再繼續使用的(引用計數:現代瀏覽器不再使用,
標記清除:常用),使用a = null其實僅僅只是做了一個釋放引用的操作,讓 a 原本對應的值失去引用,脫離執行環境,這個值會在下一次垃圾收集器執行操作時被找到並釋放。

[堆內存]
var o ={};當前對象對應的堆內存被變量o佔用着呢,堆內存是無法銷燬的。
o = null;null空對象指針,此時上一次的堆內存就沒有被佔用了。瀏覽器會在空閒時間把沒有被佔用的堆內存自動釋放(銷燬/回收)

[棧內存]
函數執行形成棧內存(執行上下文),函數執行完,生命週期結束,那麼該函數的執行上下文就會失去引用,其佔用的內存空間很快就會被垃圾回收器釋放。可是閉包的存在,會阻止這一過程。(執行上下文(A),以及在該執行上下文中創建的函數(B)。當B執行時,如果訪問了A中變量對象中的值,那麼閉包就會產生。)

var fn = null;
function foo() {
	 var a = 2;
	 function innnerFoo() {
	   console.log(a);
	 }
	 fn = innnerFoo; // 將 innnerFoo的引用,賦值給全局變量中的fn
}
function bar() {
	fn(); // 此處的保留的innerFoo的引用
}
foo();
bar(); // 2

全局作用域在加載頁面的時候執行,在關掉頁面的時候銷燬;
使用 Node 提供的 process.memoryUsage 方法可以查看內存使用。

console.log(process.memoryUsage());
// 輸出
// { 
//   rss: 19656704,		// resident set size,所有內存佔用,包括指令區和堆棧
//   heapTotal: 6537216,   // "堆"佔用的內存,包括用到的和沒用到的
//   heapUsed: 3842960,	// 用到的堆的部分
//   external: 8272 		// V8 引擎內部的 C++ 對象佔用的內存
// }

V8回收機制

  • v8限制用戶只能使用部分內存(64位約爲1.4GB,32位約爲0.7GB)

  • 原因:v8執行垃圾回收時會阻斷js運行,以1.5GB的垃圾回收堆內存爲例,v8做一次小的垃圾回收需要50ms以上,一次垃圾回收甚至要1秒以上。

  • 在自動垃圾回收的演變過程中,沒有固定一種回收算法能勝任所有場景,V8採取分代式(新生代和老生代),可以對不同的代進行不能的處理,以提高效率。

堆空間:在這裏插入圖片描述

新生代 :主要採用Scavenge算法
  1. 過程:先在新生代區,將堆內存對半分,一半處於使用(即From區),另一半處於閒置(即To區),平時在From區進行操作。到了要回收的時候,檢測From區的對象,存活的對象複製到To區,然後From區被釋放了,之後對To區和From區調換名字,繼續重複之前操作
  2. 缺點:只能使用一半空間,耗空間
  3. 優點:只需要複製少部分存活的對象,因爲生命週期短的對象中存活的比較少,節省時間,典型的犧牲空間換時間

注意:當一個對象被複制多次依然存活,則被晉升到老生代,進行管理
(晉升條件:經歷過Scavenge算法或To區使用超出25%)

老生代算法: 標記清除算法 搭配 標記整理算法
  1. 標記清除:標記清除算法,先將存活的對象進行標記,清除時只清除沒有標記的(老生代中都是生命週期長的對象,剛好死亡的對象少),不過此時會出現內存不連續的情況。但是,如果此時需要放一個大對象,則放不下,從而導致再次引起回收。然而這次回收是不必要的。在這裏插入圖片描述
  2. 標記整理:先將死亡的對象進行標記,然後將存活得對象往一段移動,移動完成後,清理掉邊界外的內存。在這裏插入圖片描述

V8主要還是使用標記清除,只有在新生代發生晉升,導致老生代分配不出足夠空間時,採用標記整理

以上只是個人理解,不保證正確無疑

發佈了8 篇原創文章 · 獲贊 8 · 訪問量 2336
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章