引言:带着疑问探索栈内存的来龙去脉
栈内存大小是如何被确定的,最终又是如何被创建的?
栈保留大小和栈提交大小是什么?
为什么会产生栈溢出?
带着这些疑问,这个视频我将带着搞清楚栈内存的来龙去脉。

程序启动时栈空间的创建
当你的程序启动时,首先主线程会被创建,操作系统在创建线程时,会为每个线程在虚拟地址空间预留一块固定大小的逻辑上连续的栈空间。
对,请记住,它是连续的栈空间。
你也没有听错,是线程不是进程。 操作系统会为每个线程预留栈内存,每个线程拥有自己独立的栈内存空间。
这个大小通常是操作系统默认的,例如WINDOWS下默认给每个线程分配1M的栈内存,而LINUX下给每个线程分配8M的栈内存。
当然你也可以在编译器指定程序的默认栈空间大小。
如何在Visual Studio中设置栈大小
以VS为例,你可以在编译器中指定线程默认的栈内存大小。
打开项目配置中,选择链接器(linker),再找到system。配置中的stack reserve size就是指线程的栈保留大小。
而Stack commit size是指栈提交大小。
现在请先记住这2个名词,后边我会讲到它们。
栈保留大小(Stack Reserve Size)与栈提交大小(Stack Commit Size)
但是,系统并不是立即在物理内存中为它分配1M的栈内存,而是在虚拟地址空间保留了1M的栈内存,也就是栈保留大小stack reserve size。
被保留的栈空间只会用于这条线程,其它线程不能使用。
然后按实际需要将虚拟内存提交到物理内存,也就是栈提交大小Stack commit size,在WINDOWS下,默认的栈提交大小是4K。
每次向物理内存提交4K大小,不够再提向物理内存申请新的。
为什么采用按需提交物理内存的设计?
为什么要这么设计呢?
其实系统虽然为线程分配了1M的栈内存,但其实,大多数情况下函数的调用都用不了1M的栈内存。
如果直接为每个线程向物理内存提交1M的内存,那内存就不够用了。
以我的系统为例,当前系统正在运行的线程是6402条,以每个线程默认1M的栈内存来算就需要6g用在栈内存上。
而这些线程还需要创建堆内存,另外还有操作系统内核需要的内存,可想而知,如果不采用按需提交物理内存的设计,那么内存是完全不够用的。
统计系统线程数量的PowerShell示例
# Get all processes
$processes = Get-Process
# Initialize a counter for threads
$totalThreads = 0
# Iterate through each process and sum up the thread counts
foreach ($process in $processes) {
$totalThreads += $process.Threads.Count
}
# Output the total thread count
Write-Output "Total Threads: $totalThreads"
函数调用与栈帧
当一个函数被调用时,首先创建一个栈帧。
栈帧的内存分配并不会向操作系统申请新的内存,而是直接在线程创建时分配好的栈空间中进行操作。
你肯定知道,一个线程中会有很多的函数调用,甚至还有函数的递归调用。
而这些函数调用都共用线程的栈空间,并通过栈帧来维持每个函数的调用。
栈帧中保存在着函数调用的参数,返回地址,上一帧的基址指针,局部变量等等并通过ESP, EBP两个寄存器来维持栈的平衡以及对变量的读取。
关于栈帧的知识比较复杂,我会另外来专门做讲解,就不在这个视频中展开了。
栈溢出的原因
你可能要问,默认情况下系统只为线程分配了1M的栈空间,那单个线程中的函数调用都使用这个栈空间,不会不够吗?
会的,这也是为什么当你递归太深,分配的局部变量过大都会导致栈溢出的原因。
栈内存与数据结构中栈的关系
你说了这么多的栈内存知识,那这个栈和数据结构中的栈同一回事吗?它们有什么联系?
栈内存之所以叫栈,是因为它的分配和管理方式完全符合栈数据结构的 后进先出(LIFO) 特性。
之后我给你讲解栈帧的时候,里边会频繁使用PUSH, POP操作来管理栈。
而它与栈数据结构中的 push 和 pop 操作完全一致,到时候你就会更加明白了。
总结
好了,关于栈内存的来龙去脉,你现在有更深入的认识了吗?


