javascript內存管理(堆和棧)和javascript運行機制


內存基本概念

內存的生命周期:

1、分配所需的內存

2、內存的讀與寫

3、不需要時將其釋放

所有語言的內存生命周期都基本一致,不同的是最后一步在低級語言中很清晰,但是在像JavaScript 等高級語言中,這一步是隱藏的、透明的。

 

js的內存生命周期:

1、定義變量時就完成了內存分配

2、使用值的過程實際上是對分配內存進行讀取與寫入的操作。讀取與寫入可能是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。

3、而內存的釋放而依賴GC機制(高級語言解釋器嵌入的“垃圾回收器”)。

 

程序運行的時候,需要內存空間存放數據。一般來說,系統會划分出兩種不同的內存空間:一種叫做棧(stack),另一種叫做堆(heap)。

堆(heap)與棧(stack)

heap是沒有結構的,數據可以任意存放。heap用於復雜數據類型(引用類型)分配空間,例如數組對象、object對象。

 

 

 stack是有結構的,每個區塊按照一定次序存放(后進先出),stack中主要存放一些基本類型的變量和對象的引用,存在棧中的數據大小與生存期必須是確定的。可以明確知道每個區塊的大小,因此,stack的尋址速度要快於heap。

函數調用形成了一個棧幀。

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7));

當調用bar時,創建了第一個幀 ,幀中包含了bar的參數和局部變量。

 

bar調用foo時,第二個幀就被創建,並被壓到第一個幀之上,幀中包含了foo的參數和局部變量。當foo返回時,最上層的幀就被彈出棧(剩下bar函數的調用幀 )。

bar返回的時候,棧就空了。

 

堆與棧的大小

程序運行時,每個線程分配一個stack,每個進程分配一個heap,也就是說,stack是線程獨占的,heap是線程共用的。此外,stack創建的時候,大小是確定的,數據超過這個大小,就發生stack overflow錯誤,而heap的大小是不確定的,需要的話可以不斷增加。所以這里只看stack的大小限制。下面是一個簡單的測試:

var i=0;
function inc() {
    i++;
    if(i>41909){return;}
    inc();
}
inc();

測試環境是16G內存的電腦,需要注意的是:根據棧的定義可以知道如果 inc 函數里有變量申明的話也是會有內存占用的。

1、谷歌瀏覽器chrome 55.0版本下限制是41909條。

2、IE8瀏覽器下限制是3062條。

 

stack overflow(棧溢出)

因為stack是有限制的,而且stack超出瀏覽器的規定的棧限制時就會報stack overflow。一般情況下不會出現這種情況,因為js語言有他自己的GC機制,而出現這種情況一般是js的死循環或者沒有正確的停止遞歸造成的,可以通過調試去追蹤stack。我還碰到過c++編繹的activx控件,使用事件函數做實時推送時stack overflow。原因是控件的事件函數並不會等showMsg函數執行完再進行推送,解決方法是推送每次只推送一條,當js執行完后再請求下一次推送。

function showMsg(msg){
    return msg;
}
function msgctrl::OnMsgNtf(msg)
{
    showMsg()
}

 

 

javascript 的單線程

JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為准?

所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。

為了利用多核CPU的計算能力,HTML5提出Web Worker標准,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標准並沒有改變JavaScript單線程的本質。

 

Event-Loop(事件循環)

單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等着。

如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑着的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等着結果出來,再往下執行。

JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。

於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。

常見的異步任務有Ajax操作、定時器(setTimeout/setInterval)、UI事件(load(圖片js文件的加載等)、resize、scroll、click等)。網上有文章說定時器是另起一個線程並行執行是不對的,下面是簡單的測試:

setTimeout(function(){console.log(111)},5);
console.log(new Date().getTime())
for(var i=0; i<10000000; i++){

}
console.log(new Date().getTime())
console.log(777);

運行結果:

可以看出只有等主線程執行完畢后才會執行任務隊列中的任務。

 

具體來說,異步執行的運行機制如下。(同步執行也是如此,因為它可以被視為沒有異步任務的異步執行。)

(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。

(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主線程不斷重復上面的第三步。

下圖就是主線程和任務隊列的示意圖。

只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重復。

參考鏈接:http://www.ruanyifeng.com/blog/2014/10/event-loop.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM