内存管理与垃圾回收机制
前置知识
字和字节
位(bit): 一个二进制数码0或1, 是计算机存储处理信息最基本和最小的数据单位. 字节(byte): 一个字节由8个位组成, 是计算机存储信息的基本单位, 也是计算机存储空间大小的最基本单位. 1TB = 2^10GB = 2^20MB = 2^30KB = 2^40B = 2^50b 字(word): 若干个字节组成字, 16个位为一个字, 32位为一个双字, 64位是两个双字, 它代表计算机处理指令或数据的二进制数位数, 是计算机进行数据存储和数据处理的运算单位. 字长: CPU内每个字可以包含的二进制的长度称为字长(word size), 如32位(四个字节)表示CPU一次能处理四个字节.
内存
内存是一种用半导体技术做成的电子设备, 用于存储数据, 电子电路的数据是以二进制的方式存储, 存储器的每一个存储单元称为记忆元. 内存分为易失性的和非易失性的存储器, 但是一般我们说的内存指的是易失性的, 而非易失性的存储器我们称为磁盘. 内存主要是进程,文件加载,系统运行等高速缓存的临时运行存储空间. 内存速度快, 但是存储空间小, 而其中的缓存部分速度更加快. 每个进程在运行的时候都需要被分配内存.
内存的分配主要有静态分配和动态分配两种. 静态分配需要在编译阶段知道内存可用空间的大小, 是在编译时分配, 从栈空间中分配, 以后进先出的顺序移除这些调用. 动态分配不用知道内存可用空间的大小, 在运行时动态分配,从堆空间中分配, 没有特定的执行顺序.
JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏
概述
像 C 这样的编程语言,具有低级内存管理原语,如malloc()和free()。开发人员使用这些原语显式地对操作系统的内存进行分配和释放。
而JavaScript在创建对象(对象、字符串等)时会为它们分配内存,不再使用对时会“自动”释放内存,这个过程称为垃圾收集。这种看“自动”似释放资源的的特性是造成混乱的根源,因为这给JavaScript(和其他高级语言)开发人员带来一种错觉,以为他们可以不关心内存管理的错误印象,这是想法一个大错误。
即使在使用高级语言时,开发人员也应该了解内存管理(或者至少懂得一些基础知识)。有时候,自动内存管理存在一些问题(例如垃圾收集器中的bug或实现限制等),开发人员必须理解这些问题,以便可以正确地处理它们(或者找到一个适当的解决方案,以最小代价来维护代码)。
内存的生命周期
无论使用哪种编程语言,内存的生命周期都是一样的:
这里简单介绍一下内存生命周期中的每一个阶段:
- 分配内存 — 内存是由操作系统分配的,它允许您的程序使用它。在低级语言(例如C语言)中,这是一个开发人员需要自己处理的显式执行的操作。然而,在高级语言中,系统会自动为你分配内在。
- 使用内存 — 这是程序实际使用之前分配的内存,在代码中使用分配的变量时,就会发生读和写操作。
- 释放内存 — 释放所有不再使用的内存,使之成为自由内存,并可以被重利用。与分配内存操作一样,这一操作在低级语言中也是需要显式地执行。
内存是什么?
硬件层面上,计算机内存由大量的触发器缓存的。每个触发器包含几个晶体管,能够存储一位,单个触发器都可以通过唯一标识符寻址,因此我们可以读取和覆盖它们。因此,从概念上讲,可以把的整个计算机内存看作是一个可以读写的巨大数组。
作为人类,我们并不擅长用比特来思考和计算,所以我们把它们组织成更大的组,这些组一起可以用来表示数字。8位称为1字节。除了字节,还有字(有时是16位,有时是32位)。
很多东西都存储在内存中:
- 程序使用的所有变量和其他数据。
- 程序的代码,包括操作系统的代码。
编译器和操作系统一起为你处理大部分内存管理,但是你还是需要了解一下底层的情况,对内在管理概念会有更深入的了解。
在编译代码时,编译器可以检查基本数据类型,并提前计算它们需要多少内存 。然后将所需的大小分配给调用堆栈空间中的程序,分配这些变量的空间称为堆栈空间。因为当调用函数时,它们的内存将被添加到现有内存之上,当它们终止时,它们按照后进先出(LIFO)顺序被移除。例如:
编译器能够立即知道所需的内存:4 + 4×4 + 8 = 28字节。
这段代码展示了整型和双精度浮点型变量所占内存的大小。但是大约20年前,整型变量通常占2个字节,而双精度浮点型变量占4个字节。你的代码不应该依赖于当前基本数据类型的大小。
编译器将插入与操作系统交互的代码,并申请存储变量所需的堆栈字节数。
在上面的例子中,编译器知道每个变量的确切内存地址。事实上,每当我们写入变量 n 时,它就会在内部被转换成类似“内存地址4127963”这样的信息。
注意,如果我们尝试访问 x[4],将访问与m关联的数据。这是因为访问数组中一个不存在的元素(它比数组中最后一个实际分配的元素x[3]多4字节),可能最终读取(或覆盖)一些 m 位。这肯定会对程序的其余部分产生不可预知的结果。
当函数调用其他函数时,每个函数在调用堆栈时获得自己的块。它保存所有的局部变量,但也会有一个程序计数器来记住它在执行过程中的位置。当函数完成时,它的内存块将再次用于其他地方。