JS 語言不像 C/C++, 讓程序員本身去開闢或者釋放內存,而是相似Java,採用本身的一套垃圾回收算法進行自動的內存管理。 做爲一名資深的前端工程師,對於JS內存回收的機制是須要很是清楚,以便於在極端的環境下可以分析出系統性能的瓶頸,另外一方面,學習 這其中的機制,也對咱們深刻理解JS的閉包特性、以及對內存的高效使用,都有很大的幫助。前端
在其餘的後端語言中,如Java/Go, 對於內存的使用沒有什麼限制,可是JS不同,V8只能使用系統的一部份內存,具體來講,在64
位系統下, V8最多隻能分配1.4G
, 在 32 位系統中,最多隻能分配0.7G
。你想一想在前端這樣的大內存需求其實並不大,但對於後端而言,nodejs若是遇到 一個2G多的文件,那麼將沒法所有將其讀入內存進行各類操做了。node
咱們知道對於棧內存而言,當ESP指針下移,也就是上下文切換以後,棧頂的空間會自動被回收。但對於堆內存而言就比較複雜了,咱們下面着重分析 堆內存的垃圾回收。程序員
全部的對象類型的數據在JS中都是經過堆進行空間分配的。當咱們構造一個對象進行賦值操做的時候,其實相應的內存 已經分配到了堆上。你能夠不斷的這樣建立對象,讓 V8 爲它分配空間,直到堆的大小達到上限。算法
那麼問題來了,V8 爲何要給它設置內存上限?明明個人機器大幾十G的內存,只能讓我用這麼一點?後端
究其根本,是由兩個因素所共同決定的,一個是JS單線程的執行機制,另外一個是JS垃圾回收機制的限制。瀏覽器
首先JS是單線程運行的,這意味着一旦進入到垃圾回收,那麼其它的各類運行邏輯都要暫停; 另外一方面垃圾回收實際上是很是耗時間的操做,V8 官方是這樣形容的:前端工程師
以 1.5GB 的垃圾回收堆內存爲例,V8 作一次小的垃圾回收須要50ms 以上,作一次非增量式(ps:後面會解釋)的垃圾回收甚至要 1s 以上。閉包
可見其耗時之久,並且在這麼長的時間內,咱們的JS代碼執行會一直沒有響應,形成應用卡頓,致使應用性能和響應能力直線降低。所以,V8 作了一個簡單粗暴的 選擇,那就是限制堆內存,也算是一種權衡的手段,由於大部分狀況是不會遇到操做幾個G內存這樣的場景的。性能
不過,若是你想調整這個內存的限制也不是不行。配置命令以下:學習
// 這是調整老生代這部分的內存,單位是MB。後面會詳細介紹新生代和老生代內存
node --max-old-space-size=2048 xxx.js
複製代碼
或者
// 這是調整新生代這部分的內存,單位是 KB。
node --max-new-space-size=2048 xxx.js
複製代碼
V8 把堆內存分紅了兩部分進行處理——新生代內存和老生代內存。顧名思義,新生代就是臨時分配的內存,存活時間短, 老生代是常駐內存,存活的時間長。V8 的堆內存,也就是兩個內存 之和。
根據這兩種不一樣種類的堆內存,V8 採用了不一樣的回收策略,來根據不一樣的場景作針對性的優化。
首先是新生代的內存,剛剛已經介紹了調整新生代內存的方法,那它的內存默認限制是多少?在 64 位和 32 位系統下分別爲 32MB 和 16MB。夠小吧,不過也很好 理解,新生代中的變量存活時間短,來了立刻就走,不容易產生太大的內存負擔,所以能夠將它設的足夠小。
那好了,新生代的垃圾回收是怎麼作的呢?
首先將新生代內存空間一分爲二:
其中From部分表示正在使用的內存,To 是目前閒置的內存。
當進行垃圾回收時,V8 將From部分的對象檢查一遍,若是是存活對象那麼複製到To內存中(在To內存中按照順序從頭放置的),若是是非存活對象直接回收便可。
當全部的From中的存活對象按照順序進入到To內存以後,From 和 To 二者的角色對調
,From如今被閒置,To爲正在使用,如此循環。
那你極可能會問了,直接將非存活對象回收了不就萬事大吉了嘛,爲何還要後面的一系列操做?
注意,我剛剛特別說明了,在To內存中按照順序從頭放置的,這是爲了應對這樣的場景:
深色的小方塊表明存活對象,白色部分表示待分配的內存,因爲堆內存是連續分配的,這樣零零散散的空間可能會致使稍微大一點的對象沒有辦法進行空間分配, 這種零散的空間也叫作內存碎片。剛剛介紹的新生代垃圾回收算法也叫Scavenge算法。
Scavenge 算法主要就是解決內存碎片的問題,在進行一頓複製以後,To空間變成了這個樣子:
是否是整齊了許多?這樣就大大方便了後續連續空間的分配。
不過Scavenge 算法的劣勢也很是明顯,就是內存只能使用新生代內存的一半,可是它只存放生命週期短的對象,這種對象通常不多
,所以時間
性能很是優秀。
剛剛介紹了新生代的回收方式,那麼新生代中的變量若是通過屢次回收後依然存在,那麼就會被放入到老生代內存
中,這種現象就叫晉升
。
發生晉升其實不僅是這一種緣由,咱們來梳理一下會有那些狀況觸發晉升:
如今進入到老生代的垃圾回收機制當中,老生代中累積的變量空間通常都是很大的,固然不能用Scavenge
算法啦,浪費一半空間不說,對龐大的內存空間進行復制豈不是 勞民傷財?
那麼對於老生代而言,到底是採起怎樣的策略進行垃圾回收的呢?
第一步,進行標記-清除。這個過程在《JavaScript高級程序設計(第三版)》中有過詳細的介紹,主要分紅兩個階段,即標記階段和清除階段。首先會遍歷堆中的全部對象, 對它們作上標記,而後對於代碼環境中使用的變量
以及被強引用
的變量取消標記,剩下的就是要刪除的變量了,在隨後的清除階段
對其進行空間的回收。
固然這又會引起內存碎片的問題,存活對象的空間不連續對後續的空間分配形成障礙。老生代又是如何處理這個問題的呢?
第二步,整理內存碎片。V8 的解決方式很是簡單粗暴,在清除階段結束後,把存活的對象所有往一端靠攏。
因爲是移動對象,它的執行速度不可能很快,事實上也是整個過程當中最耗時間的部分。
因爲JS的單線程機制,V8 在進行垃圾回收的時候,不可避免地會阻塞業務邏輯的執行,假若老生代的垃圾回收任務很重,那麼耗時會很是可怕,嚴重影響應用的性能。 那這個時候爲了不這樣問題,V8 採起了增量標記的方案,即將一口氣完成的標記任務分爲不少小的部分完成,每作完一個小的部分就"歇"一下,就js應用邏輯執行一下子, 而後再執行下面的部分,若是循環,直到標記階段完成才進入內存碎片的整理上面來。其實這個過程跟React Fiber的思路有點像,這裏就不展開了。
通過增量標記以後,垃圾回收過程對JS應用的阻塞時間減小到原來了1 / 6, 能夠看到,這是一個很是成功的改進。
JS垃圾回收的原理就介紹到這裏了,其實理解起來是很是簡單的,重要的是理解它爲何要這麼作
,而不只僅是如何作的
,但願這篇總結可以對你有所啓發。
參考資料:
《JavaScript高級程序設計(第三版)》
《深刻淺出nodejs》 樸靈著
極客時間《瀏覽器工做原理與實踐》