内存堆栈结构
参考:
堆栈与堆(Stack vs Heap):有什么区别?图文并茂拆解代码解析! - 知乎 (zhihu.com)
我们都知道值类型存在“栈”中,引用类型存在“堆”中。这篇文章深入讨论下内存的栈和堆的结构。
1.前言
1.指针
人们通常把“内存地址”形象的称作“指针”。栈内存和堆内存都是通过指针找到值(基本数据、对象、函数体等内容)。CPU为了管理内存而建立的虚拟地址空间(指针页表,map或表结构),将虚拟地址映射到物理地址。
指针页表:指针变量(后面会提到)到物理地址的映射。 管理内存的Map表由操作系统按一定规则创建,不同的应用程序管理自己独占的内存空间。
2.变量
变量是栈内存指针的别名。
声明变量是在指针页表里建立变量信息。而赋值才是真正的开辟内存空间。但是为了节省内存,会现在内存中查找有没有相同的值(栈的内存共享),如果有就把找到的内存地址更新到对应的map页表中,如果没有才会开辟内存空间。
所以变量名与值数据是分开存放的。保存变量名的内存地址称为指针变量。
我们读取或修改变量就是CPU和内存等计算机硬件根据“执行上下文”对内存进行寻址和修改的过程。
3.垃圾回收
当变量赋值为null时,也就是将指针页表中的值地址指向null,表示该变量可以被释放。是否释放或销毁该变量,需要看作用域中是否有其他地方引用了该变量。
垃圾回收机制每隔一段时间自动扫描指针页表,检索所有变量,判断被引用的次数,同时在作用域树上查找是否引用了该变量。如果引用标记次数为0,或者通过扫描未找到变量的引用。那该变量就会被释放,即从指针页表中删除。
4.内存溢出
内存溢出是指程序在申请内存的时候,没有足够的空间供其使用。比如内存用完了或者申请了一个int,但给他存了一个double才能存下的数
什么情况会导致这个:
1.栈帧过多导致栈内存溢出。一个函数内调用另一个函数,不断重复这个过程,每次调用都会分配一个栈帧,导致栈爆掉。
2.栈帧过大导致内存溢出,比如int去存double的值。
5.内存泄漏
内存泄露是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。比如,栈内存指向堆内存的地址丢失,导致无法及时回收。
2.内存分配
一般来说,内存可以分为以下几个部分:
1.全局段:存储全局变量和静态变量,这些变量的生命周期等于程序执行的整个持续时间。
2.代码段:包含组成我们程序的实际机器代码或指令,包括函数和方法。
3.堆栈段:用于管理局部变量、函数参数和控制信息(例如返回地址)
4.堆段:提供了一个灵活的区域来存储大型数据结构和具有生命周期的对象,堆内存可以在程序执行期间分配或释放。
2.栈内存(有序连续存储)
“栈”具有线程和“先进后出”的特点,也就是每个栈桢一般会保存下一个栈桢的地址,指向next节点(即指向下一个栈桢),从而手牵手形成类似队列的链式结构。同时先入栈的会先执行,后入栈的会先弹出(执行完销毁)。
特点:
1.数据一执行完毕,变量会立即释放,节约内存空间(函数运行完其申请的栈会全部释放(函数申请的栈是在总栈的最上面))
2.由于有序连续,存取速度快。
3.存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
3.堆内存(无序不连续)
堆内存允许我们在程序执行期间随时分配和释放内存。它非常适合存储大刑数据结构或大小事先未知的对象。
堆数据结构:图解基础数据结构——堆 - 知乎 (zhihu.com)
特点:
1.堆内存大小可以在程序执行过程中发生变化
2.在堆中分配和释放内存速度较慢,因为它涉及寻找合适的内存帧和处理碎片
3.存储在堆内存中的数据将一直保留在那里,直到我们手动释放它或程序结束
4.语言没有自动管理的话,需要手动管理
4.函数调用(C#)
调用函数时会创建称为堆栈帧的内存块。堆栈帧存储与局部变量、参数和函数的返回地址相关的信息。该内存是在堆栈段上创建的。
例子:
1 2 3 4 5 6 7 8 | internal class Employee { public int32 GetYearsEmployed() { ... } public virtual String GenProgressReport() { ... } public static Employee Lookup(String name) { ... } } internal sealed class Manager : Employee { public override String GenProgressReport() { ... } } |
1.线程栈就是前面提到的栈帧,走到M3函数的时候建好线程栈。
2.当JIT编译器将M3的IL代码转换成本地CPU指令时,会注意到M3的内部引用的所有类型:Employee、Int32、Manager以及String(因为"Joe")。假设走进M3之前string和Int32已经创建了类型对象。这里只加载Employee和Manager
3.在线程栈分配局部变量,初始化为null或0
4.在托管堆创建Manager对象实例,使其类型对象指针指向对应的类型对象,并调用该类的构造器。创建完成后将其在堆上的地址返回给e
5.调用Employee的静态方法Lookup。调用一个静态方法时,CLR会定位到与定义静态方法的类型对应的类型对象。然后,JIT再在其方法表中找到方法并编译。构造一个新的Manager对象并将地址赋给e。这时原来的Manager会被GC回收掉
6.调用非虚实例方法GetYearsEmployed。调用一个非虚实例方法时,JIT编译器会找到e对应的类型对象的方法并执行,如果没有找到,就依次回溯其类型对象的基类型的类型对象,之所以能这样回溯,是因为每个类型对象都有一个字段引用了他的基类型
7.调用虚实例方法GenProgressReport。调用一个虚实例方法时,会找到引用实例的真实类型(这里是Manager),通过他的虚表找到对应方法并执行(没找到就往上找)。
虚函数实现:虚函数表(转) - mc宇少 - 博客园 (cnblogs.com)
8类型对象的类型对象指针是指向Type类型对象的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了