内存控制总结-1
发布于 5 年前 作者 halu886 3896 次浏览 来自 分享

该文章阅读需要7分钟,更多文章请点击本人博客halu886

V8的垃圾回收机制和内存限制

Nodejs类似Java,存在自动垃圾回收机制进行自动内存管理。当使用Javascript的场景从客户端切换到了服务器端时,内存管理的好坏,垃圾回收的是否优良,都与Nodejs的Javascript执行引擎v8息息相关。

Node 与 V8

Node 的发展离不开 V8 ,当初 Node 选型时,正值第三次浏览器大战,Chrome的V8引擎以卓越的性能遥遥领先。正因V8的卓越性能,node才能在服务端用Js实现高性能后端服务程序。

Node在执行上受益于V8,并且能够享受V8的迭代升级带来的更好的性能,以及对ES5/ES6语法特性,但是同时也受到V8的限制。

V8的内存限制

在V8,所有Javascript的对象内存都是基于堆的分配。并且Node提供了V8内存使用量的查看方式。

$ node
> process.memoryUsage();
{ rss:14754135,
  heapTotal:7480983,
  heapUsed: 4531543 }

heapTotal是堆总共申请的内存,heapUsed是正在被使用堆的内存。

1

当申明和定义变量时则会使用堆中的内存。如果已经存在空闲内存不够,则会申请新的堆内存,直到超过V8的内存限制。

至于为什么要限制内存的大小,表层原因是浏览器不需要这么大的内存。深层原因是V8的垃圾回收的限制,以1.5G内存为例,当V8进行小的垃圾回收时,耗时在50毫秒左右,做一次非增量式的垃圾回收甚至需要1s+,并且对于内存清理是会阻塞线程,在这样的事件开销下,应用的性能和响应速度都会直线下降,所以直接限制堆内存是一个好的选择。

不过,能在Node在启动时传递`–max-old-space-size或者–max-new-space-size来调整内存大小。

这个选项时在Node在启动时生效,一旦生效不能动态更改。当遇上内存不够,可以用以上方法放宽V8默认的内存限制,防止出现内存溢出,进程崩溃的情况。

V8的垃圾回收机制

在展开垃圾回收机制前,先简略介绍一下V8用到的各种垃圾回收算法。

V8主要的垃圾回收算法

V8的垃圾回收算法是基于分代式垃圾回收算法。没有任何一种算法能够解决所有的情况,因为在实际的应用中,对象的生存周期长短不一,不同的算法只能在特定情况下取得最好的效果。统计学对现代垃圾回收算法起到了较大的作用,并且根据对象的存活时间将垃圾回收进行不同的分代,然后分别对不同的分代内存采用更高效的效果,

V8的内存分代

在V8中,主要将内存分为新生代和老生代,新生代的对象存活时间较短,老生代为存活时间较长或者常驻内存的对象

2

V8堆的大小就是新生代的内存大小加上老生代的内存大小,并且可以通过--max-old-space--max-new-space设置老生代和新生代的内存大小。不过必须要在进程初始化的时候指定,不能动态分配。当内存分配过程中超过极限值时这会引起进程出错。

在默认情况下,在64位的计算机和32位计算机的大小限制分别约为1.4G和0.7G,这个限制可以在源码中找到。Page::kPageSize的值为1MB。可以看到,老生代的设置为64位系统下为1400MB,在32位系统下为700MB

// semispce_size_ should be a power of 2 and old_generation_size_ should be
// a multiple of Page::kPageSize
#if defined(V8_TARGET_ARCH_X64)
#define LUMP_OF_MEMORY(2*MB)
    code_range_size_(512*MB),
#else
#define LUMP_OF_MEMORY MB
    code_range_size_(0),
#endif
#if defined(ANDROID)
        reserved_semispace_size_(4*Max(LUMP_OF_MEMORY,Page::kPageSize)),
        max_semispace_size_(4*Max(LUMP_OF_MEMORY,Page::kPageSize)),
        initial_semispace_size_(Page::kPageSize),
        max_old_generation_size_(192*MB),
        max_executable_size_(max_old_generation_size_),
#else
        reserved_semispace_size_(8*Max(LUMP_OF_MEMORY,Page::kPageSize)),
        max_semispace_size_(8*Max(LUMP_OF_MEMORY,Page::kPageSize)),
        initial_semispace_size_(Page::kPageSize),
        max_old_generation_size_(700ul*LUMP_OF_MEMORY),
        max_executable_size_(2561*LUMP_OF_MEMORY),
#endif

对于新生代的内存,是由两个reserved_semispace_size_决定的,reserved_semispace_size_在64位和32位机器上分别为16M和8M。所有新生代在64位和32位上的内存分别为32M和16M。

V8堆内存的最大保留空间可以从下面代码看出来,其公式为 4 * reserved_semispace_size_ + max_old_generation_size:

// Returns the maximum amount of memory reverved for the heap. For
// the young generation,we reserve 4 times the amount needed for a
// semi space. The young generation consists of two semi spaces and
// we reserve twice the amount needed for those in order to ensure
// that new space can be aligned to its size
intptr_t MaxReserved(){
    return 4 * reserved_semispace_size_ + max_old_generation_size_;
}

因此默认情况下,V8堆内存在64位机器下为1464MB,在32位机器上为732MB。

Scavenge 算法

在分代的基础上,新生代是使用Scavenge算法进行回收,在Scavenge算法具体实现中,是使用cheney算法,是C.J.Cheney在1970首次发布在ACM上。

Cheney算法是一种是使用复制的垃圾回收算法,主要是使用将内存一分为二,都称为semispace,一个是在使用中,一个是空闲中。使用中的semispace称为from,空闲中的semispace称为to,每次进行垃圾回收时,将from中存在引用的复制到to中,然后清空from,然后两者再互换位置。

Scavenge算法的缺陷是只能使用堆内存的一半,但是这个由于划分空间和复制的导致,但是由于新生代中,每次垃圾回收时存活的对象比较少,所以在性能上有有优异的表现。

Scavenge算法是典型的空间换时间,所以无法大范围的使用,但是因为新生代的生命周期比较短,所以恰恰适合这个算法。

V8堆内存示意图

3

堆内存的大小实际上是两个semispace和老生代所用内存之和。

当一个对象经过多次复制仍然存活,则会被移动到老生代中,这个过程被称为晋升。

在单纯的Scavenge中,会将from中的存活对象复制到to中,然后对两个semispace进行翻转,但是在分代式下,每次复制前都会进行检查,如果存活周期较长,则进行对象晋升。

对象晋升的主要条件是两个,是否经历过Scavenge,或者To空间的内存占用比超过限制。

在默认情况下,V8分配对象空间是在from空间,然后再scavenge时,判断这个对象是否经历过Scavenge回收。如果是,则将对象晋升为老生代,否则就复制到to空间。

4

另一个是内存占用比,当进行复制时,如果to空间内存占用比超过25%,则直接晋升到老生代中。

5

使用25%进行限制,是因为复制完后,to则会翻转为from,如果占用比过高,则会影响后面的内存分配。

当对象在老生代中,则会接收新的回收算法处理。

Mark-Sweep & Mark-Compact

对于老生代的对象来说,存活对象比较多,如果再使用scavenge则会有两个问题,复制存活对象效率太低,同时浪费一般的内存空间。所以V8采用Mark-Sweep与Mark-Compact相结合的方式。

Mark-Sweep会将没被引用的对象清除,由于老生代中,未存活的对象只占很小的一部分,Mark-Sweep更合适,以下是示意图:

6

Mark-Sweep会造成一个问题,当进行清理完后,所造成的内存空间是不连续的,如果此时需要分配一个比较大对象,碎片空间是无法完成此次分配的,则会提前触发垃圾回收,但是这是不必要的。

为了解决Mark-Sweep的问题,则Mark-Compact在这个基础上被提出,在标记的过程中,将存活的对象向一端移动,移动完毕后,统一将死亡对象回收。如图所示,白色格子为存货对象,浅色为空间,深色为死亡对象。

7

下图是V8算法的简单对比。

8

从表中可以看出,Mark-Compact需要移动对象,所以比较耗时。所以取舍上,V8主要使用Mark-Sweep。只有当内存不能分配新生代对象对象时,才执行Mark-Compact。

Incremental Marking

为了避免Javascript应用逻辑运算和垃圾回收器结果不一致,三种算法执行时会在垃圾回收时将Javascript应用逻辑暂停,这个逻辑称为全停顿(stop-the-world)。每次小的垃圾回收,新生代配置较少,影响不大,然而老生代分配的比较多,一旦阻塞对业务影响挺大的,需要优化。

为了降低全堆垃圾回收的卡顿,将一口气的标记动作改为了增量标记(incremental marking),也就是每标记一小部分,执行一下Javascript应用逻辑,然后再继续标记,直至标记阶段完成。

9

V8经过增量标记优化后,最大延迟时间减小到原来的1/6了。

V8后续还引入了延迟清除(lazy sweeping)和增量整理(incremental compaction),让清理和整理动作也变为增量式的了。

以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)

1 回复

你说的还行,这个事情我比较认可 现在我开始说了 ** 哈哈哈**

  1. 现在我们
  2. 你是谁
回到顶部