V8 垃圾回收机制

简介: V8 垃圾回收机制简单分享~

垃圾回收策略

垃圾回收有手动回收和自动回收两种策略。

手动回收需要自己控制内存的分配和销毁,如果分配了内存在使用结束后没有进行销毁,会造成内存泄漏。

而 JavaScript 采用的是另外一种策略即通过垃圾回收器自动回收的机制。

因为 JavaScript 中的数据存储在栈(原始数据类型)和堆(引用数据类型)中,所以它的垃圾回收也包含栈中的垃圾回收和堆中的垃圾回收两种。

栈中的垃圾回收

通过下面的一段代码,我们可以先看下调用栈中的垃圾是如何回收的。

function fn() {
    let num1 = 1;
    let obj1 = {test: "haha"};
    function fn2() {
        let num2 = 2;
        let obj2 = {test: "xixi"};
    }
    fn2();
}
fn();

执行到 fn2 时,此时调用栈和堆的状态如下图所示

image.png

从图中可以看出,原始类型的数据是分配在栈中的,对象这些引用类型的数据是分配在堆中的。

当 fn2 执行完的时候,fn2 所对应的栈中的数据就会被销毁掉,它是如何被销毁的呢?

在栈中有一个记录当前执行状态的指针 ESP,它指向当前执行函数的上下文,当 fn2 执行完的时候,这个指针就会下移到 fn 的执行上下文,这个下移的操作就是销毁 fn2 执行上下文的过程。

如果之后还有别的函数执行,那么该函数的执行上下文就会直接覆盖在原来 fn2 执行上下文的地方。

即当一个函数执行结束之后,JS 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。

堆中的垃圾回收

当栈中的数据被回收掉之后,接下来我们看看堆中的数据是如何进行回收的。

要回收堆中的垃圾数据,就需要用到 JavaScript 的垃圾回收器。

V8 引擎把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的数据,老生代中存放的是生存时间长的数据。

新生区出于效率的考虑,一般比较小,只有 1-8 M 的容量,而老生区容量则大很多。这两个区域使用的垃圾回收器是不同的。新生代的垃圾是通过副垃圾回收器回收的,而老生代的垃圾是通过主垃圾回收器进行回收的。

垃圾回收器的工作流程

垃圾回收器的工作流程其实都是差不多的。

第一步是先对空间中的对象进行标记,将空间中的对象分为活动对象和非活动对象,非活动对象就是可以进行垃圾回收的对象。

第二步是回收非活动对象所占据的内存。就是在所有的标记都完成之后,对内存中所有标记为非活动对象进行清理。

第三步是进行内存整理。垃圾回收后,内存中可能会出现不连续的空间,如果之后需要分配较大的连续内存,那么这些不连续的空间就可能导致分配不了,所以我们需要对内存进行整理。不过内存整理这一步不是一定需要的,因为有些垃圾回收器在进行回收的时候不会产生内存碎片,例如我们下面要说到的副垃圾回收器。

副垃圾回收器

副垃圾回收器负责新生区的垃圾回收。除了占用内存比较大的对象,一般的对象都是分配到新生区的,所以新生区的垃圾回收相对老生区来说会更频繁。

副垃圾回收器使用 Scavenge 算法进行处理。该算法将新生区的空间进行对半划分,一个作为存储对象的区域,另外一个作为空闲区域。

新加入的对象都会放到对象区域,当对象区域快要满了的时候,就会执行垃圾回收。

副垃圾回收器先对对象区域中的垃圾进行标记,标记完成后就将活动对象有序地复制到空闲区域中,这样在垃圾清理的过程中也相当于完成了内存的整理,复制后的空闲区域就变成对象区域,而原来的对象区域则变成了空闲区域。这样就完成了垃圾的回收。

每当对象区域快满了的时候就会进行垃圾回收,所以对象区域和空闲区域也是不断进行翻转,可以无限重复地使用下去。

那么会有一个问题,新生区的空间不大,如果被装满了怎么办?为了解决这个问题,V8 引擎使用了对象晋升策略,如果一个对象经过两次垃圾回收依然存活着,那么它就会被移动到老生区中。

主垃圾回收器

从上面对副垃圾回收器的介绍中,我们可以看出来,老生区中的对象主要是占用内存比较大的对象和存活时间久的对象,其中占用内存大的对象是直接分配到老生区的。

因为老生区的对象比较大,如果采用和副垃圾回收器一样的回收策略的话,复制的操作需要花费比较长的时间,并且有一半的空间会被浪费掉,所以主垃圾回收器采用的是另外一种算法,标记-整理算法来进行垃圾回收。

首先是从一组根元素开始,递归遍历这组根元素,能遍历到的对象标记为活动对象,没有遍历到的对象标记为需要进行垃圾回收的非活动数据。

标记完之后就将所有活动对象向同一边进行移动,移动完之后就将端边界外的内存都清理掉,这样就完成了内存整理和垃圾回收。

增量标记算法

因为 JavaScript 是单线程的,在执行垃圾回收的时候无法执行其他任务,所以如果垃圾回收时间过长的话那么就会造成明显的卡顿。

因为新生区的空间较小,所以造成的影响也比较小,我们可以忽略不计,而老生区因为其空间比较大,所以垃圾回收的时间可能过长。

为了降低老生区垃圾回收造成的卡顿,V8 将垃圾回收中的标记过程分为一个个子标记过程,同时让垃圾回收标记和 JavaScript 逻辑的交替执行,直到标记阶段完成,这个算法就是增量标记算法。

通过将一个大的任务划分为一个个小的任务,就可以让垃圾回收和 JavaScript 逻辑都正常地进行,同时不影响用户的体验。

目录
相关文章
|
3月前
|
算法 Java
JVM垃圾回收机制
JVM垃圾回收机制
33 0
|
3月前
|
Java 程序员
探讨JVM垃圾回收机制与内存泄漏
探讨JVM垃圾回收机制与内存泄漏
|
3月前
|
存储 缓存 算法
JVM的垃圾回收机制
JVM的垃圾回收机制
|
3月前
|
人工智能 Java 数据库连接
【C#】浅谈C#中垃圾回收机制
【C#】浅谈C#中垃圾回收机制
|
2月前
|
存储 算法 Java
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
30 1
|
3月前
|
算法 Java
垃圾回收机制
垃圾回收是自动内存管理机制,用于检测和回收不再使用的内存资源,防止泄漏和浪费。主要算法包括:标记-清除、引用计数(难以处理循环引用)、分代回收(基于对象生命周期)、增量回收(减少应用停顿时间)和并发回收(同时执行回收和应用)。不同语言和环境选择不同策略,垃圾回收性能直接影响程序内存管理和执行效率。
|
3月前
|
存储 缓存 监控
Java内存管理:垃圾回收与内存泄漏
【4月更文挑战第16天】本文探讨了Java的内存管理机制,重点在于垃圾回收和内存泄漏。垃圾回收通过标记-清除过程回收无用对象,Java提供了多种GC类型,如Serial、Parallel、CMS和G1。内存泄漏导致内存无法释放,常见原因包括静态集合、监听器、内部类、未关闭资源和缓存。内存泄漏影响性能,可能导致应用崩溃。避免内存泄漏的策略包括代码审查、使用分析工具、合理设计和及时释放资源。理解这些原理对开发高性能Java应用至关重要。
62 5
|
3月前
|
算法 Java PHP
JVM 的垃圾回收机制以及垃圾回收算法的详解
JVM 的垃圾回收机制以及垃圾回收算法的详解
32 0
|
3月前
|
存储 算法 Java
了解Java内存管理与垃圾回收机制
了解Java内存管理与垃圾回收机制
31 0
|
3月前
|
算法 Java 程序员
JVM的垃圾回收机制(GC)
JVM的垃圾回收机制(GC)