1 V8內存管理
1.1 V8n內存限制
- 64位系統可用1.4G內存
- 32位系統可用0.7G內存
1.2 V8內存管理
- JS對象都是通過V8進行分配管理內存的
- process.memoryUsage()返回一個對象,包含了Node進程的內存佔用信息
console.log(process.memoryUsage());
//結果如下:
{
rss: 19550208, // 所有內存佔用,包括指令區和堆棧
heapTotal: 5533696, // “堆”佔用的內存,包括用到的和沒用到的
heapUsed: 2645088, // 用到的堆的部分。判斷內存泄漏,以heapUsed字段爲準
external: 778349 // V8引擎內部的C++對象佔用的內存
}
- 所有內存佔用結構圖:
------------------------------------------
| |
| Resident Set 所有內存佔用 |
| |
| ---------------------------- |
| | 代碼區域 Code Segment | |
| ---------------------------- |
| |
| ---------------------------- |
| | 棧(Stack):本地變量、指針 | |
| ---------------------------- |
| |
| ---------------------------- |
| | HeapTotal(堆):對象,閉包 | |
| | | |
| | --------------------- | |
| | |heapUsed 使用到的堆。| | |
| | | 判斷內存泄漏, | | |
| | | 以heapUsed字段爲準| | |
| | --------------------- | |
| | | |
| ---------------------------- |
------------------------------------------
- 上圖舉例:
-
var a = {name:‘yuhua’};這句代碼會做如下幾步:
- 將這句代碼放入“代碼區域 Code Segment”
- 將變量a放入“棧(Stack):本地變量、指針”
- 將{name:‘yuhua’}放入“HeapTotal(堆):對象,閉包”
-
注意:基本數據類型都在棧中,引用類型都在堆中
-
1.3 爲何限制內存大小
- 因爲V8垃圾收集工作原理導致的,1.4G內存完全一次垃圾收集需要1s以上
- 這個垃圾回收這段時間(暫停時間)成爲Stop The World,在這期間,應用的性能和響應能力都會下降
1.4 如何打開內存限制
- 一旦初始化成功,生效後不能修改
- -max-new-space-size 最大 new space 大小,執行scavenge回收,默認16M,單位KM
- -max-old-space-size,最大 old space 大小,執行MarkSweep回收,默認1G,單位MB
2.V8的垃圾回收機制
- V8是基於分代的垃圾回收
- 不同代垃圾回收機制也不一樣
- 按存貨的時間分爲新生代和老生代
2.1 分代
- 年齡小的是新生代,由From區域和To區域兩個區域組成
- 在64位系統裏,新生代內存是32M,From區域和To區域各佔16M
- 在32位系統裏,新生代內存是16M,From區域和To區域各佔8M
- 年齡大的是老生代,默認情況下:
- 64位系統下老生代內存是1400M
- 32位系統下老生代內存是700M
2.2 新生代的垃圾回收(新生代GCGC)
2.2.1 過程
- 新生代區域一分爲二,每個16M,一個使用,一個空閒
- 開始垃圾回收的時候,會檢查FROM區域中的存活對象,如果還活着,拷貝到TO空間,所有存活對象拷貝完後,清空(釋放)FROM區域
- 然後FROM和To區域互換
2.2.2 特點
-
新生代掃描的時候是一種廣度優先的掃描策略
- 什麼叫做廣度優先的掃描策略?這裏涉及到掃描指針跟分配指針
- 舉例:
- 假設全局變量下的變量A引用了變量B和變量C,變量B引用了變量D,變量E沒被引用
ROOT ↓ A E ↓→ → → ↓ ↓ B C ↓ D
- 那麼在to區域,存在一個掃描指針和分配指針,起初都指向最開始
- 然後A發現被全局引用,那麼A拷貝到to區域,這時候掃描指針指向A,分配指針指向後面一位
A ↑ ↑ SM FP
- 然後A又引用了B,這時候把B拷貝到to區域,放在A後面,掃描指針還是指向A,分配指針往後移動一位
A B ↑ ↑ SM FP
- 然後再看A又引用了C,這時候把C拷貝到to區域,放在B後面,掃描指針還是指向A,分配指針往後移動一位
A B C ↑ ↑ SM FP
- 然後發現A沒有引用其他了,那將掃描指針往後移動一位指向B
A B C ↑ ↑ SM FP
- 然後發現B沒有引用其他了,那再將掃描指針往後移動一位指向C
A B C ↑ ↑ SM FP
- 然後C也沒有引用其他了,還剩下D,D不存在引用,所以不需要移動到to區域,這時候清空FROM區域,然後將FROM和To交換。
- 這就完成了一次垃圾回收。
- 舉例:
- 什麼叫做廣度優先的掃描策略?這裏涉及到掃描指針跟分配指針
-
新生代的空間小,存活對象少
-
當一個對象經理多次的垃圾回收依然存活的時候,生存週期比較差的對象會被移動到老聲帶,這個移動過程被稱爲晉升或升級
- 經歷過5次以上的回收還存在
- TO的空間使用佔比超過25%,或者超大對象
-
瀏覽器的memory中可以通過拍快照看變量是否被垃圾回收
-
置爲undefined 或 null 都能將引用計數減去1
2.2.3 舉例
- 例一:
function Person(){
this.name = name;
}
let p1 = new Person('yuhua1');// 引用計數爲1
let p2 = new Person('yuhua2');// 引用計數爲1
// p1 p2 不會被自動銷燬,因爲全局(雖然是let),如果是局部,會自動銷燬
- 例二:
function Person(){
this.name = name;
}
let p1 = new Person('yuhua1');// 引用計數爲1
let p2 = new Person('yuhua2');// 引用計數爲1
setTimeout(function(){
p1 = null;// 3秒後引用計數減1,變成0,就銷燬,
}, 3000)
setTimeout(function(){
p1 = null;// 10秒後引用計數減1,變成0,就銷燬,
}, 10000)
- 例三:
function Person(name){
this.name = name;
}
let set = new Set();
let p1 = new Person('yuhua');//引用計數爲1
set.add(p1);//引用計數爲2
p1 = null;//引用計數減1,爲1,並不會銷燬,雖然p1確實變爲null了
//這時候怎麼銷燬p1,不能再將p1置爲null,這是沒用的。由於p1是被set引用,所以應該將set銷燬,這樣才能銷燬p1
set = null;//這時候p1引用計數減1,爲0,銷燬。同時set引用計數也爲0,也銷燬了
- 例四:
function Person(name){
this.name = name;
}
let PersonFactory = function(name){
let p = new Person(name);
return function(){
console.log(p);
}
}
let p1 = PersonFactory('zfpx');// 這裏的p引用計數爲1
p1();//這裏的p引用計數爲2
2.3 老生代的垃圾回收策略
2.3.1 基礎
- 老生代垃圾回收策略分爲兩種
- mark-sweep 標記清除
- 標記活着的對象,雖然清楚在標記階段沒有標記的對象,只清理死亡對象
- 會出現的問題:清除後內存不連續,碎片內存無法分配
- mark-compact 標記整理
- 標記死亡後會對對象進行整理,活着的左移,移動完成後清理掉邊界外的內存(死亡的對象)
- mark-sweep 標記清除
- 老生代空間大,大部分都是活着的對象,GC耗時比較長
- 在GC期間無法想聽,STOP-THE-WORLD
- V8有一個優化方案,增量處理,把一個大暫停換成多個小暫停 INCREMENT-GC
- 也就是把大暫停分成多個小暫停,每暫停一小段時間,應用程序運行一會,這樣垃圾回收和應用程序交替進行,停頓時間可以減少到1/6左右
2.3.2 過程
假設有10個大小的內存,內存佔用了6個,
- Mark-Sweep模式垃圾回收:
- 那麼會給每個對象做上標記:如下圖
A b C d E f 空 空 空 空 //對上面每個對象做上標記,大寫表示活着,小寫表示死了 //這時候,會存在一個問題,就是內存碎片無法使用,因爲小寫的內存沒有跟後面空空空空的內存放在一起,不能使用
- 這時候小寫(死)的都會被幹掉,只保留大寫(活)的,導致的問題就是內存碎片無法使用
- Mark-Compact模式垃圾回收:
- 將活的左移
A C E b d f 空 空 空 空
- 然後回收死了的區域
A C E 空 空 空 空 空 空 空
2.4 三種垃圾回收算法對比
回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空間開銷 | 少 | 少 | 雙倍空間(無碎片) |
是否移動對象 | 否 | 是 | 是 |
- V8老生代主要用Mark-Sweep,因爲Mark-Compact需要移動對象,執行速度不快。空間不夠時,纔會用Mark-Compact