浏览器垃圾回收

什么是垃圾?

在浏览器的垃圾回收(Garbage Collection)中,"垃圾"是指那些不再被程序所使用且不可访问的内存对象。也就是说,垃圾是程序中不再需要的、无法被引用到的对象。

浏览器执行JavaScript代码时,会创建一些对象来存储数据、执行函数等操作。随着代码的执行,对象可能会被不再使用,比如变量的作用域结束或者对象的引用被解除。这些无用的对象占用着内存空间,如果不进行垃圾回收,就会导致内存泄露和内存占用过高的问题。

垃圾回收器的任务就是检测和回收这些无用的对象,以释放内存资源。它通过识别活跃对象和不再被引用的对象之间的差异来确定垃圾。一般来说,垃圾回收器将标记所有活跃对象,并清除那些没有被标记的对象,使其成为可回收的垃圾。

需要注意的是,垃圾回收机制只能回收动态分配的内存对象,而不能直接处理静态分配的内存(如全局变量或静态变量)。因此,垃圾回收主要用于自动管理堆内存(heap)中动态分配的对象

通过垃圾回收机制,浏览器可以自动管理内存,有效地回收垃圾对象,防止内存泄露和过度占用内存,提高代码的可靠性和性能。

垃圾回收机制主要负责回收动态分配内存的对象,而对于静态分配的内存(例如全局变量或静态变量)的处理则不在垃圾回收机制的范围之内。

在计算机内存管理中,静态分配的内存是在程序编译时分配的,其生命周期是整个程序的运行周期,直到程序结束才会被释放。这些静态分配的内存通常包括全局变量、静态变量以及程序的代码段等。由于其生命周期的特殊性,这部分内存的管理不属于垃圾回收机制的范畴。因此,即使静态分配的内存变量不再被使用,垃圾回收机制也无法自动回收这部分内存,因为这不是动态分配的内存。

垃圾回收机制主要针对动态分配的内存对象,即程序运行过程中,由代码动态创建并使用的内存对象。这些对象位于堆内存(heap)中,其生命周期不固定,需要动态分配和释放。垃圾回收机制通过识别不再被程序所引用的动态分配的对象,来释放这部分内存,以避免内存泄漏和过度占用内存。
在 JavaScript 中,静态分配的全局变量和静态变量可以通过使用关键字 constlet 来定义。

全局变量的定义方式如下:

// 静态分配的全局变量(常量)
const staticGlobalVariable = 10;

静态变量的定义方式如下:

function myFunction() {
  // 静态分配的局部变量
  let staticLocalVariable = 5;
  console.log("Static local variable: " + staticLocalVariable);
  staticLocalVariable++;
}

myFunction(); // 输出: Static local variable: 5
myFunction(); // 输出: Static local variable: 5(staticLocalVariable 在每次调用后重新初始化)

需要注意的是,JavaScript 是一种自动内存管理的语言,而不同于 C++ 这样的静态语言。在 JavaScript 中,垃圾回收机制会负责自动管理动态分配的内存对象,而静态分配的全局变量和静态变量则不受垃圾回收机制的管理,它们的内存空间由 JavaScript 引擎在程序加载时分配,并在整个程序生命周期内保持不变。

静态分配的局部变量通常指的是在程序编译阶段就确定并分配内存空间的变量,其在程序运行过程中的内存地址是固定的。在像 JavaScript 这样的动态语言中,这种概念并不是很明显,因为 JavaScript 主要进行的是动态内存分配和自动内存管理。

在 JavaScript 中,垃圾回收机制主要用于管理动态分配的内存对象,即通过 new 关键字创建的对象、数组、闭包等,在这种情况下,垃圾回收机制会负责检测这些对象是否还被引用,以决定是否回收其内存。

对于静态分配的局部变量,它们在函数执行的过程中分配内存,并且在函数执行完毕后就会被销毁,因此其内存管理不受垃圾回收机制的影响。一旦函数执行完毕,这些静态分配的局部变量所占用的内存空间会被立即释放,而不需要等待垃圾回收机制的介入。

因此,可以说静态分配的局部变量在 JavaScript 中并不受垃圾回收机制的管理,因为它们的内存生命周期是有程序执行流程明确定义的,而不需要依赖垃圾回收机制来释放其内存。
闭包声明的静态分配的局部变量通常是指在一个函数中定义的变量,然后在内部函数中引用这些变量,从而形成闭包。在 JavaScript 中,闭包可以使内部函数访问外部函数作用域中的变量,并且在外部函数执行完毕后,这些局部变量不会被立即销毁,因为内部函数仍然持有对这些变量的引用。

在 JavaScript 中,闭包声明的静态分配的局部变量受到垃圾回收机制的管理。如果闭包中的内部函数仍然存在于内存中(例如被外部变量引用或者作为返回值返回),那么它会继续持有对外部函数作用域中变量的引用,这些变量就不会被垃圾回收机制回收,直到闭包本身被销毁时。这就意味着,闭包中引用的局部变量会一直存在于内存中,直到不再有对闭包的引用,或者程序退出。

因此,闭包声明的静态分配的局部变量需要谨慎使用,因为它们可能会导致内存泄漏,即使在函数执行完毕后,由于闭包的存在,局部变量仍然持续占用内存。

为什么要垃圾回收?

声明变量时,都会分配内存存储,当不再使用时,就要及时释放,否则内存满了造成系统崩溃

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。在 C 和 C++等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。JavaScript 为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。

我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数

chrome浏览器有使用Reference Counting(引用计数)回收机制吗

在V8引擎中,Chrome浏览器的默认垃圾回收机制不是使用引用计数(Reference Counting)回收机制。在过去,引用计数是一种常见的垃圾回收方法,它通过记录每个对象的引用数量来确定对象是否仍然被引用。当引用数量减少到零时,该对象被认为是垃圾,可以被回收。

然而,引用计数回收机制有一些缺点,如循环引用问题。当两个或多个对象相互引用时,它们的引用计数永远不会降为零,即使它们不再被程序使用,也无法被垃圾回收。这可能导致内存泄漏。

为了解决这些问题,V8采用了基于分代回收的垃圾回收机制,而不是依赖引用计数。这种分代回收机制通过定义不同的内存代(如新生代和老生代)来管理不同生命周期的对象,并使用增量标记、标记-清除和标记-整理等算法来进行垃圾回收。

V8的垃圾回收机制更加高效和灵活,能够处理复杂的内存管理情况,如循环引用、跨代引用等,以减少内存泄漏和提高性能。因此,Chrome浏览器默认使用V8的分代回收机制,而不是引用计数回收机制。

chrome浏览器有使用标记清除回收机制吗

是的,Chrome浏览器的默认垃圾回收机制之一是标记-清除(Mark and Sweep)回收机制。标记-清除是一种常见的垃圾回收算法,用于管理长期存在并相互引用的对象。

在标记-清除回收机制中,垃圾回收器首先会标记所有从根对象(如全局变量、执行上下文等)可达的对象,并将它们标记为“存活”。然后,它会遍历堆内存中的所有对象,将未被标记为“存活”的对象判定为垃圾,即可回收。最后,垃圾回收器会清除这些未被标记的垃圾对象所占用的内存空间。

标记-清除回收机制的优点是可以处理复杂的对象引用关系,包括循环引用等。它通过标记过程确定哪些对象是存活的,然后清除未被标记的垃圾对象。这种方式相对较为灵活和高效。

除了标记-清除回收机制,V8引擎还使用了其他的垃圾回收算法,例如标记-整理(Mark-Compact)回收机制。标记-整理回收机制将存活的对象移动到一端,然后清理并压缩剩余的内存,以减少内存碎片。这种方式可以进一步提高内存利用率。

总之,Chrome浏览器利用标记-清除和标记-整理等垃圾回收机制来有效地管理JavaScript代码所使用的内存,并确保合理地分配和释放内存资源,提高性能和响应性。

垃圾回收器

  1. js引擎中的一个后台进程----监听所有对象,删除不可访问对象

  2. JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收

    /* 案例一 */
    let user = {
        name: "John"
    };
    /* 全局环境 可以通过访问user就可以得到{name:'John'} */
    user = null
    /* 这样{name:'John'}就不能访问了,就要被回收 */
    
    /* 案例二 */
    // user具有对象的引用
    let user = {
        name: "John"
    };
    let admin = user;
    /* 全局环境 可以通过访问user或者admin就可以得到{name:'John'} */
    
    user = null
    /* 这样{name:'John'}就不能访问了,就要被回收 */
    
    /* 案例三 */
    function marry (man, woman) {
        woman.husban = man;
        man.wife = woman;
        return {
            father: man,
            mother: woman
        }
    }
    
    let family = marry({
        name: "John"
        }, {
        name: "Ann"
    })
    /* 如图neicun1 */
    delete family.father;
    delete family.mother.husband;
    /* 如图neicun2 neicun3 neicun4*/
    /* John 现在是不可访问的,并将从内存中删除所有不可访问的数据。 */
    
    1. 在这里插入图片描述
    2. 在这里插入图片描述
  3. 在这里插入图片描述

  4. 在这里插入图片描述

标记清除

JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。

垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

到了 2008 年,IE、Firefox、Opera、Chrome 和 Safari 都在自己的 JavaScript 实现中采用标记清理(或其变体),只是在运行垃圾回收的频率上有所差异。

  1. 当变量进入环境时,就标记这个变量为进入环境

  2. 当变量离开环境时,就标记为离开环境。

  3. 垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。

  4. 在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题

  5. 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块

  6. 分配速度慢,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢

    var a="a";
    var b="b";
    a = b;
    

在这里插入图片描述

引用计数

另一种不太常见的方法就是引用计数法,引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为 1,;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减 1,当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为 0 的这些值。

let a = new Object() 	// 此对象的引用计数为 1(a引用)
let b = a 		// 此对象的引用计数是 2(a,b引用)
a = null  		// 此对象的引用计数为 1(b引用)
b = null 	 	// 此对象的引用计数为 0(无引用)

用引用计数法会存在内存泄露,下面来看原因:

function test(){
    let A = new Object()
    let B = new Object()
    A.b = B
    B.a = A
}

对象 A 和 B 通过各自的属性相互引用着,按照上文的引用计数策略,它们的引用数量都是 2,但是,在函数 test 执行完成之后,对象 A 和 B 是要被清理的,但使用引用计数则不会被清理,因为它们的引用数量不会变成 0,假如此函数在程序中被多次调用,那么就会造成大量的内存不会被释放

缺点

引用计数的缺点想必大家也都很明朗了,首先它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限,还有就是无法解决循环引用无法回收的问题,这也是最严重的

另一种没那么常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
引用计数最早由 Netscape Navigator 3.0 采用,但很快就遇到了严重的问题:循环引用。所谓循环引用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。比如:

function problem() {
 let objectA = new Object();
 let objectB = new Object();
 objectA.someOtherObject = objectB;
 objectB.anotherObject = objectA;
}

在这个例子中,objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是 2。在标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA 和 objectB 在函数结束后还会存在,因为它们的引用数永远不会变成 0。如果函数被多次调用,则会导致大量内存永远不会被释放。为此,Netscape 在 4.0 版放弃了引用计数,转而采用标记清理。事实上,引用计数策略的问题还不止于此。

在 IE8 及更早版本的 IE 中,并非所有对象都是原生 JavaScript 对象。BOM 和 DOM 中的对象是 C++实现的组件对象模型(COM,Component Object Model)对象,而 COM 对象使用引用计数实现垃圾回收。因此,即使这些版本 IE 的 JavaScript 引擎使用标记清理,JavaScript 存取的 COM 对象依旧使用引用计数。

换句话说,只要涉及 COM 对象,就无法避开循环引用问题。下面这个简单的例子展示了涉及 COM对象的循环引用问题:

let element = document.getElementById("some_element");
let myObject = new Object();
myObject.element = element;
element.someObject = myObject;

这个例子在一个 DOM 对象(element)和一个原生 JavaScript 对象(myObject)之间制造了循环引用。myObject 变量有一个名为 element 的属性指向 DOM 对象 element,而 element 对象有一个someObject 属性指回 myObject 对象。由于存在循环引用,因此 DOM 元素的内存永远不会被回收,
即使它已经被从页面上删除了也是如此。
为避免类似的循环引用问题,应该在确保不使用的情况下切断原生 JavaScript 对象与 DOM 元素之间的连接。比如,通过以下代码可以清除前面的例子中建立的循环引用:

myObject.element = null;
element.someObject = null;

把变量设置为 null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。
为了补救这一点,IE9 把 BOM 和 DOM 对象都改成了 JavaScript 对象,这同时也避免了由于存在两套垃圾回收算法而导致的问题,还消除了常见的内存泄漏现象。

JavaScript 对象生命周期的理解?

当创建一个对象时,JavaScript 会自动为该对象分配适当的内存垃圾回收器定期扫描对象,并计算引用了该对象的其他对象的数量如果被引用数量为 0,或惟一引用是循环的,那么该对象的内存即可回收

详细介绍V8 垃圾回收机制

V8是一种高性能JavaScript引擎,广泛用于Chrome浏览器和Node.js等平台。V8引擎采用了先进的垃圾回收机制来管理内存,确保有效地分配和释放内存资源。下面是V8的垃圾回收机制的详细介绍:

  1. 分代回收:
    V8的垃圾回收机制基于分代回收的原理。它将内存分为几个不同的代,其中新创建的对象被称为"新生代",而存活时间较长、经过多次垃圾回收的对象则被称为"老生代"。这种分代的机制可以根据对象的生命周期选择更适合的回收策略。

  2. 增量标记:
    V8的垃圾回收器采用增量标记算法,它将垃圾回收过程分为一系列的阶段。在每个阶段中,垃圾回收器会标记所有活跃的对象,并识别出未被使用的对象。这种增量标记的方式可以在回收过程中将工作分摊到多个阶段,减少对执行的影响。

  3. Scavenge回收器:
    对于新生代中的对象,V8使用Scavenge回收器进行垃圾回收。它将新生成的对象分为两个区域:From空间和To空间。在对象分配的过程中,V8会将对象分配到From空间,在触发垃圾回收时,回收器会检查From空间中的存活对象,并将其复制到To空间。然后,From空间和To空间进行角色翻转,以便下一次的垃圾回收。这种方式有效地清除不再使用的对象,并紧凑内存空间。

  4. Mark-Sweep和Mark-Compact回收器:
    对于老生代中的对象,V8使用Mark-Sweep和Mark-Compact回收器来进行垃圾回收。Mark-Sweep回收器首先会标记所有的活跃对象,然后将未被标记的对象释放。这种方式会导致内存空间的不连续性,可能会产生内存碎片。为了解决这个问题,V8还引入了Mark-Compact回收器,它会在清理垃圾对象之后,将存活对象向一端移动,从而实现内存空间的整理。

  5. 垃圾回收优化:
    V8的垃圾回收机制还采取了一些优化策略来提高性能。其中包括:

    • 对象分配:V8使用Bump指针分配方式来快速分配对象,避免了复杂的内存管理。
    • 并行回收:V8采用并行回收的方式,在垃圾回收过程中可以充分利用多核处理器的优势,加速回收过程。
    • 增量回收:在执行中断时,V8会尽量将垃圾回收分散到多个小的步骤中,以保持较短的中断时间,提高响应性能。

通过以上的垃圾回收机制,V8能够高效地管理JavaScript代码所使用的内存,有效地避免内存泄漏和过度内存占用,提高执行效率和性能。

1. 内存泄漏

内存泄漏是指,应当被回收的对象没有被正常回收,变成常驻老生代的对象,导致内存占用越来越高。内存泄漏会导致应用程序速度变慢、高延时、崩溃等问题。

内存生命周期

  1. 分配:按需分配内存。
  2. 使用:读写已分配的内存。
  3. 释放:释放不再需要的内存。

内存泄漏常见原因

  • 创建全局变量,且没有手动回收。
  • 事件监听器 / 定时器 / 闭包等未正常清理。
  • 使用 JavaScript 对象来做缓存,且不设置过期策略和对象大小控制。

V8 垃圾回收机制

V8 中有两个垃圾收集器。主要的 GC 使用 Mark-Compact 垃圾回收算法,从整个堆中收集垃圾。小型 GC 使用 Scavenger 垃圾回收算法,收集新生代垃圾。

两种不同的算法应对不同的场景:

  • 使用 Scavenger 算法主要处理存活周期短的对象中的可访问对象。
  • 使用 Mark-Compact 算法主要处理存活周期长的对象中的不可访问的对象。

因为新生代中存活的可访问对象占少数,老生代中的不可访问对象占少数,所以这两种回收算法配合使用十分高效。

分代垃圾收集

在 V8 中,所有的 JavaScript 对象都通过来分配。V8 将其管理的堆分成两代:新生代和老生代。其中新生代又可细分为两个子代(Nursery、Intermediate)。

即新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

image

Mark-Compact 算法(Major GC)

Mark-Compact 算法可以看作是 Mark-Sweep(标记清除)算法和 Cheney 复制算法的结合。该算法主要分为三个阶段:标记、清除、整理。

image

  1. 标记(Mark)

    标记是找所有可访问对象的过程。GC 会从一组已知的对象指针(称为根集,包括执行堆栈和全局对象等)中,进行递归标记可访问对象。

  2. 清除(Sweep)

    清除是将不可访问的对象留下的内存空间,添加到空闲链表(free list)的过程。未来为新对象分配内存时,可以从空闲链表中进行再分配。

  3. 整理(Compact)

    整理是将可访问对象,往内存一端移动的过程。主要解决标记清除阶段后,内存空间出现较多内存碎片时,可能导致无法分配大对象,而提前触发垃圾回收的问题。

Scavenger 算法(Minor GC)

V8 对新生代内存空间采用了 Scavenger 算法,该算法使用了 semi-space(半空间) 的设计:将堆一分为二,始终只使用一半的空间:From-Space 为使用空间,To-Space 为空闲空间。

image

新生代在 From-Space 中分配对象;在垃圾回收阶段,检查并按需复制 From-Space 中的可访问对象到 To-Space 或老生代,并释放 From-Space 中的不可访问对象占用的内存空间;最后 From-Space 和 To-Space 角色互换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值