堆和栈分别存储什么?

29

谁能清晰地解释C、C++和Java中堆栈的使用,哪些内容存储在堆上,哪些内容存储在栈上,以及何时进行分配。

据我所知,每次函数调用的所有本地变量(包括基本数据类型、指针或引用变量)都保存在新的堆栈帧中。而使用new或malloc创建的任何内容都存储在堆上。

我对一些事情感到困惑。

如果对象成员是引用/基本数据类型,它们是否也存储在堆上?那些在每个帧中递归创建的方法的本地成员又如何呢?它们都在栈上,如果是的话,那么该栈内存是在运行时分配的吗?对于字面量,它们是否属于代码段?C语言中的全局变量,C++/Java中的静态变量以及C语言中的静态变量呢?


3
局部变量不一定在堆栈帧中 - 它们可能仅存在于寄存器中,或者甚至被完全优化掉。 - Paul R
2
理想情况下,这些都是实现细节。你正在比较哪些实现? - trashgod
标准实现是Sun/Oracle实现的JDK实现。 对于C/C++,我指的是gcc。 - Amogh Talpallikar
https://dev59.com/hHVD5IYBdhLWcg3wHn2d - Jeegar Patel
如果你只是在搜索框中输入了你的主题,你应该会看到很多现有的答案来回答这个问题。事实上,在你输入问题的同时,那些答案应该已经出现了。 - kdgregory
显示剩余2条评论
6个回答

46

程序在内存中的结构

下面是当程序被加载到内存中时的基本结构。

 +--------------------------+
 |                          |
 |      command line        |
 |        arguments         |
 |    (argc and argv[])     |
 |                          |
 +--------------------------+
 | Stack                    |
 | (grows-downwards)        |
 |                          |
 |                          |
 |                          |
 |         F R E E          |
 |        S P A C E         |
 |                          |
 |                          |
 |                          |
 |                          |
 |     (grows upwards) Heap |
 +--------------------------+
 |                          |
 |    Initialized data      |
 |         segment          |
 |                          |
 +--------------------------+
 |                          |
 |     Initialized to       |
 |        Zero (BSS)        |
 |                          |
 +--------------------------+
 |                          |
 |      Program Code        |
 |                          |
 +--------------------------+

需要注意以下几点:

  • 数据段
    • 已初始化数据段(由程序员显式初始化)
    • 未初始化数据段(初始化为零数据段——BSS [带符号块开始])
  • 代码段
  • 堆栈区域和堆区域

数据段

数据段包含全局和静态数据,这些数据由用户明确初始化并包含有初始化值。

数据段的另一部分称为BSS(��为旧的IBM系统将该段初始化为零)。这是操作系统初始化内存块为零的内存部分。这就是未初始化的全局数据和静态变量的默认值为零的原理。此区域是固定的,大小静态。

数据区根据显式初始化分为两个区域,因为要初始化的变量可以逐个初始化。但是,不需要显式初始化未初始化的变量。相反,将变量的初始化工作留给操作系统。这种批量初始化可以大大缩短加载可执行文件所需的时间。

大多数情况下,数据段的布局由底层操作系统控制,但某些加载器会给用户部分控制权。在嵌入式系统等应用中,这些信息可能很有用。

可以使用指针从代码中访问和访问此区域。自动变量在每次需要时都要初始化变量,因此代码需要进行初始化。但是,数据区域中的变量没有这种运行时负担,因为初始化仅在加载时完成一次。

代码段

程序代码是可执行代码可用于执行的代码区域。该区域的大小也是固定的。只能通过函数指针访问此区域,而不能通过其他数据指针访问此区域。还有一个重要信息需要注意,即系统可能将此区域视为只读内存区域,任何尝试写入此区域的操作都会导致未定义的行为。

常量字符串可以放置在代码区域或数据区域,具体取决于实现方式。

尝试写入代码区域会导致未定义的行为。例如(我只会给出基于C的示例),以下代码可能导致运行时错误甚至崩溃系统。

int main()
{
    static int i;
    strcpy((char *)main,"something");
    printf("%s",main);
    if(i++==0)
    main();
}

堆和栈区

在执行过程中,程序使用两个主要部分:栈和堆。栈用于函数的堆栈帧,堆用于动态内存分配。栈和堆是未初始化的区域。因此,无论内存中存在什么内容都将成为在该空间创建的对象的初始(垃圾)值。

让我们看一个样例程序,展示哪些变量存储在哪里:

int initToZero1;
static float initToZero2;
FILE * initToZero3; 
// all are stored in initialized to zero segment(BSS)

double intitialized1 = 20.0;
// stored in initialized data segment

int main()
{
    size_t (*fp)(const char *) = strlen;
    // fp is an auto variable that is allocated in stack
    // but it points to code area where code of strlen() is stored

    char *dynamic = (char *)malloc(100);
    // dynamic memory allocation, done in heap

    int stringLength;
    // this is an auto variable that is allocated in stack

    static int initToZero4; 
    // stored in BSS

    static int initialized2 = 10; 
    // stored in initialized data segment   

    strcpy(dynamic,”something”);    
    // function call, uses stack

    stringLength = fp(dynamic); 
    // again a function call 
}

或者考虑一个更为复杂的例子,

// command line arguments may be stored in a separate area  
int main(int numOfArgs, char *arguments[])
{ 
    static int i;   
    // stored in BSS 

    int (*fp)(int,char **) = main;  
    // points to code segment 

    static char *str[] = {"thisFileName","arg1", "arg2",0};
    // stored in initialized data segment

    while(*arguments)
        printf("\n %s",*arguments++);

    if(!i++)
        fp(3,str);
}
希望这可以帮到你!

当我在程序空间上映射一些东西时,它被映射到哪里了?+1 对于这样好的解释。 - Jeegar Patel
1
@Mr.32 谢谢您的点赞。我不确定mmap()在上面的布局中属于哪个部分。这是一个好问题。我正在尝试研究同样的问题!另一方面,我试图让所有上述编程语言的描述通用化。我应该也涵盖了registerextern变量! - Sangeeth Saravanaraj

8
在 C/C++ 中: 局部变量在当前堆栈帧上分配(属于当前函数)。如果您静态分配一个对象,则整个对象都分配在堆栈上,包括其所有成员变量。在使用递归时,每次调用函数都会创建一个新的堆栈帧,并在堆栈上分配所有局部变量。堆栈通常具有固定大小,这个值通常在编译/链接期间在可执行二进制文件头中写入。但是这非常依赖于操作系统和平台,一些操作系统可能会在需要时动态扩展堆栈。由于堆栈的大小通常受限制,当您使用深度递归或有时甚至在没有递归时静态分配大型对象时,可能会耗尽堆栈。
堆通常被认为是无限空间(仅受物理/虚拟内存可用空间限制),您可以使用 malloc/new(和其他堆分配函数)在堆上分配对象。创建堆上的对象时,它的所有成员变量都在其中创建。您应该将对象视为连续的内存区域(此区域包含成员变量和指向虚拟方法表的指针),不管它在哪里分配。
文字,常量和其他“固定”东西通常编译/链接到二进制文件中作为另一个段,因此它实际上不是代码段。通常您无法在运行时从此段中分配或释放任何内容。但是这也是平台特定的,它可能在不同的平台上有不同的工作方式(例如 iOS Obj-C 代码有许多常量引用直接插入到函数之间的代码段中)。

3
在C和C++中,至少这全部都是实现特定的。标准没有提到“栈”或“堆”。

2
在Java中,局部变量可以分配在堆栈上(除非被优化掉)。
对象中的基本类型和引用都在堆上(因为对象在堆上)。
线程创建时会预先分配一个堆栈。它不使用堆空间。(但是创建线程会导致创建一个线程本地分配缓冲区,这会显著减少可用内存)
唯一的字符串字面量被添加到堆上。原始字面量可能在代码中某个位置(如果没有被优化掉)。字段是静态的还是非静态的没有任何区别。

如果本地变量是对象,则只有指针存储在堆栈上。Java中的堆通常更有组织。一种JVM实现维护一个“年轻”代(即短寿命对象),有点类似于堆栈概念,但需要管理。相反,C++有时必须克隆堆栈对象。 - Joop Eggen
1
@JoopEggen 自Java 6 Update 14以来,Oracle(Sun)JVM也能够进行逃逸分析并防止对象在堆上创建。请参见http://www.oracle.com/technetwork/java/javase/6u14-137039.html。 - Roger Lindsjö
逃逸分析有时候很有效,但在许多你认为它应该有效的情况下,它并不起作用。 ;) - Peter Lawrey

2

关于C++堆和栈的部分问题:

首先要说的是,如果没有使用new创建的对象会作为一个连续的单元存储在栈上,如果是全局变量,则存储在某个全局段(平台特定)。

对于使用new在堆上创建的对象,其成员变量将作为一个连续的内存块存储在堆上。这适用于原始类型和嵌入式对象的成员变量。对于指针和引用类型的成员变量,原始指针值将存储在对象中。该值所指向的内容可以存储在任何地方(堆、栈、全局)。任何情况都有可能。

至于对象方法中的局部变量,它们存储在栈上,而不是存储在堆上对象的连续空间中。栈通常在运行时创建一个固定大小。每个线程都有一个栈。局部变量甚至可能不会占用栈上的空间,因为它们可能被优化掉了(正如Paul所说)。主要的问题是,它们不会因为是堆上对象的成员函数而存储在堆上。如果它们是指针类型的局部变量,则它们可以存储在栈上并指向堆或栈上的某些内容!


2
《Java虚拟机规范》的3.5节描述了运行时数据区域(堆栈和堆)。
C和C++语言标准都没有明确指定某个变量应该存储在堆栈还是堆中。它们只定义对象的生命周期、可见性和可修改性;将这些要求映射到特定平台的内存布局取决于实现方式。
通常使用*alloc函数分配的内存位于堆中,而auto变量和函数参数位于堆栈中。字符串字面量可能存在“其他地方”(必须在程序生命周期内进行分配和可见),但尝试修改它们是不明确定义的;有些平台使用一个单独的只读内存段来存储它们。
请记住,有一些真正奇怪的平台可能不符合常见的堆栈模型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接