摘要:垃圾回收內存管理實踐先通過一個來看看在中進行垃圾回收的過程是怎樣的內存泄漏識別在環境里提供了方法用來查看當前進程內存使用情況,單位為字節中保存的進程占用的內存部分,包括代碼本身棧堆。
作者 | 五月君
Node.js 技術棧 | https://www.nodejs.red
慕課認證作者 | https://imooc.com/u/2667395
對于 Node.js 服務端研發的同學來說,關于垃圾回收、內存釋放這塊不需要向 C/C++ 的同學那樣在創建一個對象之后還需要手動創建一個 delete/free 這樣的一個操作進行 GC(垃圾回收), Node.js 與 Java 一樣,由虛擬機進行內存自動管理。
但是這樣并不表示就此可以高枕無憂了,在開發中可能由于疏忽或者程序錯誤導致的內存泄漏也是一個很嚴重的問題,所以做為一名合格的服務端研發工程師,還是有必要的去了解下虛擬機是怎樣使用內存的,遇到問題才能從容應對。
快速導航Nodejs中的GC
Nodejs垃圾回收內存管理實踐
內存泄漏識別
內存泄漏例子
手動執行垃圾回收內存釋放
V8垃圾回收機制
V8堆內存限制
新生代與老生代
新生代空間 & Scavenge 算法
老生代空間 & Mark-Sweep Mark-Compact 算法
V8垃圾回收總結
內存泄漏
全局變量
閉包
慎將內存做為緩存
模塊私有變量內存永駐
事件重復監聽
其它注意事項
內存檢測工具
Nodejs中的GCNode.js 是一個基于 Chrome V8 引擎的 JavaScript 運行環境,這是來自 Node.js 官網的一段話,所以 V8 就是 Node.js 中使用的虛擬機,在之后講解的 Node.js 中的 GC 其實就是在講 V8 的 GC。
Node.js 與 V8 的關系也好比 Java 之于 JVM 的關系,另外 Node.js 之父 Ryan Dahl 在選擇 V8 做為 Node.js 的虛擬機時 V8 的性能在當時已經領先了其它所有的 JavaScript 虛擬機,至今仍然是性能最好的,因此我們在做 Node.js 優化時,只要版本升級性能也會伴隨著被提升。
Nodejs垃圾回收內存管理實踐先通過一個 Demo 來看看在 Node.js 中進行垃圾回收的過程是怎樣的?內存泄漏識別
在 Node.js 環境里提供了 process.memoryUsage 方法用來查看當前進程內存使用情況,單位為字節
rss(resident set size):RAM 中保存的進程占用的內存部分,包括代碼本身、棧、堆。
heapTotal:堆中總共申請到的內存量。
heapUsed:堆中目前用到的內存量,判斷內存泄漏我們主要以這個字段為準。
external: V8 引擎內部的 C++ 對象占用的內存。
/** * 單位為字節格式為 MB 輸出 */ const format = function (bytes) { return (bytes / 1024 / 1024).toFixed(2) + " MB"; }; /** * 封裝 print 方法輸出內存占用信息 */ const print = function() { const memoryUsage = process.memoryUsage(); console.log(JSON.stringify({ rss: format(memoryUsage.rss), heapTotal: format(memoryUsage.heapTotal), heapUsed: format(memoryUsage.heapUsed), external: format(memoryUsage.external), })); }內存泄漏例子
堆用來存放對象引用類型,例如字符串、對象。在以下代碼中創建一個 Fruit 存放于堆中。
// example.js function Quantity(num) { if (num) { return new Array(num * 1024 * 1024); } return num; } function Fruit(name, quantity) { this.name = name this.quantity = new Quantity(quantity) } let apple = new Fruit("apple"); print(); let banana = new Fruit("banana", 20); print();
執行以上代碼,內存向下面所展示的,apple 對象 heapUsed 的使用僅有 4.21 MB,而 banana 我們對它的 quantity 屬性創建了一個很大的數組空間導致 heapUsed 飆升到 164.24 MB。
$ node example.js {"rss":"19.94 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"} {"rss":"180.04 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}
我們在來看下內存的使用情況,根節點對每個對象都持有引用,則無法釋放任何內容導致無法 GC,正如下圖所展示的
手動執行垃圾回收內存釋放假設 banana 對象我們不在使用了,對它重新賦予一些新的值,例如 banana = null,看下此刻會發生什么?
結果如上圖所示,無法從根對象在到達到 Banana 對象,那么在下一個垃圾回收器運行時 Banana 將會被釋放。
讓我們模擬一下垃圾回收,看下實際情況是什么樣的?
// example.js let apple = new Fruit("apple"); print(); let banana = new Fruit("banana", 20); print(); banana = null; global.gc(); print();
以下代碼中 --expose-gc 參數表示允許手動執行垃圾回收機制,將 banana 對象賦為 null 后進行 GC,在第三個 print 打印出的結果可以看到 heapUsed 的使用已經從 164.24 MB 降到了 3.97 MB
$ node --expose-gc example.js {"rss":"19.95 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"} {"rss":"180.05 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"} {"rss":"52.48 MB","heapTotal":"9.33 MB","heapUsed":"3.97 MB","external":"0.01 MB"}
下圖所示,右側的 banana 節點沒有了任何內容,經過 GC 之后所占用的內存已經被釋放了。
V8垃圾回收機制垃圾回收是指回收那些在應用程序中不在引用的對象,當一個對象無法從根節點訪問這個對象就會做為垃圾回收的候選對象。這里的根對象可以為全局對象、局部變量,無法從根節點訪問指的也就是不會在被任何其它活動對象所引用。V8堆內存限制
內存在服務端本來就是一個寸土寸金的東西,在 V8 中限制 64 位的機器大約 1.4GB,32 位機器大約為 0.7GB。因此,對于一些大內存的操作需謹慎否則超出 V8 內存限制將會造成進程退出。
一個內存溢出超出邊界限制的例子
// overflow.js const format = function (bytes) { return (bytes / 1024 / 1024).toFixed(2) + " MB"; }; const print = function() { const memoryUsage = process.memoryUsage(); console.log(`heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${format(memoryUsage.heapUsed)}`); } const total = []; setInterval(function() { total.push(new Array(20 * 1024 * 1024)); // 大內存占用 print(); }, 1000)
以上例子中 total 為全局變量每次大約增長 160 MB 左右且不會被回收,在接近 V8 邊界時無法在分配內存導致進程內存溢出。
$ node overflow.js heapTotal: 166.84 MB, heapUsed: 164.23 MB heapTotal: 326.85 MB, heapUsed: 324.26 MB heapTotal: 487.36 MB, heapUsed: 484.27 MB heapTotal: 649.38 MB, heapUsed: 643.98 MB heapTotal: 809.39 MB, heapUsed: 803.98 MB heapTotal: 969.40 MB, heapUsed: 963.98 MB heapTotal: 1129.41 MB, heapUsed: 1123.96 MB heapTotal: 1289.42 MB, heapUsed: 1283.96 MB <--- Last few GCs ---> [87581:0x103800000] 11257 ms: Mark-sweep 1283.9 (1290.9) -> 1283.9 (1290.9) MB, 512.1 / 0.0 ms allocation failure GC in old space requested [87581:0x103800000] 11768 ms: Mark-sweep 1283.9 (1290.9) -> 1283.9 (1287.9) MB, 510.7 / 0.0 ms last resort GC in old space requested [87581:0x103800000] 12263 ms: Mark-sweep 1283.9 (1287.9) -> 1283.9 (1287.9) MB, 495.3 / 0.0 ms last resort GC in old space requested <--- JS stacktrace --->
在 V8 中也提供了兩個參數僅在啟動階段調整內存限制大小
分別為調整老生代、新生代空間,關于老生代、新生代稍后會做介紹。
--max-old-space-size=2048
--max-new-space-size=2048
當然內存也并非越大越好,一方面服務器資源是昂貴的,另一方面據說 V8 以 1.5GB 的堆內存進行一次小的垃圾回收大約需要 50 毫秒以上時間,這將會導致 JavaScript 線程暫停,這也是最主要的一方面。
新生代與老生代絕對大多數的應用程序對象的存活周期都會很短,而少數對象的存活周期將會很長為了利用這種情況,V8 將堆分為兩類新生代和老生代,新空間中的對象都非常小大約為 1-8MB,這里的垃圾回收也很快。新生代空間中垃圾回收過程中幸存下來的對象會被提升到老生代空間。
新生代空間由于新空間中的垃圾回收很頻繁,因此它的處理方式必須非常的快,采用的 Scavenge 算法,該算法由 C.J. Cheney 在 1970 年在論文 A nonrecursive list compacting algorithm 提出。
Scavenge 是一種復制算法,新生代空間會被一分為二劃分成兩個相等大小的 from-space 和 to-space。它的工作方式是將 from space 中存活的對象復制出來,然后移動它們到 to space 中或者被提升到老生代空間中,對于 from space 中沒有存活的對象將會被釋放。完成這些復制后在將 from space 和 to space 進行互換。
Scavenge 算法非常快適合少量內存的垃圾回收,但是它有很大的空間開銷,對于新生代少量內存是可以接受的。
老生代空間新生代空間在垃圾回收滿足一定條件(是否經歷過 Scavenge 回收、to space 的內存占比)會被晉升到老生代空間中,在老生代空間中的對象都已經至少經歷過一次或者多次的回收所以它們的存活概率會更大。在使用 Scavenge 算法則會有兩大缺點一是將會重復的復制存活對象使得效率低下,二是對于空間資源的浪費,所以在老生代空間中采用了 Mark-Sweep(標記清除) 和 Mark-Compact(標記整理) 算法。
Mark-Sweep
Mark-Sweep 處理時分為標記、清除兩個步驟,與 Scavenge 算法只復制活對象相反的是在老生代空間中由于活對象占多數 Mark-Sweep 在標記階段遍歷堆中的所有對象僅標記活對象把未標記的死對象清除,這時一次標記清除就已經完成了。
看似一切 perfect 但是還遺留一個問題,被清除的對象遍布于各內存地址,產生很多內存碎片。
Mark-Compact
在老生代空間中為了解決 Mark-Sweep 算法的內存碎片問題,引入了 Mark-Compact(標記整理算法),其在工作過程中將活著的對象往一端移動,這時內存空間是緊湊的,移動完成之后,直接清理邊界之外的內存。
V8垃圾回收總結為何垃圾回收是昂貴的?V8 使用了不同的垃圾回收算法 Scavenge、Mark-Sweep、Mark-Compact。這三種垃圾回收算法都避免不了在進行垃圾回收時需要將應用程序暫停,待垃圾回收完成之后在恢復應用邏輯,對于新生代空間來說由于很快所以影響不大,但是對于老生代空間由于存活對象較多,停頓還是會造成影響的,因此,V8 又新增加了增量標記的方式減少停頓時間。
關于 V8 垃圾回收這塊筆者講的很淺只是自己在學習過程中做的總結,如果你想了解更多原理,深入淺出 Node.js 這本書是一個不錯的選擇,還可參考這兩篇文章 A tour of V8: Garbage Collection、 Memory Management Reference.。
內存泄漏內存泄漏(Memory Leak)是指程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。全局變量
未聲明的變量或掛在全局 global 下的變量不會自動回收,將會常駐內存直到進程退出才會被釋放,除非通過 delete 或 重新賦值為 undefined/null 解決之間的引用關系,才會被回收。關于全局變量上面舉的幾個例子中也有說明。
閉包這個也是一個常見的內存泄漏情況,閉包會引用父級函數中的變量,如果閉包得不到釋放,閉包引用的父級變量也不會釋放從而導致內存泄漏。
一個真實的案例 — The Meteor Case-Study,2013年,Meteor 的創建者宣布了他們遇到的內存泄漏的調查結果。有問題的代碼段如下
var theThing = null var replaceThing = function () { var originalThing = theThing var unused = function () { if (originalThing) console.log("hi") } theThing = { longStr: new Array(1000000).join("*"), someMethod: function () { console.log(someMessage) } }; }; setInterval(replaceThing, 1000)
以上代碼運行時每次執行 replaceThing 方法都會生成一個新的對象,但是之前的對象沒有釋放導致的內存泄漏。這塊涉及到一個閉包的概念 “同一個作用域生成的閉包對象是被該作用域中所有下一級作用域共同持有的” 因為定義的 unused 使用了作用域的 originalThing 變量,因此 replaceThing 這一級的函數作用域中的閉包(someMethod)對象也持有了 originalThing 變量(重點:someMethod 的閉包作用域和 unused 的作用域是共享的),之間的引用關系就是 theThing 引用了 longStr 和 someMethod、someMethod 引用了 originalThing、originalThing 又引用了上次的 theThing,因此形成了鏈式引用。
上述代碼來自 Meteor blog An interesting kind of JavaScript memory leak,更多理解還可參考 Node-Interview issues #7 討論
慎將內存做為緩存通過內存來做緩存這可能是我們想到的最快的實現方式,另外業務中緩存還是很常用的,但是了解了 Node.js 中的內存模型和垃圾回收機制之后在使用的時候就要謹慎了,為什么呢?緩存中存儲的鍵越多,長期存活的對象也就越多,垃圾回收時將會對這些對對象做無用功。
以下舉一個獲取用戶 Token 的例子,memoryStore 對象會隨著用戶數的增加而持續增長,以下代碼還有一個問題,當你啟動多個進程或部署在多臺機器會造成每個進程都會保存一份,顯然是資源的浪費,最好是通過 Redis 做共享。
const memoryStore = new Map(); exports.getUserToken = function (key) { const token = memoryStore.get(key); if (token && Date.now() - token.now > 2 * 60) { return token; } const dbToken = db.get(key); memoryStore.set(key, { now: Date.now(), val: dbToken, }); return token; }模塊私有變量內存永駐
在加載一個模塊代碼之前,Node.js 會使用一個如下的函數封裝器將其封裝,保證了頂層的變量(var、const、let)在模塊范圍內,而不是全局對象。
這個時候就會形成一個閉包,在 require 時會被加載一次,將 exports 對象保存于內存中,直到進程退出才會回收,這個將會導致的是內存常駐,所以對一個模塊的引用建議僅在頭部引用一次緩存起來,而不是在使用時每次都加載,否則也會造成內存增加。
(function(exports, require, module, __filename, __dirname) { // 模塊的代碼實際上在這里 });事件重復監聽
在 Node.js 中對一個事件重復監聽則會報如下錯誤,實際上使用的 EventEmitter 類,該類包含一個 listeners 數組,默認為 10 個監聽器超出這個數則會報警如下所示,用于發現內存泄漏,也可以通過 emitter.setMaxListeners() 方法為指定的 EventEmitter 實例修改限制。
(node:23992) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit
Cnode 論欄有篇文章分析了 Socket 重連導致的內存泄漏,參考 原生Socket重連策略不恰當導致的泄漏,還有 Node.js HTTP 模塊 Keep-Alive 產生的內存泄漏,參考 Github Node Issues #714
其它注意事項在使用定時器 setInterval 時記的使用對應的 clearInterval 進行清除,因為 setInterval 執行完之后會返回一個值且不會自動釋放。另外還有 map、filter 等對數組進行操作,每次操作之后都會創建一個新的數組,將會占用內存,如果單純的遍歷例如 map 可以使用 forEach 代替,這些都是開發中的一些細節,但是往往細節決定成敗,每一次的內存泄漏也都是一次次的不經意間造成的。因此,這些點也是需要我們注意的。
console.log(setInterval(function(){}, 1000)) // 返回一個 id 值 [1, 2, 3].filter(item => item % 2 === 0) // [2] [1, 2, 3].map(item => item % 2 === 0) // [false, true, false]內存檢測工具
node-heapdump
heapdump是一個dumpV8堆信息的工具,node-heapdump
node-profiler
node-profiler 是 alinode 團隊出品的一個 與node-heapdump 類似的抓取內存堆快照的工具,node-profiler
Easy-Monitor
輕量級的 Node.js 項目內核性能監控 + 分析工具,https://github.com/hyj1991/easy-monitor
Node.js-Troubleshooting-Guide
Node.js 應用線上/線下故障、壓測問題和性能調優指南手冊,Node.js-Troubleshooting-Guide
alinode
Node.js 性能平臺(Node.js Performance Platform)是面向中大型 Node.js 應用提供 性能監控、安全提醒、故障排查、性能優化等服務的整體性解決方案。alinode
閱讀推薦Node.js Garbage Collection Explained
A tour of V8: Garbage Collection 中文版 V8 之旅: 垃圾回收器
Memory Management Reference.
深入淺出 Node.js
如何分析 Node.js 中的內存泄漏
公眾號 “Nodejs技術棧”,專注于 Node.js 技術棧的分享
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105253.html
摘要:的內存限制和垃圾回收機制內存限制內存限制一般的后端語言開發中,在基本的內存使用是沒有限制的。的內存分代目前沒有一種垃圾自動回收算法適用于所有場景,所以的內部采用的其實是兩種垃圾回收算法。 前言 從前端思維轉變到后端, 有一個很重要的點就是內存管理。以前寫前端因為只是在瀏覽器上運行, 所以對于內存管理一般不怎么需要上心, 但是在服務器端, 則需要斤斤計較內存。 V8的內存限制和垃圾回收機...
摘要:正好最近在學習的各種實現原理,在這里斗膽翻譯一篇垃圾回收機制原文鏈接。自動管理的機制中,通常都會包含垃圾回收機制。二垃圾回收機制的概念垃圾回收,是一種自動管理應用程序所占內存的機制,簡稱方便起見,本文均采用此簡寫。 最近關注了一個國外技術博客RisingStack里面有很多高質量,且對新手也很friendly的文章。正好最近在學習Node.js的各種實現原理,在這里斗膽翻譯一篇Node...
摘要:新生代的對象為存活時間較短的對象,老生代中的對象為存活時間較長或常駐內存的對象。分別對新生代和老生代使用不同的垃圾回收算法來提升垃圾回收的效率。如果指向老生代我們就不必考慮它了。 這篇文章的所有內容均來自 樸靈的《深入淺出Node.js》及A tour of V8:Garbage Collection,后者還有中文翻譯版V8 之旅: 垃圾回收器,我在這里只是做了個記錄和結合 垃圾回收...
摘要:關鍵是釋放內存這一步,各種語言都有自己的垃圾回收簡稱機制。用的是這種,在字末位進行標識,為指針。對于而言,最初的垃圾回收機制,是基于引用計次來做的。老生代的垃圾回收,分兩個階段標記清理有和這兩種方式。 不管是高級語言,還是低級語言。內存的管理都是: 分配內存 使用內存(讀或寫) 釋放內存 前兩步,大家都沒有太大異議。關鍵是釋放內存這一步,各種語言都有自己的垃圾回收(garbage ...
摘要:內存管理具有垃圾自動回收機制簡稱。標記清除標記清除是目前大部分引擎使用的判斷方式,通過標記變量的狀態來確定是否可被回收。被標記,進入環境被標記,進入環境執行完畢之后被標記,離開環境引用計數引擎維護一張引用表,保存內存中所有的資源的引用次數。 JavaScript 內存管理 JavaScript 具有垃圾自動回收機制(Garbage Collection)簡稱 GC。垃圾回收機制會中斷整...
閱讀 2423·2021-10-09 09:59
閱讀 2177·2021-09-23 11:30
閱讀 2591·2019-08-30 15:56
閱讀 1145·2019-08-30 14:00
閱讀 2939·2019-08-29 12:37
閱讀 1253·2019-08-28 18:16
閱讀 1656·2019-08-27 10:56
閱讀 1022·2019-08-26 17:23