数据结构的堆、栈和操作系统的堆内存、栈内存的区别

Posted by Kylen on 2019-06-17

前言

喜欢瞎琢磨的人应该都过这个疑问。数据存在堆中、栈中。。数据结构中也有堆栈这种数据结构,这两者却不是一个意思。栈内存和数据结构栈有关系,堆内存和数据结构堆却毫无瓜葛。一脸懵逼。

做为一名前端工程师,我估摸着产生这种疑惑最大的问题在于不是很理解内存空间的概念。我们平时所说的堆内存、栈内存其实我们并没有对它有一个明确的定义,然后随着对堆、栈这种数据结构有有了一定的认识之后一对比就发现问题了。当然有些人可能和我的顺序相反。总之,随着对系统和程序理解的加深,会明显感觉到这两者的差异。

数据结构堆和栈

学过数据结构和算法的同学都知道:数据结构是一组数据的存储结构,而算法是操作数据的一组方法

堆和栈的数据结构,是一种数据存储的方式,是一种设计。各种编程语言、数据库以及我们平日的coding都可以有它具体的实现。

栈的结构比较好理解,一种线性结构,限制:只允许一端出入。所以栈的特点是LIFO(Last In First Out)后进先出。比较容易的记忆方式:羽毛球盒子、手枪弹夹。如下图所示:
stack-heap-004.png

堆在数据结构中是一种特殊的完全二叉树,所以是一种树状结构。它的存取数据的方式类似于图书馆和书的关系,图书馆里的书按照一定规则分类,我们可以把图书馆想象成一颗庞大的树,树上的每个节点都是一本书。我们只要通过书名确定书的具体类别,就可以找到书的位置。

后面准备总结一下数据结构相关的知识点。这里只做简单介绍


操作系统的内存

物理内存和虚拟内存

我们都知道计算机存储的物理介质有硬盘和内存,这两者都是物理的,是计算机中的重要部件,拆开电脑能真真切切摸到的东西。计算机可以连接物理存储进行读写。

在早些年的操作系统中,程序直接访问和操作的都是物理内存。这种形式无法保证系统的安全和稳定,破坏性高;而且很难同时运行多个程序,类比一下JS的单线程,可想而知,同时使用一套存储,即使是最简单的同时运行多个程序这种情况也会变得难以实现。

虚拟内存是现代操作系统普遍使用的一种技术,基本思想是:通过对主存进行抽象,为每个进程提供独立的逻辑空间。

程序运行时会创建进程,进程有自己的内存,往往称为进程地址空间。进程的内存空间只是虚拟内存,而程序运行时需要实实在在的内存,即物理内存(RAM)。在必要的时候,操作系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程能够使用物理内存。

程序运行中的内存

为了了解系统运行程序的过程,可以了解下CPU的结构:

stack-heap-001.jpg

程序被加载到内存中,这个过程往往不是一次性全量加载,而是一段一段的。

程序运行时会创建进程,进程有自己的虚拟内存空间,而虚拟内存空间一般分为两部分,应用程序使用的内存空间(process virtual memory)和系统内核程序(kernel virtual memory)使用的内存空间。同时程序运行时可能会产生很多线程。

无论是进程还是线程,本质都是CPU的工作时间段的描述。CPU要运行,环境必须先准备好,内存要ready。所以进程和线程都需要内存空间。

堆(heap)和栈(stack)是两种内在的管理形式。它们的主要区别是stack按次序排放,大小明确;heap结构则不固定,是一种可动态分配和释放的内存。单从这一点看,stack的寻址速度要比heap快,heap的灵活性则比较高。一般来说,每个线程分配一个stack,每个进程分配一个heap。

程序运行中的内存也因此就有了堆内存和栈内存。基本存放规则是:局部的,占用空间确定的数据,一般都放在stack中,反之就放在heap中。

不同语言程序中的堆内存和栈内存

C、C++

以C++语言为例,我们有一个经典main.cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 0; /* 全局初始化区 */   
char *p1; /* 全局未初始化区 */
main()
{
int b; /* 栈 */
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; /* 123456在常量区,p3在栈上。 */
static int c =0/* 全局(静态)初始化区 */
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
/* 分配得来得10和20字节的区域就在堆区。 */
strcpy(p1, "123456"); /* 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 */
}

C/C++是高级语言,这种亲近人的高级语言计算机是读不懂的。需要编译成二进制语言。经过预处理器、编译器、汇编器、链接器,C/C++程序(main.cpp)变成了可执行的二进制程序存储在硬盘中,当程序被执行时会被加载到内存中,由系统运行。

C/C++这种是直接编译成二进制文件由系统直接运行,所以可以直接对应到操作系统的堆内存和栈内存。在C/C++语言中,堆和栈的申请方式是不一样的,例如int b; char *p2;系统会自动分配栈内存空间;例如使用malloc函数或是使用new操作符这些需要自己申请的一般都是分配堆内存。

可以看到C/C++程序在开发过程中堆栈的区分十分明确,因为直接对应操作系统的堆内存和栈内存。

Java

Java程序是运行在jvm上的,jvm本身就是个C/C++程序。jvm的堆栈和C/C++使用的本机堆栈相同。Java程序运行在jvm上,如何管理和分配内存都是jvm的事,开发java程序只需按照Java的规范。

Javascript

JS的堆栈?JS有堆栈?其实单纯写JS本身不需要关心这个问题。当然弄清楚更好。

因为JS脚本语言的特性,通常运行在浏览器中,所以执行引擎比较多,多数使用C/C++实现,也有使用java实现的。最流行的Chrome浏览器的V8就是使用C++实现的。

所以某种意义上Javascript和Java就有点类似了,它们都是运行在一个程序上面,且这个程序都是C/C++写的。

V8使用类似于JVM和大多数其他语言的堆。

数据结构堆栈和系统的堆栈内存

这样一看其实是没有关系,那为什么又说数据结构的栈和系统的栈内存有关系呢?主要是栈内存本身就是一种栈结构,符合栈的定义。据说早期Lisp就是使用数据结构堆来做内存池的,现在的堆内存的分配方式类似于链表,所以和数据结构堆是一点关系也没有了。

参考链接

内存空间详解
深入理解计算机系统(二):Hello World 是如何运行的
操作系统与内存管理
虚拟内存布局、内存的分工、堆与栈
Stack的三种含义
线程和进程的区别是什么?
Java虚拟机的堆、栈、堆栈如何去理解?
Javascript中堆栈到底是怎样划分的?