栈和堆两种内存空间的一些理解

68 阅读4分钟

思考

在刚学编程语言之初,对这两种内存空间是很难理解的,对于一些使用自动管理内存方式的语言(例如 java、python、javascript等)的开发者其实很多时候并不怎么关注

我在最初学javascript的时候听到的表述就是

基本类型存储在栈上,对象存储在堆上,少量数据存储在栈上,大量数据存储在堆上

这些说法有一定道理但是不完全对,并没有抓到实质,作为一名开发者我们要有思考问题本质的一个能力,下面我将浅谈一下我对设计原理的一些思考

栈和堆

栈是程序运行的基础,一个线性结构,遵循着先进后出的一个原则,这和函数式编程的逻辑是一致的,每当一个函数被调用,一块连续的内存会在栈顶被分配出来,这块内存被称为栈帧,栈帧的大小一般是固定的,编译时期就确定的,所以存放在栈上的数据大小一般都是确定的,大小不可变的,大小会变化的一般会放在堆上

栈本身没有很大的结构复杂性线性空间一般比较好操作,效率很高,但是开发中也要合理控制栈空间内存的分配,防止栈溢出,一旦超出系统分配的栈空间,程序就容易崩溃

堆空间的一些问题

堆内存空间的灵活性实际上给内存管理带来了很大挑战

像 c 和 c++ 这种手动管理内存的语言,堆上分配内存后很容易忘记释放,非常容易造成内存泄露,对于这类语言的开发者需要有很强的内存管理意识,一旦内存泄漏,程序运行的越久就越容易造成内存积压,时间久了就容易崩溃,现代生产中有不少企业的工业机器会面临间歇性的宕机的问题,如果使用 rust 会很有效的避免这一问题,当然我这里并不是吹捧 rust,后面我也会出一期文章谈一谈rust的优势以及面临的一些挑战

常说的两大内存安全问题,一个是堆越界(第一大内存安全问题),一个是栈上指针悬空

堆越界

现代系统设计往往追求高并发会开辟多个线程来处理任务,每个线程都会有自己的栈空间,不同的栈空间的数据是很难互相访问的,多数语言的选择是访问堆空间的数据,如果堆空间的数据被多个线程的调用栈引用就不可避免的出现一些竞态关系,所以对内存的改动要十分小心,往往需要加锁来限制对同一块数据的访问

image.png

如图例所示,如果A和B线程的执行栈中都有访问堆空间有个列表的胖指针,A线程释放掉了列表的某一项空间,B如果还遍历这个列表,就有可能访问野指针,导致堆越界,根据微软安全反应中心的研究,这是第一大内存安全问题

指针悬空

使用已经释放掉的内存的情况是第二大内存安全问题

image.png

如图所示一开始Vec A指向对应的堆空间,但是由于A的push操作超出了Vec原先的Capacity,这时候一般会重新开辟一个更大的空间将原先空间的数据拷贝过去,如果拷贝过后原先的空间被释放,那么后续Vec A的指针指向的会是已经释放的空间,这种情况很容易造成程序崩溃甚至会有安全隐患

现代编程语言自动管理内存的方式

内存问题确实带来了很大困扰,当然也有一些自动内存管理的语言,降低了开发者的心智负担,但是是以性能损耗换来的

对于像 java 和 javascript 这类语言往往内存管理都是自动的,开发者往往不需要在这上面花费太多精力 常用的两种管理堆内存的方式,一个是追踪式垃圾回收,一个是自动引用技术,当然这里就不做详解了,后续我会详细介绍一下

而 rust 给我的感觉属于介于两者中间,给你自由空间,但是也做了一些限制,因为要严格保证内存安全,毕竟 rust 以内存安全著称,rust引入生命周期和所有权的概念也是为了解决内存问题,所以我觉得一个合格的rust开发者需要对内存管理有一个充分的理解