问题由来
因为业务的需求,某 Node.js 性能平台用户需要调节新生代大小,Node.js 的启动参数里面的max_semi_space_size
可以设置新生代堆空间的大小。
node --v8-options | grep max_semi -A 3 -B 2
--min_semi_space_size (min size of a semi-space (in MBytes), the new space consists of twosemi-spaces)
type: int default: 0
--max_semi_space_size (max size of a semi-space (in MBytes), the new space consists of twosemi-spaces)
type: int default: 0
--semi_space_growth_factor (factor by which to grow the new space)
type: int default: 2
相关文档里该值是一个以MB
为单位的整数,并没有其他特别的约束。
然而,当用户设置 max_semi_space_size
为200时,Node.js 性能平台 GC Trace分析
结果显示 inactive new_space semispace:256MB
,说明新生代中未使用的那部分是256MB(有关GC的一些知识可以参阅文档最后的列表),如下图所示。
我们也线下验证了一下:
- 当设置
max_smi_space_size
为100,110时,inactive new_space semispace:128MB
。 - 当设置
max_smi_space_size
为50,60时,inactive new_space semispace:64MB
。
那么问题来了,是 Node.js 性能平台的bug?还是v8引擎本身的设计就是如此?
问题定位
其实,从最终64/128/256,这些数值就能推测到,设计就是如此。在计算机的世界里,2的整数次幂会带来很多方便。
还是老方法,既然文档里没有明确说明,而 Node.js 性能平台运行时在该部分功能跟社区运行时是完全一致的,那么只能去代码里找原因了。
一番操作之后,在 deps/v8/src/heap/heap.cc
中找到函数:
这里摘抄一点max_semi_space
相关代码,相关注释直接写到代码里面了,感兴趣的同学建议去看看完整代码。
bool Heap::ConfigureHeap(size_t max_semi_space_size, size_t max_old_space_size,
size_t code_range_size) {
if (HasBeenSetUp()) return false;
// Overwrite default configuration.
// 未设置 max_semi_space_size 时,默认值是 0
if (max_semi_space_size != 0) {
max_semi_space_size_ = max_semi_space_size * MB;
}
...
// If max space size flags are specified overwrite the configuration.
// 命令行 --max_semi_space_size 设置的新生代大小是通过 FLAG_max_semi_space_size 传递到v
if (FLAG_max_semi_space_size > 0) {
max_semi_space_size_ = static_cast<size_t>(FLAG_max_semi_space_size) * MB;
}
...
/*
ROUND_UP 的定义:
// Round up n to be a multiple of sz, where sz is a power of 2.
#define ROUND_UP(n, sz) (((n) + ((sz) - 1)) & ~((sz) - 1))
*/
// 操作系统相关的内存页大小,我的ubuntu16.04上该值是 512KB
if (Page::kPageSize > MB) {
max_semi_space_size_ = ROUND_UP(max_semi_space_size_, Page::kPageSize);
...
}
// 该参数默认是false
if (FLAG_stress_compaction) {
// This will cause more frequent GCs when stressing.
max_semi_space_size_ = MB;
}
// The new space size must be a power of two to support single-bit testing
// for containment.
// 关键点在这里
// 为什么 50 变成了 64, 100/120 变成了128, 200 变成了 256
// 下面的函数 RoundUpToPowerOfTwo32 就是这个变化的原因。
/*
uint32_t RoundUpToPowerOfTwo32(uint32_t value) {
DCHECK_LE(value, uint32_t{1} << 31);
if (value) --value;
// Use computation based on leading zeros if we have compiler support for that.
#if V8_HAS_BUILTIN_CLZ || V8_CC_MSVC
return 1u << (32 - CountLeadingZeros32(value));
#else
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
#endif
}
*/
max_semi_space_size_ = base::bits::RoundUpToPowerOfTwo32(
static_cast<uint32_t>(max_semi_space_size_));
// 这里是 min_semi_space_size 的设置,这里不讨论
if (FLAG_min_semi_space_size > 0) {
...
}
// 新生代初始大小是 min_semispace_size,如果需要,那么增大到 max_semi_space_size
initial_semispace_size_ = Min(initial_semispace_size_, max_semi_space_size_);
if (FLAG_semi_space_growth_factor < 2) {
FLAG_semi_space_growth_factor = 2;
}
...
configured_ = true;
return true;
}
问题结论
max_semi_space_size
设置看起来是个任意整数,但是实际使用中 v8 会把该值转换成一个不小于该值的2的整数次幂的值。也就是说:
-
max_semi_space_size
设置为 33, 34, ..., 64,最终结果都是 64MB。 -
max_semi_space_size
设置为 65, 66, ..., 128,最终结果都是 128MB。 - 依次类推
heap.cc 里面的注释是
// The new space size must be a power of two to support single-bit testing
// for containment.