前端同学的rust入门(三)--了解内存

117 阅读6分钟

概述

上一章讲述了Rust的一些基础类型,按照常理,本章应该讲解复合类型了。 但是在开始讲解之前,我们需要一些储备知识。 本章的内容需要一些计算机基础,我尽量讲述的简单一点。本章的内容一定要读懂,否则后面的章节会很难理解。

内存

计算机的本质,就是把数据从某个寄存器中读出来,进行计算,再放入另一个寄存器中。 类似的,程序的本质,其实就是告诉内核,如何从某一部分内存中读取数据,再交给CPU计算,然后把结果放入另一部分内存。

讲讲内存

计算机中说的内存,对应的硬件就是我们往主板上插的内存条。 但是编程中常说的内存,并不是这个叫内存的硬件。 操作系统对硬件进行了复杂的虚拟映射,编程中的说的内存,其实是这个虚拟内存。

内存的大小是按字节数来衡量的,内核会把内存按照一定长度,比如4个字节等分,分成很多个内存单元,每个内存单元有自己的编号,这个编号就是我们常说的指针。对于这些内存的使用,主要是堆和栈两种方式。

我们来做一个比喻,我们把内存单元比作碗,给碗刷一个编号叫指针,数据就是各种食物,往碗里放。

操作系统给每一个进程都会分配一定额度的虚拟内存,并将这部分内存区分为很多个区域。最常说的就是堆区和栈区(另外还有数据段,代码段不在本文介绍)

如果你对堆栈完全不了解,可以打开红宝书《JavaScript高级程序设计》查看‘变量,作用域和内存’相关章节。

[栈]

对前端来说,栈这种数据结构日常中接触的也会比较多。内存中的栈区,主要是用来存放基础变量。 比如number,bool这种数据。这些类型的特点是,他们在内存中的所占的字节数是确定的,都是4个字节。这样在栈区中存取都非常的方便。 需要注意,像字符串这种基础类型的数据,并不放在栈中,而是存放在前文所提的数据段中的。

[堆]

与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。 当需要一个较大内存时,内核会去查找足够多的碗,将其分组,来盛放你的食物。并且将这一组碗的编号作为指针,用以索引。 比如在JS中声明一个数组: const a = new Array(); 此时发生了两个事情, 首先在堆上申请了一块内存,然后把这块内存的指针赋值给了变量a,也就是把这个指针放在了栈中。 这里会有同学疑问, new Array 并没有写长度,内核怎么知道申请多大的内存呢。实际上这是V8做了默认的配置,当你使用New Array时,会默认开辟一定长度的内存。当你无限的往数组里push内容时,内存的容量不足了,就会再次重新开辟一段更大的内存。

[性能区别]

写入方面:入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备。

读取方面:得益于 CPU 高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在 10 倍以上!栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。

因此,处理器处理分配在栈上数据会比在堆上的数据更加高效。

自高级编程语言以来,程序员们就在于内存做艰苦的斗争,在C和C++的开发中,最多的问题就是内存'panic'了。 我们来看一下不同的编程语言,对内存的处理方式,其实也主要就是堆区内存的斗争:

C 语言

以C语言和C++为代表的初代高级编程语言,需要手动去管理内存。 比如我们需要实现一个动态数组,就需要做两个事情:

  1. 调用malloc方法申请一个内存空间
  2. 在函数结束时,调用free方法释放掉内存

很显然,如果你的程序逻辑复杂,漏掉一两个变量的free是很常见的事件。这就是大名鼎鼎的'内存泄漏'。 更严重的是,如果你只申请了100个字节的内存,但是通过指针,你是可以合法的读写第101个字节的内容,这就有可能引起非常严重的安全事故。

JS

内存带来的心智负担相当繁重,以java,js为代表的语言通过引入运行时或者解释器,来消除这一负担。 比如JS的引擎V8,通过引入一套复杂的GC算法,来进行内存管理。 但是GC算法有一个问题,就是它无法精确的知道内存释放的时间,只能靠一些手段去猜测,这自然带来性能的下降。 并且这些手段虽然能解决大部分的内存问题,但是依然会有少量情况会引起内存泄漏,比如循环引用。

rust

rust带来另一个方案,它吸收两者的优缺点,做了一个巧妙,但是又令人难以掌握的设计。 rust接管了内存的释放,通过在编译阶段做的处理,使得编译后的代码,一旦离开了作用域就会被释放内存,这样就没有引入GC,但是达到了有GC的效果,程序员就不怕出现内存泄漏了。 但是这样势必会带来新的问题:

  1. 按照这个设计,一旦我在某个函数中返回了一个复合类型的数据,rust直接释放,就会导致这个返回值无效
  2. 一个我某一个参数同时传递给了函数A和函数B,但是函数A提前执行结束,释放了参数的内存,此时B在执行时就会报错。

如何解决这些问题呢? rust设计除了借用和引用的概念。