內存基本概念
內存的生命周期:
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