一、内存分配
了解内存分配是计算机编程的基础。内存提供了存储数据和程序高效运行所需的所有命令的空间,程序本质上就是数据+指令,两者都需要分配内存空间。就好比菜板和菜刀是指令,蔬菜是数据,厨房就是计算机的内存,我们要进行“切菜”这个程序,厨具和蔬菜肯定都在厨房里面占用一定的空间才行。要是厨房里面连厨具都没有,那还切什么菜呢,只能是歇菜了。
计算机的内存可以分为以下几个部分:全局段(Global segment)、代码段(Code segment)、堆栈(Stack)、堆(Heap)。
- 全局段:负责存储全局变量和静态变量,这些变量的生命周期等于程序执行的整个持续时间。
- 代码段:也称为文本段,包含组成我们程序的实际机器代码或指令,包括函数和方法。
- 堆栈段:用于管理局部变量、函数参数和控制信息(例如返回地址)。
- 堆段:提供了一个灵活的区域来存储大型数据结构和具有动态生命周期的对象。堆内存可以在程序执行期间分配或释放。
二、栈内存(Stack)
每个程序都有自己的虚拟内存布局,由操作系统映射到物理内存。栈内存是为线程留出的临时空间,每个线程都有一个固定大小的栈空间,而且栈空间存储的数据只能由当前线程访问,所以它是线程安全的。栈空间的分配和回收是由系统来做的,我们不需要手动控制。
栈内存用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。当一个函数调用时,系统就会为该函数的调用分配栈空间,当函数返回后,系统就会自动回收这块空间,同理,下次其它函数调用和返回,系统还是会自动分配和回收空间。
栈内存通常存储程序中的值类型数据,因为它是按顺序排序的,所以访问速度非常快,比堆内存的访问速度快。栈内存中的数据随着方法的结束而自动释放,程序员不必担心内存泄露的问题。
什么是内存泄露
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
三、堆内存(Heap)
堆内存,也称为动态内存。堆内存允许我们在程序执行期间随时分配和释放内存。它非常适合存储大型数据结构或大小事先未知的对象。比如在C#中使用new关键字实例化的对象就会存储在堆内存上。堆内存让程序员又爱又恨,爱的是它非常灵活,可以动态申请任意长度的内容空间。这与栈内存不同,比如我们在栈内存上申请了一个byte数据,它的长度就只有8位,固定得死死的,而我们new一个对象,这时会在堆上开辟内存空间,空间长度随着这个对象的长度而定,富得流油的感觉。
当然,它也有缺点啦,第一是容易产生内存泄露,第二是容易产生内存碎片,第三是线程不安全。
可见,堆内存和栈内存都有自己的优势与不足,作为程序员,要非常了解它们的特点,尽量发挥它们的长处,避免它们的短处。
比较点 | 栈 | 堆 |
速度 | 快 | 慢 |
空间管理 | 高效,不会产生碎片 | 会产生内存碎片 |
访问权限 | 只能局部变量 | 可以访问全局变量 |
空间大小限制 | 操作系统限制 | 没有特定的限制 |
内存分配 | 连续 | 随机分配 |
分配和释放 | 编译器指令自动管理 | 程序员手动管理 |
开销 | 低 | 高 |
主要问题 | 空间小 | 内存碎片 |
灵活性 | 固定大小 | 可改变 |
四、引用类型与值类型
在前面我们学习了.NET的公共类型系统(Common Type System,CTS),它提供了一系列的数据类型,在这些类型中,有些是基础的数据类型,例如Boolean
、Byte
、Char
等,有些则是比较复杂的数据类型,例如类、结构、枚举、界面、委托。不同的数据类型,本质上是对计算机内存的长度的一种别称。比如程序员要临时处理一个人的年龄数据,这个我们在前面也讨论过,人的年龄大概范围就是0-120之间,很少使用小数点的情况,所以,申请一个byte(字节)大小的内存空间就足够了,因为byte表示0-255的整数。事实上我们通常使用int(整型)去定义一个变量来保存年龄数据,int长度为32位,可表示的数值范围是-2,147,483,648 到 2,147,483,647。我们要保存一个人的名字时,由于名字是一个字符串,所以就不能用byte、int之类的数据类型,而应该采用string去声明一个变量,这个string也是向计算机内存开辟一个空间,最后将名字写到这个内存空间上。
由于计算机拥有四种内存:全局段(Global segment)、代码段(Code segment)、堆栈(Stack)、堆(Heap),那么,哪些数据类型会在堆(Heap)上开辟,哪些数据类型又在栈(栈)上开辟呢?.NET将数据类型分成两种,分别是值类型(Value Type)和引用类型(Reference Type)。值类型的数据都会在栈内存上开辟,引用类型的数据会在堆内存上开辟。但是,引用类型变量在堆内存中的首地址会保存在栈内存中,也就是说,引用类型变量的实际数据存储于托管堆,变量本身仅仅是一个指向堆中实际数据的地址。那么从这里开始,一个神奇的效果就产生了,那就是,如果我们将一个值类型的变量赋值给另一个值类型的变量,会在内存中产生两份相同的数据,两份数据各自拥有自己的内存地址;如果我们将一个引用类型变量a赋值给另一个引用类型变量b,由于a只是代表这个引用类型数据在堆内存上的地址,所以b也只是保存了这个地址,换句话说,a和b共同指向这个堆内存上的地址,此时依旧只有一份引用类型数据(实例)。
这就是值类型的值传递和引用类型的引用传递的区别!
最后,我们来了解C#的数据类型分类,哪些是引用类型,哪些是值类型。
——重庆教主 2023年12月21日