V8垃圾回收分析

关于垃圾回收机制

由于JavaScript 的对象是在V8上生产的,所以生命周期也是由V8来控制,这里就牵扯到了垃圾回收。任何垃圾回收都会做以下三件事情:

  • 识别不再被引用的对象(Dead Objects)
  • 回收这些对象所占用的内存
  • 重新压碎整理碎片化的内存块(可选的)

分代机制

V8的垃圾回收机制也与其他主流语言一样(有垃圾回收机制的语言),使用的世代回收机制,主要有两个世代:新生代(Young generation)老年代(Old generation),V8在这个两个世代的前提下,又增加了独特的空间,用来存放专门的对象。以下是V8在内存管理中的所有空间:

  RO_SPACE,    // Immortal, immovable and immutable objects,
  NEW_SPACE,   // Young generation semispaces for regular objects collected with
               // Scavenger.
  OLD_SPACE,   // Old generation regular object space.
  CODE_SPACE,  // Old generation code object space, marked executable.
  MAP_SPACE,   // Old generation map object space, non-movable.
  LO_SPACE,    // Old generation large object space.
  CODE_LO_SPACE,  // Old generation large code object space.
  NEW_LO_SPACE,   // Young generation large object space.

这些空间组成了V8的heap管理并且在Isolate初始化时完成各自空间的初始化。
在了解每个世代的垃圾回收之前,我们先看一下V8的heap结构:
4ac4842bc4dde9cbb4da78e60704dd56ce63ed4d

新生代

新生代又分了两个SemiSpace(from和to),默认的初始化容量是1MB、最大容量8MB,这2个空间类似于Java中的Eden和Survivor区,这2个半区每次GC后,总有一个区是空的,为什么呢?

每次进行minor GC时,将from区中仍然存活的对象copy到to区,然后将from区的内存全部清除,然后设置两个区的page的flag,将FROM_PAGE设置为TO_PAGE,这样from区就有之有之前to区的存活对象,而to区经过交换数据后,没有存活的对象了,自然就空了。虽然新生代只有一半的空间在使用,但是新生代的对象,大部分生命周期都很短,这样做也是出于效率的考虑。用空间换时间,这样效率还是很高的。

cheneys-semispace-copy

那么V8内部是如何触发内存回收的呢?是监听Heap的内存大小?还是定期主动进行GC?我们通过代码一探究竟。
V8中的所有对象都是通过工程方法创建的。它有一个专门的工厂类(factory.h)用来创建所有的对象。

 Handle<FixedArray> NewFixedArray(
      int length, AllocationType allocation = AllocationType::kYoung);
Handle<PropertyArray> NewPropertyArray(
      int length, AllocationType allocation = AllocationType::kYoung);
MaybeHandle<String> NewStringFromOneByte(
      const Vector<const uint8_t>& str,
      AllocationType allocation = AllocationType::kYoung);
MaybeHandle<String> NewStringFromUtf8(
      const Vector<const char>& str,
      AllocationType allocation = AllocationType::kYoung);
Handle<Struct> NewStruct(InstanceType type,
                           AllocationType allocation = AllocationType::kYoung);
Handle<ByteArray> NewByteArray(
      int length, AllocationType allocation = AllocationType::kYoung);
Handle<Object> NewNumber(double value,
                           AllocationType allocation = AllocationType::kYoung);
Handle<JSObject> NewJSObject(
      Handle<JSFunction> constructor,
      AllocationType allocation = AllocationType::kYoung);

这里列出了主要使用的API,上述还有很多API没有列出,都是与创建V8对象相关的。每个对象的生成都会调用AllocateRawWithRetryOrFail这个方法

HeapObject AllocateRawWithRetryOrFail(
      int size, AllocationType allocation,
      AllocationAlignment alignment = kWordAligned) {
      AllocationResult alloc;
  HeapObject result = AllocateRawWithLightRetry(size, allocation, alignment);
  if (!result.is_null()) return result;

  isolate()->counters()->gc_last_resort_from_handles()->Increment();
  CollectAllAvailableGarbage(GarbageCollectionReason::kLastResort);
  {
    AlwaysAllocateScope scope(isolate());
    alloc = AllocateRaw(size, allocation, alignment);
  }
  if (alloc.To(&result)) {
    DCHECK(result != ReadOnlyRoots(this).exception());
    return result;
  }
  // TODO(1181417): Fix this.
  FatalProcessOutOfMemory("CALL_AND_RETRY_LAST");
  return HeapObject();
}

AllocateRawWithRetryOrFail会在调用AllocateRawWithLightRetryAllocateRaw

AllocateRawWithLightRetry->AllocateRaw->生成对象

AllocateRaw会针对 不同的AllocationType调用对应space生成对象。代码如下:

AllocationResult Heap::AllocateRaw(int size_in_bytes, AllocationType type,
                                   AllocationAlignment alignment) {
  DCHECK(AllowHandleAllocation::IsAllowed());
  DCHECK(AllowHeapAllocation::IsAllowed());
  DCHECK(gc_state_ == NOT_IN_GC);
#ifdef V8_ENABLE_ALLOCATION_TIMEOUT
  if (FLAG_random_gc_interval > 0 || FLAG_gc_interval >= 0) {
    if (!always_allocate() && Heap::allocation_timeout_-- <= 0) {
      return AllocationResult::Retry();
    }
  }
#endif
#ifdef DEBUG
  IncrementObjectCounters();
#endif

  bool large_object = size_in_bytes > kMaxRegularHeapObjectSize;

  HeapObject object;
  AllocationResult allocation;

  if (AllocationType::kYoung == type) {
    if (large_object) {
      if (FLAG_young_generation_large_objects) {
        allocation = new_lo_space_->AllocateRaw(size_in_bytes);
      } else {
        // If young generation large objects are disalbed we have to tenure the
        // allocation and violate the given allocation type. This could be
        // dangerous. We may want to remove FLAG_young_generation_large_objects
        // and avoid patching.
        allocation = lo_space_->AllocateRaw(size_in_bytes);
      }
    } else {
      allocation = new_space_->AllocateRaw(size_in_bytes, alignment);
    }
  } else if (AllocationType::kOld == type) {
    if (large_object) {
      allocation = lo_space_->AllocateRaw(size_in_bytes);
    } else {
      allocation = old_space_->AllocateRaw(size_in_bytes, alignment);
    }
  } else if (AllocationType::kCode == type) {
    if (size_in_bytes <= code_space()->AreaSize() && !large_object) {
      allocation = code_space_->AllocateRawUnaligned(size_in_bytes);
    } else {
      allocation = code_lo_space_->AllocateRaw(size_in_bytes);
    }
  } else if (AllocationType::kMap == type) {
    allocation = map_space_->AllocateRawUnaligned(size_in_bytes);
  } else if (AllocationType::kReadOnly == type) {
#ifdef V8_USE_SNAPSHOT
    DCHECK(isolate_->serializer_enabled());
#endif
    DCHECK(!large_object);
    DCHECK(CanAllocateInReadOnlySpace());
    allocation = read_only_space_->AllocateRaw(size_in_bytes, alignment);
  } else {
    UNREACHABLE();
  }

  if (allocation.To(&object)) {
    if (AllocationType::kCode == type) {
      // Unprotect the memory chunk of the object if it was not unprotected
      // already.
      UnprotectAndRegisterMemoryChunk(object);
      ZapCodeObject(object.address(), size_in_bytes);
      if (!large_object) {
        MemoryChunk::FromHeapObject(object)
            ->GetCodeObjectRegistry()
            ->RegisterNewlyAllocatedCodeObject(object.address());
      }
    }
    OnAllocationEvent(object, size_in_bytes);
  }

  return allocation;
}

老年代

老年代的对象都是经过了新生代一轮GC,晋升之后转移过来的。另外老年代又包含新生代的指针空间新生代数据空间,前者是包含了指向新生代对象指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。