JavaScript工作原理(三):内存管理和4种常见的内存泄漏
该系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述。第二篇文章深入剖析了Google的V8 JavaScript引擎,并提供了关于如何编写更好的JavaScript代码的一些提示。
在第三篇文章中,我们将讨论另一个越来越被开发人员忽视的关键主题,因为日常使用的编程语言(内存管理)越来越成熟和复杂。我们还会提供一些关于如何处理内存泄漏的技巧。
概述
像C这样的编程语言,提供从底层上管理内存的方法(原语),如malloc()和free()。开发人员使用这些方法(原语),用来从操作系统分配内存,或释放内存到操作系统中。
当对象或字符串等被创建时,JavaScript会申请和分配内存;当对象或字符不在被使用时,它们就会被自动释放,这也被称为垃圾处理。这种释放资源的看似是“自动”的,这恰恰是误解的来源,给JavaScript(以及其他高级语言)开发人员造成了他们可能选择不关心内存管理的错误印象。这是一个大错误。
即使使用高级语言,开发人员也应该理解内存管理(或者至少是基础知识)。有时自动内存管理也会出现问题(如bugs或者垃圾回收限制等),开发人员不得不先了解它们,然后才能妥善处理。
内存生命周期
无论您使用什么编程语言,内存生命周期几乎都是一样的:
以下简单描述了在该周期的每个步骤中发生的情况:
分配内存 - 内存由操作系统分配,允许程序使用它。在底层语言(如C)中,这是一个显式操作,您作为开发人员应该处理。然而,在高级语言中,这个操作被隐藏了。
使用内存 - 这是您的程序实际使用之前分配的内存。读取和写入操作发生在您在代码中使用分配的变量时。
释放内存 - 现在是释放您不需要的整个内存的时间,以便它可以变为空闲并再次可用。 与分配内存操作一样,这个操作在底层语言中是可以直接调用的。
有关调用堆栈和内存堆的概念的概述,您可以阅读本系列第一篇文章。
什么是内存?
在开始讨论JavaScript的内存之前,我们将简要讨论一般内存概念以及它如何工作。
在硬件级别上,计算机内存由大量的触发器。每个触发器都包含一些晶体管并且能够存储一个bit。单个触发器可通过唯一标识符进行寻址,因此我们可以读取并覆盖它们。因此,从概念上讲,我们可以将整个计算机内存看作是我们可以读写的bit数组。
从人类角度来说,我们不擅长用bit来完成我们现实中思想和算法,我们把它们组织成更大的部分,它们一起可以用来表示数字。 8位(比特位)称为1个字节(byte)。除字节外,还有单词(word)(有时是16,有时是32位)。
很多东西都存储在这个内存中:
所有程序使用的所有变量和其他数据。
程序的代码,包括操作系统的代码。
编译器和操作系统一起工作,为您处理大部分内存管理,但我们建议您看看底下发生了什么。
编译代码时,编译器可以检查原始数据类型并提前计算它们需要多少内存。然后将所需的内存分配给调用堆栈空间中的程序。分配这些变量的空间称为堆栈空间,因为随着函数的调用,它们的内存将被添加到现有内存之上。当它们终止时,它们以LIFO(后进先出)顺序被移除。例如,请考虑以下声明:
int n; // 4字节int x [4]; // 4个元素的数组,每个4个字节double m; // 8个字节
编译器可以立即看到代码需要
4 + 4×4 + 8 = 28个字节。
这就是它如何处理整数和双精度的当前大小。大约20年前,整数通常是2个字节,并且是双4字节。您的代码不应该依赖于此时基本数据类型的大小。
编译器将插入与操作系统进行交互的代码,以在堆栈中请求必要的字节数,以便存储变量。
在上面的例子中,编译器知道每个变量的确切内存地址。事实上,只要我们写入变量n,就会在内部翻译成类似“内存地址4127963”的内容。
注意,如果我们试图在这里访问x[4],我们将访问与m关联的数据。这是因为我们正在访问数组中不存在的元素 - 它比数组中最后一个实际分配的元素x [3]更远了4个字节,并且可能最终读取(或覆盖)m个位中的一些位。这对方案的其余部分几乎肯定会有非常不希望的后果。
当函数调用其他函数时,每个函数在调用时都会获得自己的堆栈块。它保留了它所有的局部变量,同时还有一个程序计数器,记录它在执行时的位置。当功能完成时,其存储器块再次可用于其他目的。
动态分配内存
不幸的是,当我们在编译时有时不知道变量需要多少内存时,假设我们想要做如下的事情:
int n = readInput(); //用户输入...//常见一个长度为n的数组
在编译时,编译器不知道数组需要多少内存,因为它由用户提供的值决定。
因此,它不能为堆栈上的变量分配空间。 相反,我们的程序需要在运行时明确要求操作系统提供适当的空间。 该内存是从堆空间分配的。 下表总结了静态和动态内存分配之间的区别:
为了充分理解动态内存分配是如何工作的,我们需要在指针上花费更多时间,这可能与本文的主题偏离太多。 如果您有兴趣了解更多信息,请在评论中告诉我们,我们可以在以后的文章中详细介绍指针。
JavaScript分配内存
现在我们将解释第一步(分配内存),以及它如何在JavaScript中工作。
JavaScript减轻了开发人员处理内存分配的责任 - JavaScript自身声明的时候就分配内存,然后赋值。
var n = 374; // 为数字分配内存 var s = 'sessionstack'; // 为字符串分配内存 var o = { a: 1, b: null }; // 为对象和它的值分配内存 var a = [1, null, 'str']; // (类似对象) 为数组和它的值 // 分配内存 function f(a) { return a + 3; } // 为函数分配内存 (which is a callable object) // 函数表达式也会分配内存 someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false);
一些函数调用也会导致对象分配:
var d = new Date(); // 为日期对象分配内存var e = document.createElement('div'); // 为DOM元素分配内存
方法可以分配新的值或对象:
var s1 = 'sessionstack'; var s2 = s1.substr(0, 3); // s2 is a new string // 由于字符串是不可改变的, // JavaScript may decide to not allocate memory, // but just store the [0, 3] range. var a1 = ['str1', 'str2']; var a2 = ['str3', 'str4']; var a3 = a1.concat(a2); // new array with 4 elements being // the concatenation of a1 and a2 elements
在JavaScript中使用内存
基本上在JavaScript中使用分配的内存意味着读取和写入。
这可以通过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。
当内存不再需要时释放
大部分内存管理问题都是在这个阶段出现的。
这里最困难的任务是确定何时不再需要分配的内存。它通常需要开发人员确定程序中的哪个地方不再需要这些内存,并将其释放。
高级语言嵌入了一个名为垃圾收集器的软件,其工作是跟踪内存分配和使用情况,以便找到何时不再需要分配的内存,在这种情况下,它会自动释放它。
不幸的是,这个过程是一个大概,因为知道是否需要某些内存的一般问题是不可判定的(不能由算法解决)。
大多数垃圾收集器通过收集不能再访问的内存来工作,例如,指向它的所有变量都超出了范围。然而,这是可以收集的一组内存空间的近似值,因为在任何时候内存位置可能仍然有一个指向它的变量,但它将不会再被访问。
垃圾收集
由于发现某些内存是否“不再需要”的事实是不可判定的,所以垃圾收集实现了对一般问题的解决方案的限制。本节将解释理解主要垃圾收集算法及其局限性的必要概念。
内存引用
垃圾收集算法所依赖的主要概念是参考之一。
在内存管理的上下文中,如果一个对象可以访问后者(可以是隐式或显式的),则称该对象引用另一个对象。例如,JavaScript对象具有对其原型(隐式引用)及其属性值(显式引用)的引用。
在这种情况下,“对象”的概念扩展到比常规JavaScript对象更广泛的范围,并且还包含函数范围(或全局词法范围)。
词法范围定义了如何在嵌套函数中解析变量名称:即使父函数已返回,内部函数也包含父函数的作用域。
共同学习,写下你的评论
评论加载中...
作者其他优质文章