【原创】探索学习NodeJs内存管理

加深对NodeJs内存管理的理解,更高效地使用内存来应对服务器端大量请求,避免长时间运行导致的内存泄漏。

相关文章

目录

  • Node常驻内存模型
  • v8堆内存分配
  • v8垃圾回收
  • v8内存限制
  • 堆外内存

Node常驻内存模型

node常驻内存模型

结合上图掌握node常驻内存几点:

  • 常驻内存主要分为堆内存和堆外内存,堆内存由v8管理,堆外内存由使用对象对应模块的C++程序管理

  • 查看常驻内存

    console.log(process.memoryUsage())
    

    输出:

    { rss: 21475328,
    heapTotal: 7159808,
    heapUsed: 4358568,
    external: 8224 }
    

    rss为常驻内存,是分配给该进程的物理内存
    heapTotal为v8管理的堆内存当前可以分配的大小
    heapUsed为v8管理的堆内存当前已使用的大小
    external为V8管理的,绑定到Javascript的C++对象的内存使用情况

    rss包含v8管理的堆内存和堆外内存,会随着程序运行动态申请更多的内存

  • 查看v8管理的堆内存

    const v8 = require('v8')
    console.log(v8.getHeapSpaceStatistics())
    

    输出:

    [ { space_name: 'new_space',
      space_size: 2097152,
      space_used_size: 608784,
      space_available_size: 422384,
      physical_space_size: 2097152 },
    { space_name: 'old_space',
      space_size: 2945024,
      space_used_size: 2564512,
      space_available_size: 97400,
      physical_space_size: 2945024 },
    { space_name: 'code_space',
      space_size: 2097152,
      space_used_size: 1223168,
      space_available_size: 0,
      physical_space_size: 2097152 },
    { space_name: 'map_space',
      space_size: 544768,
      space_used_size: 282744,
      space_available_size: 0,
      physical_space_size: 544768 },
    { space_name: 'large_object_space',
      space_size: 0,
      space_used_size: 0,
      space_available_size: 1491770880,
      physical_space_size: 0 } ]
    

    space_name与常驻内存模型中描述的对应,每个space简单介绍如下,引用于https://amsimple.com/blog/article/41.html

    • new_space: 除去部分大对象(大于0.5MB),大部分的新对象都诞生在new_space
    • old_space: 大部分是从new_space中晋升过来的
    • map_space: 所有在堆上分配的对象都带有指向它的隐藏类的指针,隐藏类保存在map_space

      隐藏类主要目的是为了优化对象访问速度,因为JS是动态类型语言,编译后,无法通过内存相对偏移快速访问属性,而借助隐藏类可以优化对象属性访问速度

    • code_space: 代码对象,会分配在这,唯一拥有执行权限的内存
    • large_object_space: 大于0.5MB的内存分配请求,会直接归类为large_object_space,垃圾回收时不会被移动或复制,提高效率

对常驻内存的介绍完毕,后文将结合示例探索v8的内存管理。

v8堆内存分配

通过在每次使用内存后打印堆内存的使用量,来分析堆内存是如何分配的。

示例一

const v8 = require('v8')

function format (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}

function showMem (index) {
  const heapStat = v8.getHeapSpaceStatistics()
  console.log(
    `loop ${index}:  ` +
    heapStat.map(
      ({ space_name, space_used_size }) => `${space_name}: ${format(space_used_size)}`
    ).join(',')
  )
};

function useMem () {
  return new Array(32 * 1024).fill(0)
};

let i = -1
const arr = []
while (++i < 10) {
  showMem(i)
  arr.push(useMem())
}

输出:

loop 0:  new_space: 0.83 MB,old_space: 2.15 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 1:  new_space: 0.74 MB,old_space: 2.44 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 2:  new_space: 1.23 MB,old_space: 2.44 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 3:  new_space: 1.73 MB,old_space: 2.44 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 4:  new_space: 1.73 MB,old_space: 2.91 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 5:  new_space: 0.74 MB,old_space: 3.67 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 6:  new_space: 1.23 MB,old_space: 3.67 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 7:  new_space: 1.73 MB,old_space: 3.67 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 8:  new_space: 1.73 MB,old_space: 3.92 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 9:  new_space: 2.22 MB,old_space: 3.92 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB

从0-4次循环来看,new_space内存每次增加0.5M左右,而其他space内存基本上没发生变化;4-5次循环,new_space不变,old_space增加了0.5M;5-6次循环,new_space下降,old_space增加,new_space中部分对象转移到了old_space。

根据以上结果,应该能得出结论,新对象刚开始分配在new_space中,在一定情况下,new_space中的对象会转移到old_space中。

示例二

修改useMem方法,调整对象的大小为0.5M,如下:

function useMem () {
  return new Array(64 * 1024).fill(0)
};

输出:

loop 0:  new_space: 0.58 MB,old_space: 2.15 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.00 MB
loop 1:  new_space: 0.58 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 0.50 MB
loop 2:  new_space: 0.59 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 1.00 MB
loop 3:  new_space: 0.59 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 1.50 MB
loop 4:  new_space: 0.60 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 2.00 MB
loop 5:  new_space: 0.60 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 2.50 MB
loop 6:  new_space: 0.60 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 3.00 MB
loop 7:  new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 3.50 MB
loop 8:  new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 4.00 MB
loop 9:  new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 4.50 MB

从结果上看,除了large_object_space,其他space基本没发生变化,说明对于大于0.5MB左右的对象直接分配到large_object_space。

v8垃圾回收

修改执行方法,让对象得以释放,观察垃圾回收日志

示例一

function useMem () {
  new Array(32 * 1024).fill(0)
};

let i = -1
while (++i < 100000) {
  showMem(i)
  useMem()
}

将代码保存为文件test3.js,执行命令 node --trace_gc test3.js,可以打印出每次垃圾回收日志。部分日志如下:

loop 7732:  new_space: 3.69 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
[8716:000001CD96DFC300]     7669 ms: Scavenge 9.8 (17.8) -> 6.1 (17.8) MB, 0.3 / 0.0 ms  allocation failure
loop 7733:  new_space: 0.25 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7734:  new_space: 0.74 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7735:  new_space: 1.23 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7736:  new_space: 1.73 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7737:  new_space: 2.22 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7738:  new_space: 2.71 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7739:  new_space: 3.20 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
loop 7740:  new_space: 3.69 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB
[8716:000001CD96DFC300]     7676 ms: Scavenge 9.8 (17.8) -> 6.1 (17.8) MB, 0.2 / 0.0 ms  allocation failure
loop 7741:  new_space: 0.25 MB,old_space: 4.63 MB,code_space: 1.23 MB,map_space: 0.28 MB,large_object_space: 0.00 MB

可以清楚地看到,new_space的内存到达某个值时,触发了垃圾回收,new_space中未引用的对象内存被清空。new_space中的GC是比较频繁的,里面大部分对象存活期较短,实际采用的算法为Scavenge。

Scavenge算法将new_space的总空间一分为二,只使用其中一个,另一个处于闲置,等待垃圾回收时使用。使用中的那块空间称为From,闲置的空间称为To。当新生代触发垃圾回收时,V8将From空间中所有应该存活下来的对象依次复制到To空间。

示例二

将对象的大小设置得大一些,如下:

function useMem () {
  new Array(1024 * 1024).fill(0)
};

部分输出:

[72940:0000020B2D72B170]     2513 ms: Mark-sweep 108.1 (115.5) -> 28.0 (35.4) MB, 0.5 / 0.0 ms  (+ 5.7 ms in 6 steps since start of marking, biggest step 3.7 ms, walltime since start of marking 47 ms) finalize incremental marking via stack guard GC in old space requested

与上一个例子中触发的GC不同,这里触发了增量标记算法的GC。在old_space中GC的算法主要为标记清除(Mark-Sweep)、标记整理(Mark-Compact)和增量标记(Incremental Marking)。

标记清除:当GC触发时,V8会将需要存活对象打上标记,然后将没有标记的对象,也就是需要死亡的对象,全部清除
标记整理:标记清除会导致可使用的内存不连续,因此在标记清除的基础上,标记整理将所有存活对象往一端移动,使内存空间紧挨
增量标记:v8在垃圾回收阶段,程序会被暂停,增量标记将标记阶段分为若干小步骤,每个步骤控制在5ms内,每运行一段时间标记动作,就让JavaScript程序执行一会儿,如此交替,一定程度上避免了长时间卡顿

小结

结合v8堆内存分配和v8垃圾回收的示例,初步感受了下v8的内存管理,关于内存管理的算法讲解,网上有大量详细的文章,感兴趣的可以通过文末的拓展学习参考中推荐的文章深入学习。

v8内存限制

示例一

function useMem () {
  return new Array(20 * 1024 * 1024).fill(0)
};

let i = -1
const arr = []
while (++i < 10) {
  showMem(i)
  arr.push(useMem())
}

部分结果:

loop 8:  new_space: 0.61 MB,old_space: 2.18 MB,code_space: 1.17 MB,map_space: 0.27 MB,large_object_space: 1280.00 MB
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

在第9次循环时,提示堆内存溢出。

默认情况下,V8为堆分配的内存不超过1.4G:64位系统1.4G,32位则仅分配0.7G。新生代内存的最大值在64位系统和32位系统上分别为32MB和16 MB。可通过启动参数--max-old-space-size设置老年代内存大小,可通过--max-semi-space-size可设置新生代内存大小的一半。

堆内存为什么有限制?
内存太大,V8在GC时将要耗费更多的资源和时间,可能导致进程暂停时间过长。

示例二

将示例一保存为test4.js,执行命令node --max-old-space-size=2048 test4.js

最后部分结果:

loop 8:  new_space: 0.69 MB,old_space: 2.23 MB,code_space: 1.17 MB,map_space: 0.28 MB,large_object_space: 1280.00 MB
loop 9:  new_space: 0.69 MB,old_space: 2.23 MB,code_space: 1.17 MB,map_space: 0.28 MB,large_object_space: 1440.00 MB

可以看到最后占用的内存超过了之前的限制,程序正常执行。

堆外内存

使用Buffer对象,查看rss变化

function format (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}

function showMem (index) {
  const { rss, heapTotal } = process.memoryUsage()
  console.log(
    `loop ${index}:  ` +
    `rss: ${format(rss)}, heapTotal: ${format(heapTotal)}`
  )
};

function useMem () {
  return Buffer.alloc(200 * 1024 * 1024).fill(0)
};

let i = -1
const arr = []
while (++i < 10) {
  showMem(i)
  arr.push(useMem())
}

结果:

loop 0:  rss: 20.55 MB, heapTotal: 6.83 MB
loop 1:  rss: 220.64 MB, heapTotal: 6.83 MB
loop 2:  rss: 420.68 MB, heapTotal: 6.83 MB
loop 3:  rss: 621.07 MB, heapTotal: 9.33 MB
loop 4:  rss: 821.07 MB, heapTotal: 9.33 MB
loop 5:  rss: 1021.07 MB, heapTotal: 9.33 MB
loop 6:  rss: 1222.50 MB, heapTotal: 9.33 MB
loop 7:  rss: 1422.50 MB, heapTotal: 9.33 MB
loop 8:  rss: 1622.50 MB, heapTotal: 9.33 MB
loop 9:  rss: 1822.51 MB, heapTotal: 9.33 MB

rss超过了堆内存限制,且v8的堆内存基本没有发生改变,说明Buffer对象的内存在堆外内存中,不受v8内存限制。

当需要操作大文件时,受v8内存限制无法直接全部读取到内存中,如果不需要对文件进行操作,应该直接使用流读取,并且直接写入到其他管道中。如果需要对文件内容操作,应该先标准化文件内容,然后将文件读取到Buffer中,再一部分一部分地操作。

拓展学习参考

垃圾回收算法:

本文参考资源如下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容