Skip to content

Latest commit

 

History

History
52 lines (37 loc) · 3.71 KB

memory-model.md

File metadata and controls

52 lines (37 loc) · 3.71 KB

程序中的内存模型

Q

  • 经典的内存模型包括哪几个部分?
  • 堆和栈的区别?为什么要区分堆和栈?
  • 如何查看、修改系统范围的栈大小?什么情况下可能需要修改?

A

经典的内存模型包括 1)常量和指令 2)栈 3)堆 三个部分。栈是静态分配的,存储本地变量,函数在栈上执行,结束后即释放栈帧;堆是动态分配的,存储全局变量。

  1. 常量和指令|即静态内容,通常只是很小一部分
  2. 栈|定长的,分为许多个栈帧(stack frame),通过调用栈我们可以分析程序是如何执行的。Go 的内存模型比较特别,它使栈也具有了伸缩性,让Go语言更适合处理并发和协程。因此,Go 应用的栈是不定长的
  3. 堆|可伸缩、动态分配的,程序可以在运行时向操作系统申请

要注意的是,对物理内存做的这些区分,是 runtime 决定的(而不是操作系统),各个编程语言具体处理的方式都不一样。

  • 栈是静态分配的,编译的时候就可以确定分配多少空间;堆是动态分配的
  • 栈以 LIFO(后入先出)的方式工作,不能 random access;堆没有特定的顺序,通过指针访问,因此在栈上分配空间比堆要快
  • 函数调用都是在栈上,每一个新的函数,就是一个新的栈帧。Goroutine 也是在栈上创建
  • 栈是临时的存储空间,一旦任务执行完毕,函数退出,就会释放(pop out)栈帧。因此,应该避免返回栈上的变量的指针(在 Go 中这样做会发生内存逃逸)
  • 栈存储的都是本地变量,堆存储全局变量
  • 在栈上分配空间需要找到连续的空闲区域,在堆上则需要使用 malloc() 之类的函数

为什么需要区分栈和堆呢?

  1. 性能差异:栈内存分配相比于堆内存分配更快,因为栈内存分配仅仅是移动栈顶指针,而堆内存分配则涉及到寻找并标记一块足够大的空闲内存区域,这个过程可能需要遍历整个堆空间。
  2. 空间限制:栈是定长的,然而很多情况下不能在编译期就计算好内存大小。栈空间大小在程序开始运行时就确定了,而堆的大小可以在程序运行期间动态变化。因此,对于那些需要大量内存或者在编译时无法确定大小的数据结构(如动态数组),一般会使用堆。
  3. 生命周期:存储在栈上的变量,其生命周期由编译器自动管理,一般在函数结束后,这些变量就会自动清理。但对于堆上的数据,必须由程序员手动管理,忘记释放内存会导致内存泄漏。
  4. 作用域:存储在栈上的数据只能在其被声明的作用域中访问,而堆上的数据在整个程序中都是可访问的。

为什么不能直接让所有对象都在堆上创建呢?

理论上虽然所有对象都可以在堆上创建,但这样做会带来性能损失,内存管理复杂度增加以及增大内存泄漏的风险。在实际编程中,我们会根据变量的大小、生命周期和作用范围来决定是使用栈还是堆。

如果让所有对象都在堆上创建,就没办法提前释放一些内存资源,性能会受影响——除非它们已经在栈的顶端了。栈空间资源的销毁和创建的时机一定是相对应的

查看、修改系统范围的栈大小

# 查看系统默认的栈大小(soft limit)
ulimit -s
# 或者
ulimit -Ss

# 查看系统默认的栈大小(Hard limit)
ulimit -Hs

# 修改栈大小(soft limit)
ulimit -s 16192

如果机器内存不够用,而栈大小数值又太大的话,可能会导致过高的内存占用。减小栈大小可能能够减小 Swap 分区过大问题。