为什么JavaScript会自动清理内存,但我们仍然需要理解它的工作原理?
\n\n当你用JavaScript编写代码时,很少会考虑变量离开作用域后会发生什么。与C或C++不同,你必须手动调用free()或delete,而JavaScript承担了清理内存的脏活。这很方便——直到你的应用程序开始“卡顿”、消耗数GB内存,或者突然崩溃并报错Out of Memory。
熟悉吗?那么让我们来真正了解一下JavaScript中的内存管理以及什么是JavaScript垃圾回收器。剧透:这不是魔法,而是一个严格的算法,你在编写代码时应该(并且需要)考虑到它。
\n\nJavaScript如何存储数据:栈和堆
\n\n在讨论垃圾回收之前,需要了解数据存储在哪里。JavaScript有两个主要的存储区域:
\n\n- \n
- 栈(Stack)——快速但有限的内存。它存储原始类型(数字、字符串、布尔值)和对对象的引用。每次函数调用都会在栈中创建一个新的帧。 \n
- 堆(Heap)——非结构化的内存区域,对象、数组、函数和闭包都存储在这里。这里才是内存争夺的主战场。 \n
当你创建一个对象时:
\n\nlet user = { name: 'Иван', age: 30 };\n\n\n引用user进入栈,而对象{ name: 'Иван', age: 30 }本身进入堆。只要对象至少有一个引用,它就存活。一旦没有引用,该对象就变成了“垃圾”。
JavaScript中的垃圾回收器:主角
\n\nJavaScript中的垃圾回收器(Garbage Collector, GC)是一个后台进程,它会定期检查堆并删除不可达的对象。听起来简单,但实际上这是在清理速度和代码性能之间的复杂权衡。
\n\nMark-and-Sweep算法(标记与清除)
\n\n现代引擎(Chrome的V8,Firefox的SpiderMonkey)使用Mark-and-Sweep算法。它分两个阶段工作:
\n\n- \n
- 标记(Mark): GC从根对象(全局对象、当前函数、局部变量)开始。它遍历从根出发的所有引用,并标记每个可达的对象。 \n
- 清除(Sweep): 所有未标记的对象被视为垃圾并被删除。内存被释放。 \n
简单示例:
\n\nlet a = { data: 'A' };\nlet b = { data: 'B' };\na.ref = b; // a引用b\nb.ref = a; // b引用a\n\n// 现在我们有了循环引用\na = null; // a不再指向该对象\nb = null; // b不再指向该对象\n\n\n以前(在Mark-and-Sweep出现之前),这样的循环引用会导致内存泄漏。但现代GC明白:如果两个对象从根都不可达,那么它们就是垃圾,即使它们互相引用。
\n\n对象的分代:新生代和老年代
\n\n大多数对象存活时间很短。例如,函数内部的临时变量。因此,V8将堆分为几代:
\n\n- \n
- 新生代(Young Generation): 新创建的对象。这里的垃圾回收频繁且快速(Scavenge算法——将存活对象复制到另一个区域)。 \n
- 老年代(Old Generation): 经历了多次垃圾回收仍然存活的对象。它们被检查的频率较低,但过程耗时更长。 \n
这种方法可以避免在完整检查整个堆时“冻结”页面。
\n\nJavaScript中的内存泄漏:如何造成(以及如何避免)
\n\n即使有了智能的垃圾回收器,你仍然可能造成内存无法释放的情况。以下是导致内存泄漏的最常见原因: