内存管理,泄露,调试
# nodejs 的内存管理,内存泄露和追查调试
# 内存生命周期
在JavaScript中,当我们创建变量、函数或者任何你能想到的,JS引擎都会为它分配内存,当它不会再被用到的时候就会被释放掉。
在内存中,分配内存是保留空间的过程,而释放内存则是释放空间,释放出来的空间准备用于其他目的。
我们每次声明一个变量或创造一个函数,该变量的存储通常会经历以下相同的阶段:
Allocate 分配内存 JavaScript为我们解决了这个问题:它为我们创建的将会用到的对象分配内存。
Use 使用内存 使用内存是我们在代码中明确进行的工作:对内存的读写不过是对变量的读写。
Release 释放内存 这一步也是JS引擎处理的。一旦分配的内存被释放,它就可以被用到新的地方去了。
内存管理中“Objects”不止包含JS对象,也包括函数和函数作用域。
# 内存堆和栈
我们现在知道在JavaScript中定义的任何东西,JS引擎都会为它分配内存并在不需要的时候将其释放掉。
下一个我想到的问题是:存储到哪里?
JS引擎有两个地方可以存储数据:内存堆和栈。
堆和栈是引擎用于不同目的的两种数据结构。
# 栈:静态内存分配
你或许从本系列的第一部分call stack and event loop中已经知道了栈,在那篇文章中我重点介绍了如何使用它来跟踪JS解释器需要调用的函数。
所有的值都被存储在栈中因为它们都包含原始值
栈是一种JavaScript用来存储静态数据(static data)的数据结构。静态数据是指引擎在编译阶段就知道大小的数据。在JS中,这包括了原始值(string、number、boolean、undefined、null、symbol[1])和指向对象、函数的引用。
由于引擎知道大小不会变,所以它会给每个值分配固定数量的内存。
在执行之前立即分配内存的过程被称为静态内存分配。
因为引擎给这些值分配了固定数量的内存,所以原始值的大小是有限制的。
这些值和整个栈的限制取决于浏览器。
# 堆:动态内存分配
堆是用于存储数据的不同空间,也是JavaScript存储对象和函数的地方。
不同于栈,引擎不会给这些对象对分配固定数量的内存。相反,将根据需要分配更多空间。
这种分配方式称为动态内存分配。
这里列出了两种存储的并排比较的特征:
栈Stack | 堆Heap |
---|---|
原始值和引用 | 对象和函数 |
编译时知道大小 | 运行时知道大小 |
分配固定量空间 | 每个对象没有 |
# 例子
让我们看一些代码示例。在标题中我提到的分配的内容:
const person = {name:'john',age:12}
JS在堆中为这个对象分配内存。实际值仍然是原始值,这就是它们被存储在栈中的原因。
数组也是对象,这就是它们被存储在堆中的原因。
原始值是不可改变的,这意味着JavaScript不是更改原来的值,而是创建一个新值。
# JavaScript引用
所有变量首先指向栈。如果它是一个非原始值,栈就会包含一个指向堆中对象的引用。
堆内存不是按特定方法排序的,这就是我们需要在栈中保留对其引用的原因。你可以视引用为地址,视堆中的对象为这些地址所属的房屋。
记住JavaScript在堆中存储对象和函数,在栈中存储原始值和引用。
# 垃圾回收
现在,我们知道了JavaScript如何给不同对象分配内存,但如果我们还记得内存生命周期,还差最后一步:释放内存。
和内存分配一样,JavaScript引擎也为我们处理了这一步。更确切地说,垃圾回收器为我们做了这件事。
一旦JavaScript引擎识别出一个给定的变量或函数不会再被用到,它就会释放该变量或函数占用的内存。
其中最大的问题就是,某些内存是否仍被需要是一个无法确定的问题。这意味着不可能有一个算法能在它过时的那一刻立即收集不再需要的内存。
一些算法为该问题提供了类似的不错的解决方法。我将会在本节中讨论最常用的方法:引用计数垃圾收集和清除标记算法。
# 引用计数
这是最简单的类似解决方案。它收集那些引用为0的对象。
让我们来看看下面的例子。线代表引用。
注意如何做到最后一帧中只有 hobbies 保留在堆中的,因为最后它依旧是含有引用的对象。
循环 这个算法的问题是没有考虑循环引用。当一个或多个对象互相引用但无法再通过代码访问它们时,该问题就出现了。
# 标记清除算法
标记清除算法解决了循环依赖问题。它检测是否可以从根对象访问它们,而不是简单的计算给定对象的引用。
根对象在浏览器中指 window ,在NodeJS中指 global 该算法将无法访问的对象标记为垃圾,然后清理它们。根对象永远不会被收集。
在这种情况下,循环依赖不再是问题。在之前的例子中,无论是dad 还是 son对象都不能从根对象访问了,所以它们两个都将被标记为垃圾并收集。
2012年以来,该算法已在所有现代浏览器中实现。仅在性能和实现方面进行了改进,而没有改进算法的核心思想本身。
权衡取舍 自动垃圾回收允许我们专注于构建应用而不用在内存管理上浪费时间。但是我们仍注意一些权衡取舍。
内存使用
鉴于算法无法确定何时不再需要确切的内存,JavaScript应用程序可能会使用比它们实际需要更多的内存。
即使对象被标记为垃圾,何时及是否将收集分配的内存也是由垃圾收集器决定的。
如果你希望应用程序尽可能提高内存效率,那最好使用低级语言。但记住这需要权衡取舍。
性能
该垃圾回收算法为了清理未使用对象通常会周期性执行。
问题是我们开发人员无法确切得知道这会何时发生。收集大量垃圾或频繁收集垃圾可能会性能,因为这样做需要一定数量的计算能力。
但是,这种影响对使用者或开发者而言通常忽略不计。
# 内存泄漏
有了内存管理的知识,我们来看看最常见的内存泄漏。
如果了解幕后情况我们就可以轻松避免这些。
# 全局变量
存储全局变量可能是最常见的内存泄漏类型。
在浏览器下的JavaScript中,如果没有使用 var const let ,变量就会被赋到window 对象上。
在严格模式下运行代码可以避免这种情况。
除了意外地添加变量到根对象上,在许多情况下你可能会故意这样做。
你当然可以使用全局变量,但确保不再需要这些数据时就释放掉空间。
可以将全局变量赋值为null 从而释放内存。
# 被遗忘的定时器和回调
忘掉定时器和回调会增加应用程序的内存使用。特别是在单页应用(SPAs)中,当动态添加了事件监听器和回调时必须小心。
# 检查内存泄露
追踪NodeJS代码中的内存泄漏一直是一个很有挑战的难题。 本文讨论如何从一个node写的应用里自动的跟踪到内存泄漏问题,在这里笔者向大家推荐两款追查内存问题的神器 —— memwatch 和 heapdump