V8 堆棧空間和垃圾回收機制

微信公衆號:[前端一鍋煮]
一點技術、一點思考。
  • 棧空間
  • 堆空間
  • 新生代內存回收
  • 老生代內存回收
  • 標記清除、標記整理、增量標記

JavaScript 引擎的內存空間主要分爲棧和堆。前端

V8 的垃圾回收策略主要基於分代式垃圾回收機制。按照對象的存活時間將內存的垃圾回收進行不一樣分代,而後分別對不一樣分代的內存使用最適合的算法。主要分爲新生代和老生代,有標記清除、標記整理、增量標記等方法。vue

棧空間

棧是臨時存儲空間,主要存儲局部變量和函數調用。node

基本類型賦值(Number, Boolean, String, Null, Undefined, Symbol, BigInt),系統會爲新的變量在棧內存中分配一個新值。web

引用類型賦值,系統會爲新的變量在棧內存中分配一個值,這個值僅僅是指向同一個對象的引用,和原對象指向的都是堆內存中的同一個對象。算法

對於函數,解釋器建立了」調用棧「來記錄函數的調用過程。每調用一個函數,解釋器就把該函數添加進調用棧,解釋器會爲被添加進來的函數建立一個棧幀(用來保存函數的局部變量以及執行語句)並當即執行。若是正在執行的函數還調用了其餘函數,新函數會繼續被添加進入調用棧。函數執行完成,對應的棧幀當即被銷燬。vue-cli

兩種查看調用棧的方法

使用 console.trace() 向 web 控制檯輸出一個堆棧跟蹤。瀏覽器

瀏覽器開發者工具進行斷點調試。服務器

棧溢出

棧雖然很輕量,在使用時建立,使用結束後銷燬,可是不是能夠無限增加的,被分配的調用棧空間被佔滿時,就會引發」棧溢出「的錯誤。微信

(function foo() {
  foo()
})()

Maximum call stack size exceeded.ide

爲何基本數據類型存儲在棧中,引用數據類型存儲在堆中?

JavaScript 引擎須要用棧來維護程序執行期間的上下文的狀態,若是棧空間大了的話,全部數據都存放在棧空間裏面,會影響到上下文切換的效率,進而影響整個程序的執行效率。

堆空間

堆空間存儲的數據比較複雜,大體能夠劃分爲 5 個區域:

  1. 新生代內存區(new space):新生代內存區會被劃分爲兩塊,分別是 from space 和 to space(具體有什麼用下文會說),64位系統下默認 32MB,32位系統下默認 16MB,一般新建立的對象會先放入 from 中。
  2. 老生代內存區(old space):較爲持久的保存對象,分爲兩個區域 old pointer space 和 old data space 分別用來存放 GC 後還存活的指針信息和數據信息,64位系統下能使用約 1.4GB,32位系統下能使用約 0.7GB。
  3. 大對象區(large object space):這裏存放體積超越其餘區大小的對象,主要爲了不大對象的拷貝,使用該空間專門存儲大對象。
  4. 單元區、屬性單元區、Map區(Cell space、property cell space、map space):Map 空間存放對象的 Map 信息也就是隱藏類(Hiden Class)最大限制爲 8MB;每一個 Map 對象固定大小,爲了快速定位,因此將該空間單獨出來。
  5. 代碼區 (code Space):主要存放代碼對象,最大限制爲 512MB,也是惟一擁有執行權限的內存。

新生代內存是臨時分配的內存,存活時間短,老生代內存是常駐內存,存活時間長。

新生代內存回收

新生代內存中的垃圾回收主要經過 Scavenge 算法進行,具體實現時主要採用 Cheney 算法。

Cheney 將內存空間一分爲二,一塊叫作 From 正在使用的內存,另外一塊叫作 To 目前閒置的內存。

Scavenge GC算法:

  • 存活的對象從 from space 轉移到 to space
  • 清空 from space
  • from space 與 to space 互換
  • 完成一次新生代 GC

簡而言之,在垃圾回收的過程當中,將存活對象在兩個空間之間進行復制。

Scavenge 是典型的犧牲空間換取時間的算法,缺點是隻能使用堆內存中的一半,這是由劃分空間和複製機制所決定的。但因爲只複製存活的對象,而且對於生命週期短的場景存活對象只佔少部分,因此它在時間效率上有優異的表現。

老生代內存回收

V8 在老生代中主要採 用了 Mark-Sweep 和 Mark-Compact 相結合的方式進行垃圾回收。

晉升

當一個對象通過屢次複製依然存在時,它將會被認爲是生命週期較長的對象,這種對象會被移到老生代中,採用新的算法進行管理,這種移動稱之爲「晉級」。

對象晉級的條件主要有兩個:

已經經歷過一次 Scavenge 回收

To(閒置內存)空間的內存不足75%

標記清除(Mark-Sweep)

標記清除,分爲標記和清除兩個階段。在標記階段遍歷堆中的全部對象,並標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象。能夠看出,Scavenge 中只複製活着的對象,而 Mark-Sweep 只清理死亡對象。

標記清除最大的問題是在進行一次標記清除回收後,內存空間會出現不連續的狀態。這種內存碎片會對後續的內存分配形成問題,由於極可能出現須要分配一個大對象的狀況,這時全部的碎片空間都沒法完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。爲了解決標記清除的內存碎片問題,標記整理(Mark-Compact)被提出來。

標記整理(Mark-Compact)

Mark-Compact 是標記整理的意思,在 Mark-Sweep 的基礎上演變而來。

標記整理對待未存活對象不是當即回收,而是將存活對象移動到一邊,而後直接清掉端邊界之外的內存。

增量標記

爲了不出現 JavaScript 應用程序與垃圾回收器看到的不一致的狀況,進行垃圾回收的時候,都須要將正在運行的程序停下來,等待垃圾回收執行完成以後再回復程序的執行,這種現象稱爲「全停頓」。若是須要回收的數據過多,那麼全停頓的時候就會比較長,會影響其餘程序的正常執行。

爲了不垃圾回收時間過長影響其餘程序的執行,V8將標記過程分紅一個個小的子標記過程,同時讓垃圾回收和JavaScript應用邏輯代碼交替執行,直到標記階段完成。咱們稱這個過程爲增量標記算法。

通俗理解,就是將本來一口氣完成的標記任務分爲了不少小的部分去完成, 每完成一個小任務就停一會, 讓 js 邏輯執行一會, 而後再繼續執行下面的部分。

總結

從 V8 垃圾回收機制能夠看到,垃圾回收是一件很是耗時的事情, 以 1.5GB 的垃圾回收堆內存爲例,V8 作一次小的垃圾回收須要 50ms 以上,作一次非增量式的垃圾回收甚至要 1s 以上,因此要作限制。

新生代設計爲一個較小的內存空間是合理的,而老生代空間過大對於垃圾回收並沒有特別意義。V8 對內存限制的設置對於 Chrome 瀏覽器這種每一個選項卡頁面使用一個 V8 實例而言,內存的使用是綽綽有餘了。對於 Node 編寫的服務器端來講,內存限制也並不影響正常場景下的使用。可是對於 V8 的垃圾回收特色和js 在單線程上的執行狀況,垃圾回收是影響性能的因素之一。想要高性能的執行效率,須要注意讓垃圾回收儘可能少地進行,尤爲是全堆垃圾回收。

其餘

vue-cli 打包內存溢出,修改內存限制

node_modules/.bin

vue-cli-service

#!/usr/bin/env node --max_old_space_size=4096

調整老生代內存限制,單位mb

node --max-old-space-size=2048 build/build.js

調整新生代內存限制,單位kb

node --max-new-space-size=2048 build/build.js

相關文章
相關標籤/搜索