前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >V8 GC 的实现

V8 GC 的实现

作者头像
theanarkh
发布2022-12-06 09:36:04
2890
发布2022-12-06 09:36:04
举报
文章被收录于专栏:原创分享原创分享

前言:GC 是一个古老、复杂并且很 Cool 的技术,本文大概介绍一下早期 V8 中关于 GC 实现的部分,代码版本 0.1.5,早期版本利于快速理解整体的逻辑,因为现代版本已经非常复杂。

HandleScope 和 Handle

首先看一下 Handle 一般的用法,Handle 是 GC 非常核心的概念。

代码语言:javascript
复制
HandleScope scope;
  Handle<JSObject> obj(object);

这两句代码里涉及了两个核心的概念。首先看看 HandleScope。

代码语言:javascript
复制
class HandleScope {
 public:
  // 记录之前的上下文
  HandleScope() : previous_(current_){
    current_.extensions = 0;
  }
  ~HandleScope() {
    // 如果自己申请了内存,则释放
    if (current_.extensions > 0) DeleteExtensions();
    // 出栈,指向前面那个
    current_ = previous_;
  }
  static void** CreateHandle(void* value);
  class Data {
   public:
    // 分配的内存块数
    int extensions;
    // 下一个可用的位置
    void** next;
    // 达到limit执行的地址后说明当前内存块用完了
    void** limit;
    inline void Initialize() {
      extensions = -1;
      next = limit = NULL;
    }
  };
  // 当前的HandleScope
  static Data current_;
  // 上一个HandleScope
  const Data previous_;}

首先看一下 Data 数据结构,Data 有三个字段

1. next 表示下次创建 Handle 时使用的地址。

2. limit表示超过 limit 则需要重新申请内存,V8 是按块申请的,limit 就是块的末地址。

3. extensions 表示该 HandleScope 申请了多少块内存。

接着继续看 current 类静态变量,这个变量记录了当前用于分配 Handle 的地址信息,接着看 HandleScope 类本身。HandleScope 是基于 RAII 机制管理一个函数中,多个 Handle 创建和销毁的对象。HandleScope 在构造函数中记录了当前的上下文,然后在析构函数中进行恢复。我们看到在 HandleScope 的构造函数中把当前上下文记录在 previous_ 里,然后重置 extensions,表示该 HandleScope 还没有申请内存,因为新的 HandleScope 会沿用前一个 HandleScope 的空闲内存,因为 V8 只把 current 赋值给 previous_ ,然后接着使用 current 去分配内存,只有当 HandleScope 内存不够时,才会分配新的内存块。看一下下面的图。

首先看一下上图的左边,当我们创建第一个 HandleScope 时,该 HandleScope 对象就会保存当前 HandleScope 的上下文,这时候当前 HandleScope 上下文是 NULL,然后创建第一个 Handle 时因为 next == limit == NULL,所以需要申请一块内存,即 extensions 为 1。当创建第二个 Handle 后,就变成了左上角的样子。接着我们又定义了第二个 HandleScope,那么首先 第二个 HandleScope 同样在 previous_ 中记录当前的上下文,即 current 的值,接着在第二个 HandleScope 中分配一个 Handle 就变成了右图的样子。接下来看退出 HandleScope 时的逻辑,从代码中可以看到,首先判断本 HandleScope 是否创建了额外的内存块,是则释放,然后把 previous_ 中保存的上下文赋值给 current,那么 current 就恢复到了上一个 HandleScope 的上下文。

了解了 HandleScope 和 Handle 之后,我们接下来分析一下堆内存,主要是分析内存分配器、新生代和老生代(不包括代码区,map 区等)。

内存分配器 MemoryAllocator

V8 堆内存的申请和回收由内存分配器管理,新生代和老生代只是负责使用。首先看看 MemoryAllocator 的定义。

代码语言:javascript
复制
class MemoryAllocator : public AllStatic {
 public:
  static const int kMaxNofChunks = 1 << Page::kPageSizeBits;
  static const int kPagesPerChunk = 64;
  static const int kChunkSize = kPagesPerChunk * Page::kPageSize;

 private:
  // Maximum space size in bytes.
  static int capacity_;

  // Allocated space size in bytes.
  static int size_;

  // The initial chunk of virtual memory.
  static VirtualMemory* initial_chunk_;

  class ChunkInfo BASE_EMBEDDED {
   public:
    ChunkInfo() : address_(NULL), size_(0), owner_(NULL) {}
    void init(Address a, size_t s, PagedSpace* o) {
      address_ = a;
      size_ = s;
      owner_ = o;
    }
    Address address() { return address_; }
    size_t size() { return size_; }
    PagedSpace* owner() { return owner_; }

   private:
    Address address_;
    size_t size_;
    PagedSpace* owner_;
  };

  static List<ChunkInfo> chunks_;
  static List<int> free_chunk_ids_;
  static int max_nof_chunks_;
  static int top_;};

MemoryAllocator 中由 chunks_ 负责管理具体的内存,chunks_ 是一个 ChunkInfo 链表,从 ChunkInfo 中可以看到它主要记录了内存的起始地址和大小,以及所属的 space(新生代、老生代)。

接下来看 MemoryAllocator 的初始化。

代码语言:javascript
复制
// Setup memory allocator and allocate an initial chunk of memory.  The// initialchunk is double the size of the new space to ensure that we can// find a pair of semispaces that are contiguous and aligned to their size.// capacity 最大的内存大小(新生代 + 老生代)bool MemoryAllocator::Setup(int capacity) {  // 页的整数倍
  capacity_ = RoundUp(capacity, Page::kPageSize);
  // 最大的chunk数,
  max_nof_chunks_ = (capacity_ / (kChunkSize - Page::kPageSize)) + 4;
  size_ = 0;
  ChunkInfo info;

  for (int i = max_nof_chunks_ - 1; i >= 0; i--) {
    chunks_.Add(info);
    free_chunk_ids_.Add(i);
  }
  top_ = max_nof_chunks_;
  return true;}

主要是初始化了内部的一些数据结构,没有实质的操作。接下来才是分配内存。

代码语言:javascript
复制
void* chunk = MemoryAllocator::ReserveInitialChunk(2 * young_generation_size_);

接着看 ReserveInitialChunk

代码语言:javascript
复制
void* MemoryAllocator::ReserveInitialChunk(const size_t requested) {
  // 新建一个VM对象,分配size的虚拟内存,记录在VM对象
  initial_chunk_ = new VirtualMemory(requested);
  size_ += requested;
  // 返回分配内存的首地址
  return initial_chunk_->address();}VirtualMemory::VirtualMemory(size_t size, void* address_hint) {
  // 映射一块内存
  address_ = mmap(address_hint, size, PROT_NONE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
                  kMmapFd, kMmapFdOffset);
  size_ = size;}

这就完成了内存的申请。

新生代堆内存管理

代码语言:javascript
复制
// 刚才分配的空间中,老生代在开始位置Address old_space_start = reinterpret_cast<Address>(chunk);// 紧接着是新生代,算出大小是young_generation_size_的n倍,值大于old_space_start的最小值Address new_space_start = RoundUp(old_space_start, young_generation_size_);// 代码空间等于新生代开始+新生代大小Address code_space_start = new_space_start + young_generation_size_;// 老生代空间大小int old_space_size = new_space_start - old_space_start;/*
  因为chunk的空间两倍的young_generation_size_,新生代大小占了一半,
  所以还有一半,剩下的一半老生代占了old_space_size,所以剩下的代码区大小
*/  因为chunk的空间两倍的young_generation_size_,新生代大小占了一半,
  所以还有一半,剩下的一半老生代占了old_space_size,所以剩下的代码区大小
*/int code_space_size = young_generation_size_ - old_space_size;/*
                 |young_generation_size_|   
  chunk =>  -----------------------------------
            ^    ^                      ^      ^
            |    |                      |      |
            old  new                    code   end
*/// 分配一个管理新生代地址空间的对象,传入初始值和最大值// 因为新生代分配from和to,所以这两个初始化值是每个空间的属性new_space_ = new NewSpace(initial_semispace_size_, semispace_size_);

进行了一系列的计算后,开始初始化新生代。接下来看 NewSpace。

代码语言:javascript
复制
class NewSpace : public Malloced {
 public:
  // Create a new space with a given allocation capacity (ie, the capacity of
  // *one* of the semispaces).  The constructor does not allocate heap memory
  // from the OS.  When the space is set up, it is given a contiguous chunk of
  // memory of size 2 * semispace_capacity.  To support fast containment
  // testing in the new space, the size of this chunk must be a power of two
  // and it must be aligned to its size.
  NewSpace(int initial_semispace_capacity, int maximum_semispace_capacity);
 private:
  int capacity_;
  int maximum_capacity_;

  // The semispaces.
  SemiSpace* to_space_;
  SemiSpace* from_space_;
  Address start_;
  uint32_t address_mask_;
  uint32_t object_mask_;
  uint32_t object_expected_;
  /*
    struct AllocationInfo {
      Address top;  // current allocation top
      Address limit;  // current allocation limit
    };
  */
  // 管理新生代内存的数据结构
  AllocationInfo allocation_info_;
  AllocationInfo mc_forwarding_info_;};

新生代分为两个 SemiSpace。结构差不多,就不再介绍。了解了定义,看看初始化的逻辑。

代码语言:javascript
复制
// 设置需要管理的地址空间,start是首地址,size是大小bool NewSpace::Setup(Address start, int size) {
  // to区
  if (to_space_ == NULL
      || !to_space_->Setup(start, maximum_capacity_)) {
    return false;
  }
  // from区,和to区一人一半
  if (from_space_ == NULL
      || !from_space_->Setup(start + maximum_capacity_, maximum_capacity_)) {
    return false;
  }
  // 开始地址
  start_ = start;
  /*
    address_mask的高位是地址的有效位,
    size是只有一位为一,减一后一变成0,一右边
    的全部0位变成1,然后取反,高位的0变成1,再加上size中本来的1,
    即从左往右的1位地址有效位
  */
  address_mask_ = ~(size - 1);
  // 计算堆对象地址掩码,低位是标记位,判断的时候需要保留,kHeapObjectTag是堆对象的标记
  object_mask_ = address_mask_ | kHeapObjectTag;
  // 见 contains 函数,对象地址里低位是标记位,判断的时候需要带上
  object_expected_ = reinterpret_cast<uint32_t>(start) | kHeapObjectTag;
  // 初始化管理的内存信息
  allocation_info_.top = to_space_->low();
  allocation_info_.limit = to_space_->high();
  mc_forwarding_info_.top = NULL;
  mc_forwarding_info_.limit = NULL;
  return true;}

至此,新生代的数据结构和初始化分析完成。接下来看老生代,老生代的数据结构和新生代差不多,不再分析,区别是老生代的内存是用链表管理的,而不是连续的内存,直接看初始化流程。

代码语言:javascript
复制
bool PagedSpace::Setup(Address start, size_t size) {
  int num_pages = 0;
  first_page_ = MemoryAllocator::CommitPages(start, size, this, &num_pages);
  // 初始化page链表,清除记录集
  for (Page* p = first_page_; p->is_valid(); p = p->next_page()) {
    p->ClearRSet();
  }
  // 设置管理内存分配的结构体,为第一个空闲页
  SetAllocationInfo(&allocation_info_, first_page_);
  return true;}

接着看 CommitPages

代码语言:javascript
复制
// 保存和管理虚拟地址start到start+size的空间Page* MemoryAllocator::CommitPages(Address start, size_t size,                                   PagedSpace* owner, int* num_pages) {
  // chunk中的页数,一个chunk包括多个页
  *num_pages = PagesInChunk(start, size);
  // 保存内存信息到chunkInfo
  int chunk_id = Pop();
  chunks_[chunk_id].init(start, size, owner);
  // 按页初始化内存信息
  return InitializePagesInChunk(chunk_id, *num_pages, owner);}

继续看 InitializePagesInChunk

代码语言:javascript
复制
// 初始化某块内存为page管理的结构Page* MemoryAllocator::InitializePagesInChunk(int chunk_id, int pages_in_chunk,                                              PagedSpace* owner) {
  Address chunk_start = chunks_[chunk_id].address();
  // 算出有效的开始地址,即要对齐
  Address low = RoundUp(chunk_start, Page::kPageSize);
  Address page_addr = low;
  for (int i = 0; i < pages_in_chunk; i++) {
    Page* p = Page::FromAddress(page_addr);
    // 保存下一页的地址和当前所属的chunk_id
    p->opaque_header = OffsetFrom(page_addr + Page::kPageSize) | chunk_id;
    // 不是large object
    p->is_normal_page = 1;
    // 指向下一页地址
    page_addr += Page::kPageSize;
  }

  // page_addr此时执行最后一页的末尾,减去一页大小得到最后一页的起始地址
  Page* last_page = Page::FromAddress(page_addr - Page::kPageSize);
  // 下一页地址为0
  last_page->opaque_header = OffsetFrom(0) | chunk_id;

  return Page::FromAddress(low);}

一个内存块里面包括多个 Page,上面的代码是按 Page 为单位初始化内存。整体流程完成后,结构图如下。

新建对象

分析完数据结构和初始化流程,接下来看创建对象时的内存分配。

代码语言:javascript
复制
Local<v8::Object> v8::Object::New() {
  i::Handle<i::JSObject> obj = i::Factory::NewJSObject(i::Top::object_function());
  return Utils::ToLocal(obj);}
Handle<JSObject> Factory::NewJSObject(Handle<JSFunction> constructor, PretenureFlag pretenure) {     // CALL_HEAP_FUNCTION 会在分配失败时进行 GC  CALL_HEAP_FUNCTION(Heap::AllocateJSObject(*constructor, pretenure), JSObject);}
Object* Heap::AllocateJSObject(JSFunction* constructor,                               PretenureFlag pretenure) {
  return AllocateJSObjectFromMap(constructor->initial_map(), pretenure);}
Object* Heap::AllocateJSObjectFromMap(Map* map, PretenureFlag pretenure) {  Object* properties = AllocatePropertyStorageForMap(map);
  // Allocate the JSObject.
  // 选择分配的 Space
  AllocationSpace space = (pretenure == TENURED) ? OLD_SPACE : NEW_SPACE;
  // 对象所需内存太大(map 中记录了大小),则直接在大对象 Space 分配
  if (map->instance_size() > MaxHeapObjectSize())     space = LO_SPACE;  // 分配内存
  Object* obj = Allocate(map, space);
  // Initialize the JSObject.
  InitializeJSObjectFromMap(JSObject::cast(obj),
                            FixedArray::cast(properties),
                            map);
  return obj;}

接着看 Allocate。

代码语言:javascript
复制
// 根据map的type分配一个对象的空间,然后设置该对象的map属性是mapObject* Heap::Allocate(Map* map, AllocationSpace space) {  Object* result = AllocateRaw(map->instance_size(), space);
  // 转成HeapObject才有set_map函数
  HeapObject::cast(result)->set_map(map);
  return result;}
// 在哪块空间分配多少内存Object* Heap::AllocateRaw(int size_in_bytes, AllocationSpace space) {  // 在新生代中分配内存
  if (NEW_SPACE == space) {
    return new_space_->AllocateRaw(size_in_bytes);
  }

  Object* result;
  // 在对应的空间分配大小
  if (OLD_SPACE == space) {
    result = old_space_->AllocateRaw(size_in_bytes);
  } else if (CODE_SPACE == space) {
    result = code_space_->AllocateRaw(size_in_bytes);
  } else if (LO_SPACE == space) {
    result = lo_space_->AllocateRaw(size_in_bytes);
  } else {
    result = map_space_->AllocateRaw(size_in_bytes);
  }
  return result;}
Object* AllocateRaw(int size_in_bytes) {   // allocation_info_ 就是初始化时记录的内存信息,allocation_info_ 负责管理内存的分配
   return AllocateRawInternal(size_in_bytes, &allocation_info_);}
Object* NewSpace::AllocateRawInternal(int size_in_bytes,                                      AllocationInfo* alloc_info) {
  // alloc_info->top保存了该空间当前可分配的地址,加上当前申请分配的                                 
  Address new_top = alloc_info->top + size_in_bytes;
  // 超过了内存大小
  if (new_top > alloc_info->limit) {
    return Failure::RetryAfterGC(size_in_bytes, NEW_SPACE);
  }
  // 地址+低一位的标记,转成堆对象的地址表示,即低位置1
  Object* obj = HeapObject::FromAddress(alloc_info->top);
  // 更新指针,指向下一块可分配的内存
  alloc_info->top = new_top;
  return obj;}

我们可以看到分配内存是非常快的,只需要移动一下指针。

垃圾回收

但是有一个需要注意的是,当内存不够时就需要进行 GC,具体处理逻辑在之前代码的 CALL_HEAP_FUNCTION 宏中。

代码语言:javascript
复制
#define CALL_HEAP_FUNCTION(FUNCTION_CALL, TYPE)                              \
  do {                                                                       \
    GC_GREEDY_CHECK();                                                       \
    Object* __object__ = FUNCTION_CALL;                                      \
    if (__object__->IsFailure()) {                                           \
      if (__object__->IsRetryAfterGC()) {                                    \
        if (!Heap::CollectGarbage(                                           \
                Failure::cast(__object__)->requested(),                      \
                Failure::cast(__object__)->allocation_space())) {            \          /* TODO(1181417): Fix this. */                                     \
          v8::internal::V8::FatalProcessOutOfMemory("CALL_HEAP_FUNCTION");   \        }                                                                    \
        __object__ = FUNCTION_CALL;                                          \        if (__object__->IsFailure()) {                                       \          if (__object__->IsRetryAfterGC()) {                                \            /* TODO(1181417): Fix this. */                                   \
            v8::internal::V8::FatalProcessOutOfMemory("CALL_HEAP_FUNCTION"); \          }                                                                  \          return Handle<TYPE>();                                             \        }                                                                    \      } else {                                                               \        return Handle<TYPE>();                                               \      }                                                                      \    }                                                                        \    return Handle<TYPE>(TYPE::cast(__object__));                             \  } while (false)

重点看 Heap::CollectGarbage。

代码语言:javascript
复制
bool Heap::CollectGarbage(int requested_size, AllocationSpace space) {
  // The VM is in the GC state until exiting this function.
  VMState state(GC);

  { GCTracer tracer;
    GarbageCollectionPrologue();
    // 选择GC算法
    GarbageCollector collector = SelectGarbageCollector(space);
    tracer.set_collector(collector);

    StatsRate* rate = (collector == SCAVENGER)
        ? &Counters::gc_scavenger : &Counters::gc_compactor;
    rate->Start();
    // 开始垃圾回收
    PerformGarbageCollection(space, collector);
    rate->Stop();

    GarbageCollectionEpilogue();
  }
  // GC 后内存是否能满足当前需要分配的内存
  switch (space) {
    case NEW_SPACE:
      return new_space_->Available() >= requested_size;
    case OLD_SPACE:
      return old_space_->Available() >= requested_size;
    case CODE_SPACE:
      return code_space_->Available() >= requested_size;
    case MAP_SPACE:
      return map_space_->Available() >= requested_size;
    case LO_SPACE:
      return lo_space_->Available() >= requested_size;
  }
  return false;}

早期的 V8 GC 是同步的,还没有并行、并发等优化逻辑,继续 PerformGarbageCollection。在 V8 初始化时会初始化新生代堆内存的数据结构。

代码语言:javascript
复制
void Heap::PerformGarbageCollection(AllocationSpace space,
                                    GarbageCollector collector) {
  if (collector == MARK_COMPACTOR) {
    MarkCompact();
    int promoted_space_size = PromotedSpaceSize();
    promoted_space_limit_ =
        promoted_space_size + Max(2 * MB, (promoted_space_size/100) * 35);
    old_gen_exhausted_ = false;
    if (space == NEW_SPACE && !MarkCompactCollector::HasCompacted()) {
      Scavenge();
    }
  } else {
    Scavenge();
  }}

这里我们可以看到经常听到的 Scavenge 和 MarkCompact 算法。首先看 Scavenge。

代码语言:javascript
复制
void Heap::Scavenge() {
  gc_state_ = SCAVENGE;
  // from区和to区互换,to区变成空白,from区则分配了各种对象
  new_space_->Flip();
  // 重置to区的指针,因为to区变空闲了
  new_space_->ResetAllocationInfo();
  CopyVisitor copy_visitor;
  // 遍历根,复制对象到另一个区
  IterateRoots(&copy_visitor);
  // 设置临界线,以此判断新生代对象是否可以经历了二次 GC,即可以晋升带老生代
  new_space_->set_age_mark(new_mark);
  gc_state_ = NOT_IN_GC;}

实际的逻辑比这个负责很多,不过这里只是大致了解流程。CopyVisitor 对象负责把对象复制到另一个区。

代码语言:javascript
复制
class CopyVisitor: public ObjectVisitor {
 public:

  void VisitPointer(Object** p) {
    CopyObject(p);
  }

  void VisitPointers(Object** start, Object** end) {
    // Copy all HeapObject pointers in [start, end)
    for (Object** p = start; p < end; p++) CopyObject(p);
  }

 private:
  void CopyObject(Object** p) {
    if (!Heap::InFromSpace(*p)) return;
    Heap::CopyObject(reinterpret_cast<HeapObject**>(p));
  }};

继续看 Heap::CopyObject。

代码语言:javascript
复制
void Heap::CopyObject(HeapObject** p) {
  ASSERT(InFromSpace(*p));

  HeapObject* object = *p;
  // map 对象记录了对象的元信息
  HeapObject* first_word = object->map();
  int object_size = object->SizeFromMap(Map::cast(first_word));
  Object* result;
  // 是否满足晋升条件
  /*
    bool Heap::ShouldBePromoted(Address old_address, int object_size) {
      //  小于age_mark说明已经经过了一次gc回收了,第二次的时候还存活则可以晋升到老生代
      //  age_mark是from区的值,但是是在from区和to区交换之后,所以其实是to区的值
      return old_address < new_space_->age_mark()
          || (new_space_->Size() + object_size) >= (new_space_->Capacity() >> 2);
    }
  */
  if (ShouldBePromoted(object->address(), object_size)) {
    bool has_pointers =
        type != HEAP_NUMBER_TYPE &&
        (type >= FIRST_NONSTRING_TYPE ||
         String::cast(object)->representation_tag() != kSeqStringTag);
    // 在对应的 Space 分配内存,把对象复制过去
    if (has_pointers) {
      result = old_space_->AllocateRaw(object_size);
    } else {
      result = code_space_->AllocateRaw(object_size);
    }

    if (!result->IsFailure()) {
      *p = MigrateObject(p, HeapObject::cast(result), object_size);
      if (has_pointers) {
        promoted_top -= kPointerSize;
        Memory::Object_at(promoted_top) = *p;
      }
      return;
    }
  }
  // 不晋升的复制到新生代的另一个区,下次还存活再晋升
  result = new_space_->AllocateRaw(object_size);
  *p = MigrateObject(p, HeapObject::cast(result), object_size);}

MigrateObject 实现了对象的复制。

代码语言:javascript
复制
HeapObject* Heap::MigrateObject(HeapObject** source_p,
                                HeapObject* target,
                                int size) {
  void** src = reinterpret_cast<void**>((*source_p)->address());
  void** dst = reinterpret_cast<void**>(target->address());
  int counter = size/kPointerSize - 1;
  do {
    *dst++ = *src++;
  } while (counter-- > 0);

  (*source_p)->set_map(reinterpret_cast<Map*>(target));

  return target;}

至此就完成了新生代 GC 的分析,紧接着分析老生代的。

代码语言:javascript
复制
void Heap::MarkCompact() {
  gc_state_ = MARK_COMPACT;
  MarkCompactPrologue();
  // 开始回收
  MarkCompactCollector::CollectGarbage();

  MarkCompactEpilogue();

  gc_state_ = NOT_IN_GC;}

老生代 GC 比较复杂。

代码语言:javascript
复制
void MarkCompactCollector::CollectGarbage() {
  Prepare();
  // 标记
  MarkLiveObjects();
  // 清除大对象
  SweepLargeObjectSpace();
  // 是否压缩,压缩是为了防止内存碎片,否则直接清楚
  if (compacting_collection_) {
    EncodeForwardingAddresses();

    UpdatePointers();

    RelocateObjects();

    RebuildRSets();

  } else {
    SweepSpaces();
  }

  Finish();}

这里我们只分析标记和清除,V8 早期也还没有并行标记,延迟清除等逻辑。

代码语言:javascript
复制
void MarkCompactCollector::MarkLiveObjects() {
  // 新生代有一半空间时空闲的,可以使用,marking_stack用于遍历扫描对象的数据结构
  marking_stack.Initialize(Heap::new_space()->FromSpaceLow(),
                           Heap::new_space()->FromSpaceHigh());

  MarkingVisitor marking_visitor;
  // 从根开始遍历,进行标记
  Heap::IterateStrongRoots(&marking_visitor);

  while (true) {
    // IterateStrongRoots 会不断压对象进栈,这里继续遍历,直到为空
    MarkObjectsReachableFromTopFrame();
    // 遍历的时候,栈可能溢出,内存不够,ok 的话继续遍历其他的
    if (!marking_stack.overflowed()) {
      if (has_processed_weak_pointers) break;
      GlobalHandles::MarkWeakRoots(&MustBeMarked);
      GlobalHandles::IterateWeakRoots(&marking_visitor);
      has_processed_weak_pointers = true;
      continue;
    }
    // 清除标记
    marking_stack.clear_overflowed();
    // 继续遍历,到栈为空
    ScanOverflowedObjects(&lo_it);
  }}

首先看如何从根开始遍历。

代码语言:javascript
复制
void Heap::IterateStrongRoots(ObjectVisitor* v) {
  // 遍历 Handle,忽略其他根
  HandleScopeImplementer::Iterate(v);}
void HandleScopeImplementer::Iterate(ObjectVisitor* v) {  ImplementationUtilities::HandleScopeData* current =
      ImplementationUtilities::CurrentHandleScope();
  Iterate(v, thread_local.Blocks(), current);}

主要看 ImplementationUtilities::CurrentHandleScope()。

代码语言:javascript
复制
static HandleScopeData* CurrentHandleScope() {
  return &v8::HandleScope::current_;}

v8::HandleScope::current_ 就是前面介绍的 HandleScope。

代码语言:javascript
复制
void HandleScopeImplementer::Iterate(
    ObjectVisitor* v,
    List<void**>* blocks,
    ImplementationUtilities::HandleScopeData* handle_data) {
  // Iterate over all handles in the blocks except for the last.
  for (int i = blocks->length() - 2; i >= 0; --i) {
    Object** block =
        reinterpret_cast<Object**>(blocks->at(i));
    v->VisitPointers(block, &block[kHandleBlockSize]);
  }

  // Iterate over live handles in the last block (if any).
  if (!blocks->is_empty()) {
    v->VisitPointers(reinterpret_cast<Object**>(blocks->last()),
       reinterpret_cast<Object**>(handle_data->next));
  }}

Iterate 实现遍历全部的 Handle。接着看遍历时对对象的处理。具体实现在 MarkingVisitor。

代码语言:javascript
复制
void VisitPointer(Object** p) {
  MarkObjectByPointer(p);
}
void MarkObjectByPointer(Object** p) {  Object* obj = *p;
  if (!obj->IsHeapObject()) return;
  MarkCompactCollector::MarkObject(HeapObject::cast(obj));}
static inline void MarkObject(HeapObject* obj) {   // 没有标记则标记
   if (!is_marked(obj)) MarkUnmarkedObject(obj);}
void MarkCompactCollector::MarkUnmarkedObject(HeapObject* obj) {  // 打上标记
  set_mark(obj);
  // 押入栈,扫描它的子指针,类似遍历树
  if (!marking_stack.overflowed()) {
    marking_stack.Push(obj);
  } else {
    // Set object's stack overflow bit, wait for rescan.
    // 溢出了则后面继续扫描
    set_overflow(obj);
  }}

扫描和标记完全部对象后就进行清除。

代码语言:javascript
复制
void MarkCompactCollector::SweepSpaces() {
  SweepSpace(Heap::old_space(), &DeallocateOldBlock);}
static void SweepSpace(PagedSpace* space, DeallocateFunction dealloc) {  PageIterator it(space, PageIterator::PAGES_IN_USE);
  while (it.has_next()) {
    Page* p = it.next();

    bool is_previous_alive = true;
    Address free_start = NULL;
    HeapObject* object;
    // 遍历老生代的每一个对象,没有标记的则清除(回收内存)
    for (Address current = p->ObjectAreaStart();
         current < p->AllocationTop();
         current += object->Size()) {
      object = HeapObject::FromAddress(current);
      // 如果标记,则先清除,然后回收(如果连续的内存则一起回收)
      if (is_marked(object)) {
        clear_mark(object);
        if (!is_previous_alive) {  // Transition from free to live.
          dealloc(free_start, current - free_start);
          is_previous_alive = true;
        }
      } else {
        if (is_previous_alive) {  // Transition from live to free.
          free_start = current;
          is_previous_alive = false;
        }
      }
    }
    if (!is_previous_alive) {
      int free_size = p->AllocationTop() - free_start;
      if (free_size > 0) {
        dealloc(free_start, free_size);
      }
    }
  }}

接着看 dealloc 的回收逻辑。

代码语言:javascript
复制
void MarkCompactCollector::DeallocateOldBlock(Address start,
                                              int size_in_bytes) {
  // 清除记录集,记录集记录了老生代执行新生代的指针                                              
  Heap::ClearRSetRange(start, size_in_bytes);
  Heap::old_space()->Free(start, size_in_bytes);}
void Free(Address start, int size_in_bytes) {  int wasted_bytes = free_list_.Free(start, size_in_bytes);
  accounting_stats_.DeallocateBytes(size_in_bytes);
  accounting_stats_.WasteBytes(wasted_bytes);}
int OldSpaceFreeList::Free(Address start, int size_in_bytes) {  FreeListNode* node = FreeListNode::FromAddress(start);
  node->set_size(size_in_bytes);
  int index = size_in_bytes >> kPointerSizeLog2;
  node->set_next(free_[index].head_node_);
  free_[index].head_node_ = node->address();
  available_ += size_in_bytes;
  needs_rebuild_ = true;
  return 0;}

这里的内存并不是直接释放,而是存到空闲链表,后续使用。可参考前面的图。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HandleScope 和 Handle
  • 内存分配器 MemoryAllocator
  • 新生代堆内存管理
  • 垃圾回收
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档