NodeJS中被忽略的記憶體
如樸靈說過,Node對記憶體洩露十分敏感,一旦線上應用有成千上萬的流量,那怕是一個位元組的記憶體洩漏也會造成堆積,垃圾回收過程中將會耗費更多時間進行物件掃描,應用響應緩慢,直到程式記憶體溢位,應用奔潰。
雖然從很久以前就知道記憶體問題是不容忽視的,但是日常開發的時候並沒有碰到效能上的瓶頸,直到最近做了一個百萬PV級的營銷專案,由於訪問量,併發量都達到了一個量級。一些細小的、平時沒注意到的問題被放大,這才映入眼簾,開始注意到了記憶體問題。殊不知Node對記憶體的洩露是如此的敏感。
為此,趕緊去補習了一下V8中的記憶體處理機制。
那麼,V8中的記憶體機制是怎麼樣的?
V8的記憶體機制
記憶體的限制
Node中並不像其他後端語言中,對記憶體的使用沒有多少限制。在Node中使用記憶體,只能使用到系統的一部分記憶體,64位系統下約為1.4GB,32位系統下約為0.7GB。這歸咎於Node使用了本來執行在瀏覽器的V8引擎。
V8引擎的設計之初只是執行在瀏覽器中,而在瀏覽器的一般應用場景下使用起來綽綽有餘,足以勝任前端頁面中的所有需求。
雖然服務端操作大記憶體也不是常見的需求,但是萬一有這樣的需求,還是可以解除限制的。
在啟動node程式的時候,可以傳遞兩個引數來調整記憶體限制的大小。
node --max-nex-space-size=1024 app.js // 單位為KB
node --max-old-space-size=2000 app.js // 單位為MB
這兩條命令分別對應Node記憶體堆中的「新生代」和「老生代」
不受記憶體限制的特例
在Node中,使用Buffer可以讀取超過V8記憶體限制的大檔案。原因是Buffer物件不同於其他物件,它不經過V8的記憶體分配機制。這在於Node並不同於瀏覽器的應用場景。在瀏覽器中,JavaScript直接處理字串即可滿足絕大多數的業務需求,而Node則需要處理網路流和檔案I/O流,操作字串遠遠不能滿足傳輸的效能需求。
記憶體的分配
一切JavaScript物件都用堆來儲存
當我們在程式碼中宣告變數並賦值時,所使用物件的記憶體就分配在堆中。如果已申請的對空閒記憶體不夠分配新的物件,講繼續申請堆記憶體,直到堆的大小超過V8的限制為止。
V8的垃圾回收機制
分代式垃圾回收
V8的垃圾回收策略主要基於「分代式垃圾回收機制」,基於這個機制,V8把記憶體分為「新生代(New Space)」和 「老生代 (Old Space)」。
新生代中的物件為存活時間較短的物件,老生代中的物件為存活時間較長或常駐記憶體的物件。
前面提及到的--max-old-space-size
命令就是設定老生代記憶體空間的最大值,而--max-new-space-size
命令則可以設定新生代記憶體空間的大小。
為什麼要分成新老兩代?
垃圾回收演算法有很多種,但是並沒有一種是勝任所有的場景,在實際的應用中,需要根據物件的生存週期長短不一,而使用不同的演算法,已達到最好的效果。在V8中,按物件的存活時間將記憶體的垃圾回收進行不同的分代,然後分別對不同的記憶體施以更高效的演算法。
新生代中的垃圾回收
在新生代中,主要通過Scavenge演算法進行垃圾回收。
Scavenge
在Scavenge演算法中,它將堆記憶體一分為二,每一部分空間稱為semispace。在這兩個semispace空間中,只有一個處於使用中,另外一個處於閒置狀態。處於使用狀態的semispace稱為From空間,處於閒置狀態的semispace稱為To空間。當我們分配物件時,先是從From空間中分配。當開始進行垃圾回收時,會檢查From空間中存活的物件,這些存活的物件會被複制到To空間中,而非存活的物件佔用的空間會被釋放。完成複製後,From空間和To空間角色互換。簡而言之,在垃圾回收的過程中,就是通過將存活物件在兩個semispace空間之間進行復制。
在新生代中的物件怎樣才能到老生代中?
在新生代存活週期長的物件會被移動到老生代中,主要符合兩個條件中的一個:
1. 物件是否經歷過Scavenge回收。
物件從From空間中複製到To空間時,會檢查它的記憶體地址來判斷這個物件是否已經經歷過一次Scavenge回收,如果已經經歷過了,則將該物件從From空間中複製到老生代空間中。
2. To空間的記憶體佔比超過25%限制。
當物件從From空間複製到To空間時,如果To空間已經使用超過25%,則這個物件直接複製到老生代中。這麼做的原因在於這次Scavenge回收完成後,這個To空間會變成From空間,接下來的記憶體分配將在這個空間中進行。如果佔比過高,會影響後續的記憶體分配。
老生代中的垃圾回收
對於老生代的物件,由於存活物件佔比較大比重,使用Scavenge演算法顯然不科學。一來複制的物件太多會導致效率問題,二來需要浪費多一倍的空間。所以,V8在老生代中主要採用「Mark-Sweep」演算法與「Mark-Compact」演算法相結合的方式進行垃圾回收。
Mark-Sweep
Mark-Sweep是標記清除的意思,分為標記和清除兩個階段。在標記階段遍歷堆中的所有物件,並標記存活的物件,在隨後的清除階段中,只清除標記之外的物件。
但是Mark-Sweep有一個很嚴重的問題,就是進行一次標記清除回收之後,記憶體會變得碎片化。如果需要分配一個大物件,這時候就無法完成分配了。這時候就該Mark-Compact出場了。
Mark-Compact
Mark-Compact是標記整理的意思,是在Mark-Sweep基礎上演變而來。Mark-Compact在標記存活物件之後,在整理過程中,將活著的物件往一端移動,移動完成後,直接清理掉邊界外的記憶體。
Incremental Marking
鑑於Node單執行緒的特性,V8每次垃圾回收的時候,都需要將應用邏輯暫停下來,待執行完垃圾回收後再恢復應用邏輯,被稱為「全停頓」。在分代垃圾回收中,一次小垃圾回收只收集新生代,且存活物件也相對較少,即使全停頓也沒有多大的影響。但是在老生代中,存活物件較多,垃圾回收的標記、清理、整理都需要長時間的停頓,這樣會嚴重影響到系統的效能。
所以「增量標記 (Incrememtal Marking)」被提出來。它從標記階段入手,將原本要一口氣停頓完成的動作改為增量標記,拆分為許多小「步進」,每做完一「步進」就讓JavaScript應用邏輯執行一小會,垃圾回收與應用邏輯這樣交替執行直到標記階段完成。
記憶體洩露排查的工具
node-heapdump
它允許對V8堆記憶體抓取快照,用於事後分析。
在程式中引入
var heapdump = require("node-heapdump");
之後可以通過向伺服器傳送SIGUSR2訊號,讓node-heapdump抓拍一份堆記憶體的快照:
$ kill -USR2 <pid>
這份抓拍的快照會預設存放在檔案目錄下,這是一份大JSON檔案,可以通過Chrome的開發者工具開啟檢視。
![Chrome Profile][8]
node-memwatch
需要注意,node-memwatch只是支援到node v0.12.x為止,當使用更高的版本的時候,就會安裝不上,這時候可以使用node-watch-next 替代,一摸一樣的API。
不同於node-heapdump,它提供了兩個事件監聽器,用來提供記憶體洩露的以及垃圾回收的資訊:
- stats事件:每次進行全堆回收時,會觸發改時間,傳遞記憶體的統計資訊
- leak事件:經過五次垃圾回收之後,記憶體仍沒有被釋放的物件,會觸發leak事件,傳遞相關的資訊。
node-profiler
node-profiler 是 alinode團隊出品的一個與node-heapdump類似的抓取記憶體堆快照的工具,不同的是,node-profiler的實現不一樣,使用起來更便捷。附上他們的教程:如何使用Node Profiler
alinode
alinode官方如似說:
alinode 是阿里雲出品的 Node.js 應用服務解決方案,是一套基於社群 Node 改進的執行時環境和服務平臺。在社群的基礎上我們內建了強大的支援功能,幫助開發者迅速洞見效能細節,快速定位疑難雜症,直探問題根源。
以上內容參考自
相關文章
- nodejs爬蟲記憶體洩露排查NodeJS爬蟲記憶體洩露
- nodejs 計算記憶體使用率NodeJS記憶體
- IE CSS Bug系列:IE8中被忽略的:focusCSS
- 我是如何搞定 NodeJS 記憶體洩漏問題的NodeJS記憶體
- Java的記憶體 -JVM 記憶體管理Java記憶體JVM
- 記憶體管理篇——實體記憶體的管理記憶體
- 遊戲基礎知識——“記憶對比”,體驗分析中經常被忽略的部分遊戲
- JS中的棧記憶體、堆記憶體JS記憶體
- Redis記憶體——記憶體消耗(記憶體都去哪了?)Redis記憶體
- 記憶體_大頁記憶體記憶體
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- NodeJS V8引擎的記憶體和垃圾回收器(GC)NodeJS記憶體GC
- 記憶體管理 記憶體管理概述記憶體
- 【記憶體管理】記憶體佈局記憶體
- 什麼是Java記憶體模型(JMM)中的主記憶體和本地記憶體?Java記憶體模型
- 實體記憶體和虛擬記憶體記憶體
- 記憶體的分配與釋放,記憶體洩漏記憶體
- Aerospike的bin記憶體管理--即列記憶體管理ROS記憶體
- java棧記憶體和堆記憶體的詮釋Java記憶體
- 遊戲記憶體對比普通記憶體區別 遊戲記憶體和普通記憶體相差大嗎?遊戲記憶體
- Go:記憶體管理與記憶體清理Go記憶體
- 聊聊 記憶體模型與記憶體序記憶體模型
- NIO的JVM記憶體和機器記憶體的選擇JVM記憶體
- 自動共享記憶體管理 自動記憶體管理 手工記憶體管理記憶體
- Delphi 的記憶體操作函式(5): 複製記憶體記憶體函式
- Java記憶體區域和記憶體模型Java記憶體模型
- 記憶體溢位和記憶體洩露記憶體溢位記憶體洩露
- 直接記憶體和堆記憶體誰快記憶體
- 記憶體分析與記憶體洩漏定位記憶體
- 記憶體洩漏和記憶體溢位記憶體溢位
- JVM 記憶體模型 記憶體分配,JVM鎖JVM記憶體模型
- Linux 記憶體管理:記憶體對映Linux記憶體
- OpenResty 和 Nginx 的共享記憶體區是如何消耗實體記憶體的RESTNginx記憶體
- 虛擬記憶體到實體記憶體(32位)記憶體
- 【Java基礎】實體記憶體&虛擬記憶體Java記憶體
- 虛擬記憶體系統——瞭解記憶體的工作原理記憶體
- 伺服器記憶體和普通記憶體的不同點在哪伺服器記憶體
- C++記憶體管理:簡易記憶體池的實現C++記憶體