node的内存控制

*本文内容部分来自《深入浅出node.js》

js作为一门脚本语言能在服务端大放异彩最重要的原因就是高性能的v8引擎,但是v8引擎也有一些限制,例如内存方面的限制,本文主要讲解v8引擎的内存控制。


node通过js来使用内存时只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB),这将会导致node无法操作大内存对象。造成这个问题的主要原因在于node基于v8构建,而v8限制了内存的用量。


在v8中,所有的js对象都是通过堆来分配的,当我们在代码中声明变量并赋值时,所使用的对象内存分配在堆中,如果已申请的堆空间内存不够分配新的对象时,将继续申请堆内存,直到堆的大小超过v8的限制为止。至于v8为何要限制堆的大小,原因是v8的垃圾回收机制。1.5GB的垃圾回收堆内存,v8做一次小的垃圾回收需要50ms以上,做一次非增量的垃圾回收则需要1s以上,而垃圾回收会引起js执行线程暂时停止执行,这种时候,应用的性能和响应能力会直线下降,因此,v8在设计的时候直接限制了堆内存的大小。接下来,我们深入了解一下v8的垃圾回收机制。


v8的垃圾回收机制


v8的垃圾回收策略主要基于分代式回收机制,因为在内存中,不同的对象存在的时间与生成的频率有很大的差别,所以在v8中,主要将堆内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代对象为存活时间较长的对象或常驻对象。使用--max-old-space-size命令可以设置老生代内存的最大值,使用--max-new-space-size可以设置新生代内存的最大值,这两个方法可以放宽v8的默认内存,但是这两个方法只能在node进程启动时指定,这意味着v8的使用内存无法根据使用情况自动扩充。新生代和老生代的内存限制可以在v8的源码中找到。新生代内存由两个reserved_semispace_size_所构成,而reserved_semispace_size_在64位系统和32位系统上分别为16MB和8MB,所以新生代内存在64位系统和32位系统中分别为32MB和16MB。v8堆内存最大保留空间为4*reserved_semispace_size_ + max_old_generation_size_,因此,默认情况下,v8堆内存最大值在64位系统上为1464MB,在32位系统上为732MB。

在v8的分代机制中,新生代和老生代使用的回收算法也不一致。新生代中对象主要通过Scavenge算法来进行垃圾回收,该算法采用复制的方式实现垃圾回收算法,它将堆内存一分为二,每一部分空间称为semispace,在这两个semispace空间中,只有一个处于使用中,称为From空间,另一个处于空闲状态,称为To空间。当我们分配对象时,在From空间中进行分配,而开始进行垃圾回收时,会检查From空间中的存活对象(具体表现为被引用对象),这些存活对象将被剪切到To空间,而非存活对象占用的空间将会被释放。完成这个过程后,From空间和To空间将会对调。简而言之,就是通过将存活对象在两个semispace空间之间进行剪切。这也是为什么新生代堆空间大小是2*reserved_semispace_size_。Scavenge的缺点是只能使用一半的堆空间,但是Scavenge只复制存活的对象,对于生命周期短的场景,存活对象只有很少一部分,所以Scavenge在时间效率上有有优异的表现,是典型的空间换取时间的算法。而新生代中对象的生命周期较短,比较适合这个算法。

新生代对象在经过多次剪切之后依然存活时,它将会被认为是生命周期较长的对象,这中对象随后会被移动到老生代中,采用新的算法管理。这种移动称为晋升。对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用超过25%。设置25%这个限制是因为当这次Scavenge回收完成之后,To空间将会变成From空间,接下来的内存分配将会在这个空间中完成,如果占用比过高,将会影响后续内存分配。

对于老生代的对象,主要使用Mark-Sweep&Mark-Compact算法进行垃圾回收。Mark-Sweep是标记清除的意思,在垃圾回收时,Mark-Sweep在标记阶段遍历堆中所有的对象,并标记活着的对象,在随后的清除阶段,清除掉所有未标记的对象。而在老生代中,大部分是生存周期较长的对象或者常驻对象,所以采用这种方式比较高效。




但是Mark-Sweep存在一个问题,就是在进行一次标记清除之后,内存空间会出现不连续的状态,这种内存碎片会对后续内存分配造成问题,因此Mark-Compact算法被提出来。Mark-Compact是标记整理的意思,在整理过程中,将活着的对象往一边移动,移动完成之后,清理掉边界外的内存。如上图所示,将会留出大块的内存空间。

因为相对前者后者的回收速度是比较慢的,因为它有对象的移动,而mark-sweep没有对象移动,所以,效率会比较高。V8在清理时主要会使用Mark-sweep,在空间不足以对新生代中晋升过来的对象进行分配时才会使用Mark-compact。

以上提到的几种垃圾回收算法都需要将应用逻辑停下来,等完成垃圾回收后再恢复继续执行,即“stop-the-world”,在这点上V8也做了优化。一次小的垃圾回收只收集新生代,而新生代存活对象少,全停顿影响不大。而对于老生代,则进行增量标记,拆分成许多小“步进”,每做完一“步进”,就让应用逻辑执行一会,同时运用延迟清理和增量式整理来优化,具体不再展开。 


查看垃圾回收日志


在启动时添加--trace_gc参数,可以在垃圾回收时从标准输出中打印垃圾回收的日志信息。






我截取了其中一部分的截图,可以看到Scavenge回收占据了大部分。


内存指标


node提供了process.memoryUsage()方法查看内存使用情况




rss是进程的常驻内存部分,heaptotal是堆中总共申请的内存量,heapused是堆中目前使用的内存量。从结果来看,堆中内存占用量总是小于进程的常驻内存用量,这是因为node的内存使用并非都是通过V8进行分配的,这种不是通过V8分配的内存叫堆外内存,堆外内存主要是Buffer对象引起,在本文中暂时不解释Buffer对象。但是这意味着堆外内存可以用来突破内存限制的问题。


内存泄漏及解决方案


node运行在服务端,因此对内存泄露十分敏感,通常造成内存泄漏的原因有这么几个:


1.缓存

缓存在应用中的作用很重要,可以十分有效的节省资源。但是一个对象一旦被当做缓存使用,它将常驻在老生代中,缓存中存储的键越多,长期存活的对象也越多,这将导致在垃圾回收机进行扫描和整理的时候做很多无用功。因此我们需要对缓存做限制,一种可行性方案是将缓存键记录在数组中,一旦超过数量,就以先进先出的方式进行淘汰。对于大量缓存,最好是使用进程外的缓存比如Redis之类的来解决。


2.队列

队列的堆积会导致相关作用域得不到释放,内存占用不会回落,造成内存泄露。对于这种场景,我们之前介绍过的Bagpipe提供了超时模式和拒绝模式,可以有效的解决该问题。


3.作用域与闭包

var foo =function(){  
    var local='local var ';  
    var bar =function(){  
        var local='another var';  
        var baz = function(){  
            console.log(local);  
        };  
        baz();  
    };  
    bar();  
};  
foo();  

在上面的例子中,baz在打印local变量时,会在baz范围内找,如果没有,就会往上一级,找到bar里面的local,打印出another var,如果删除another var ,则会打印local var的值。这就是作用域链,如果在本级找不到就会往上级寻找,直到全局作用域。所以经常我们的代码里面有一些引用如果不清除就可能会导致内存泄漏,对于这种情况,建议在操作完成之后进行释放。可以使用delete操作符或者赋值为空来解决,在v8里面使用delete操作符会影响v8的优化,所以通过赋值为空的方式来解决是最好的。

对于闭包,实现外部作用域访问内部作用域中的变量的方式就叫闭包,通常是通过一个高阶函数来实现的,也就是把函数作为参数传递,对于这种情况,我们可以在操作完成之后手动释放内部作用域。



在 NUMA(Non-Uniform Memory Access,非一致性内存访问)架构的系统中,**内存 node** 是一个核心概念。我们来详细解释“内存 node”是什么、它如何工作,以及它与 Linux 内核内存管理的关系。 --- ### ✅ 回答问题:什么是内存 node? > **内存 node(Memory Node)** 是指在一个 NUMA 系统中,由一个或个 CPU 核心直接连接的一块本地物理内存区域。每个 node 是独立的内存池,有自己的内存控制器,CPU 访问本地 node内存比访问其他 node内存更快。 简单来说: - 一台路服务器有个 CPU 插槽(Socket),每个 CPU 自带本地内存。 - 每个 CPU + 其直连内存构成一个 **NUMA node**。 - 不同 node 之间的内存访问延迟不同 → 称为“非一致性内存访问”。 --- ### 🔍 Linux 中的内存 node 结构 Linux 使用 `struct pglist_data`(常简写为 `pg_data_t`)表示一个内存 node: ```c typedef struct pglist_data { struct zone node_zones[MAX_NR_ZONES]; // 包含个 zone(如 DMA、NORMAL) struct zonelist node_zonelists[MAX_ZONELISTS]; // 备选内存分配策略 int node_id; // node 编号,如 0, 1 unsigned long node_start_pfn; // 起始 PFN unsigned long node_spanned_pages; // 总页数 unsigned long node_present_pages; // 可用页数 } pg_data_t; ``` 所有 node 组织成一个全局数组: ```c extern struct pglist_data *node_data[]; ``` 可以通过 `NODE_DATA(n)` 宏获取第 `n` 个 node 的数据结构。 --- ### 📈 示例:查看系统的 NUMA 架构 #### 1. 使用 `numactl` 查看 node 信息: ```bash numactl --hardware ``` 输出示例: ``` available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 node 0 size: 8192 MB node 1 cpus: 4 5 6 7 node 1 size: 8192 MB node distances: node 0 1 0: 10 20 1: 20 10 ``` 这说明系统有两个 NUMA node: - node 0:CPU 0~3,内存 8GB,距离本 node 为 10,访问 node 1 延迟为 20。 - node 1:CPU 4~7,内存 8GB。 > 数字越小表示延迟越低。 --- #### 2. 查看 `/sys` 文件系统中的信息 ```bash ls /sys/devices/system/node/ # 输出:node0 node1 cat /sys/devices/system/node/node0/meminfo ``` 输出类似: ``` Node 0 MemTotal: 8192000 kB Node 0 MemFree: 2048000 kB ``` --- ### 💡 内存分配时的 node 行为 Linux 内核会尽量实现 **本地内存分配(local allocation)**,即: > “哪个 CPU 分配内存,就优先从它所属的 node 上分配。” 这是通过 `zonelist` 实现的。例如,在 node 0 上分配内存时,内核首先尝试 `node0:ZONE_NORMAL`,失败后再尝试 `node1:ZONE_NORMAL`。 你可以通过 `__GFP_THISNODE` 标志强制只使用本地 node。 --- ### ⚙️ 如何指定内存分配到某个 node? #### 方法一:使用 `kmalloc_node()` 或 `vmalloc_node()` ```c void *ptr = kmalloc_node(1024, GFP_KERNEL, 1); // 在 node 1 上分配 1KB ``` #### 方法二:使用 `alloc_pages_node()` ```c struct page *page = alloc_pages_node(0, GFP_KERNEL, 0); // 分配一页到 node 0 ``` #### 方法三:用户空间控制 —— `numactl` ```bash numactl --membind=0 --cpunodebind=0 ./my_program ``` 让程序运行在 node 0 上,并且只能使用 node 0 的内存。 --- ### 🧠 为什么需要关注 memory node? | 场景 | 原因 | |------|------| | 高性能计算(HPC) | 减少跨 node 访问延迟 | | 数据库(如 MySQL、Redis) | 绑定到特定 node 提升缓存命中率 | | 虚拟化(KVM/QEMU) | 为 VM 分配固定 node 内存以提高性能 | | DPDK/网络处理 | 使用 `hugepages` 并绑定到本地 node | --- ### ❗ 注意事项 - 单 node 系统(UMA)也存在 `node0`,但只是一个抽象。 - 错误的内存绑定可能导致 **远端内存访问(remote memory access)**,性能下降可达 30% 以上。 - 使用 `vmstat`, `numastat` 可监控跨 node 分配情况: ```bash numastat -v ``` 输出示例: ``` Per-node system memory usage (in MBs): Node 0 Node 1 Total: 8192 8192 Local: 7000 6000 Remote: 1192 2192 ← 跨节点访问越越差 ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值