在计算机内存管理中,数据的存储位置取决于它们的生命周期、大小、访问方式和用途。

通常,内存分为以下几部分:栈(stack)堆(heap)静态存储区(data segment)代码区(text segment)。不同的数据存储在不同的区域,每种存储方式有其特定的优缺点。

1. 栈(Stack)

栈是计算机中一种按照“后进先出”(LIFO, Last In First Out)原则进行管理的内存区域。在栈上分配的内存通常存储的是局部变量和函数调用信息。

1.1 存储的内容

1、局部变量

每个函数调用时创建的局部变量(包括基本数据类型和指向其他数据结构的指针)。这些变量在函数调用时分配,函数返回时销毁。

function exampleFunction() {
  let localVar = 10; // 局部变量
  console.log('Local variable value:', localVar);
}

exampleFunction(); // 调用函数

2、函数的参数

当调用一个函数并传递参数时,这些参数会被存储在栈上。每次函数调用时,栈会为传递的参数分配内存,函数结束时会销毁这些参数。

function exampleFunction(x, y) {
  // 函数参数
  console.log('Sum:', x + y);
}

exampleFunction(5, 7); // 传递参数 5 和 7

当调用 exampleFunction(5, 7) 时,参数 x 和 y 的值(5 和 7)会被压入栈中。栈上为 x 和 y 分配内存,它们存储值 5 和 7。函数执行完成后,栈中的参数会被销毁。 

3、函数的返回地址

在 JavaScript 中,通常看不到“返回地址”的显式表示,但它是存在的。每次函数调用时,栈上会存储函数执行完后应该跳转回的代码位置,即函数的“返回地址”。

function exampleFunction() {
  console.log('In function!');
}
function main() {
  exampleFunction(); // 调用函数
  console.log('Back to main!');
}

main();

当  exampleFunction() 被调用时,栈会存储返回地址,即 main 函数中调用  exampleFunction() 后的下一条指令 console.log('Back to main!')。当 exampleFunction 执行完毕后,程序会根据栈中存储的返回地址,跳转 main 函数的后续代码继续执行。

1.2 优缺点

1、优点

  • 访问速度快:栈采用的是顺序分配内存,访问速度非常快。
  • 内存管理简单:栈的内存管理非常简单,由操作系统或编程语言运行时自动管理,无需程序员显式释放。
  • 内存分配和回收自动:函数调用结束时,栈上分配的内存会自动回收。

2、缺点

  • 空间有限:栈的空间是有限的,过多的递归调用或大量的局部变量可能导致栈溢出(Stack Overflow)。
  • 生命周期短:栈上分配的内存随着函数的调用和返回而变化,生命周期非常短,适合存储短期数据。
1.3 为什么存放在栈上?

栈上分配的数据生命周期较短,随着函数的调用和返回自动管理,存放在栈上不仅能提高访问速度,还能简化内存管理(无需手动分配和释放)。

2. 堆(Heap)

堆是用于动态分配内存的区域,主要用于存储动态创建的对象、数据结构和其他需要手动控制生命周期的数据。

2.1 存储的内容

1、动态分配的内存

当创建一个对象、数组或其他复杂的数据结构时,内存是动态分配的。这意味着它们的内存位置是在程序运行时动态决定的,而不是在编译时静态分配的。

let obj = { name: "Alice", age: 25 };  // 动态创建的对象
let arr = [1, 2, 3, 4];  // 动态创建的数组

这些对象和数组会被存储在堆内存中,因为它们的大小和生命周期是动态变化的。

2、动态创建的对象

通过 new 关键字创建的对象、数组、正则表达式等,都是在堆内存中存储的。堆内存为动态分配的对象提供足够空间,因为这些对象的大小和生命周期是动态的,不能提前知道。

let person = new Object();  // 动态创建对象
let date = new Date();  // 动态创建对象

这些对象在堆中存储,它们的生命周期由垃圾回收机制控制。也就是说,直到没有任何变量引用这些对象,垃圾回收器才会回收它们。 

2.2 优缺点

1、优点

  • 灵活性:堆允许动态地分配和释放内存,可以在运行时根据需要调整内存大小。
  • 内存空间大:堆的内存空间通常较大,不像栈那样受限于函数调用的深度,可以存储大量数据。

2、缺点

  • 分配和回收较慢:堆的内存分配通常比栈慢,因为需要进行内存管理(查找合适的空闲内存块,可能会导致内存碎片)。
  • 需要手动管理:堆上的内存需要程序员显式地管理(例如:free、delete),否则可能会导致内存泄漏或悬空指针。
  • 可能导致内存碎片:由于内存是动态分配的,长期运行的程序可能会导致堆内存碎片化,从而影响性能。
2.3 为什么存放在堆上?

堆用于存储生命周期不确定或动态大小的数据。允许程序在运行时动态分配内存,适合存储需要跨多个函数或更长时间存在的数据。

3. 静态存储区(Data Segment)

静态存储区是程序在运行时分配的一个区域,用于存储全局变量、静态变量、常量等数据。

3.1 存储的内容

1、全局变量

全局变量是程序中定义在函数外部的变量,它的生命周期从程序开始直到程序结束。

let globalVar = 10; // 全局变量

function exampleFunction() {
  console.log(globalVar); // 访问全局变量
}

exampleFunction(); // 输出 10
console.log(globalVar); // 输出 10

globalVar 是一个全局变量,它在程序开始时分配内存,直到程序结束才销毁。无论在哪个函数中都可以访问 globalVar,它的值在整个程序的生命周期内都存在。

2、静态变量

在 JavaScript 中,虽然没有 C/C++ 中的 static 关键字,但可以通过一些方法模拟静态变量的行为。静态变量的特点是,它们的值在多次函数调用之间保持不变。

模拟静态变量

function exampleFunction() {
  if (!exampleFunction.count) {
    exampleFunction.count = 0; // 初始化静态变量
  }
  exampleFunction.count++; // 递增静态变量
  console.log(exampleFunction.count);
}

exampleFunction(); // 输出 1
exampleFunction(); // 输出 2
exampleFunction(); // 输出 3

通过将 count 属性挂载到函数 exampleFunction 上模拟静态变量的行为。count 变量在多次调用 exampleFunction 之间保持其值,即它不会在每次函数调用时被重置。count 的生命周期与 exampleFunction 函数绑定,即在整个程序运行期间它都存在。

3、常量

常量是不可修改的值,在 JavaScript 中可以使用 const 关键字来声明常量。在整个程序的生命周期中都存在,并且它的值在程序执行过程中始终保持不变。

const PI = 3.14159; // 常量

function calculateArea(radius) {
  return PI * radius * radius; // 使用常量
}

console.log(calculateArea(5)); // 输出 78.53975
​​3.2 优缺点

1、优点

  • 生命周期长:静态存储区中的数据在程序运行的整个过程中都存在,生命周期长。
  • 内存访问快:静态存储区的内存访问速度通常较快,适合存储程序中长时间需要的全局或常量数据。

2、缺点

  • 内存占用固定:静态存储区的大小在编译时就已经确定,无法动态调整,因此可能会造成内存浪费。
  • 不适合动态数据:静态存储区中的数据不能像堆一样动态分配和调整。
3.3 为什么存放在静态存储区?

静态存储区用于存储程序运行时始终存在的数据,例如全局变量和常量,这些数据的生命周期与程序的生命周期相同,程序中需要访问这些数据时,不必重新分配内存,能提高效率。

4. 代码区(Text Segment)

代码区存储程序的机器指令,即编译后的代码。它是只读的,因此程序无法修改自己的指令。

4.1 存储的内容

1、程序代码

所有的函数、方法以及控制逻辑都存储在代码区。这个区域是只读的,确保程序中的代码不会被意外修改。程序执行时,这些函数和方法的机器指令会被加载到内存中。

function greet(name) {
  return `Hello, ${name}!`; // 这是存储在代码区的函数
}

console.log(greet('Alice')); // 输出:Hello, Alice!

greet 函数本身存储在代码区。在程序启动时,JavaScript 引擎会将 greet 函数的代码加载到内存中,并在调用时执行。

无论我们在程序中调用多少次 greet 函数,代码区中的指令始终保持不变,因为代码区是只读的,确保程序的代码不会被修改。

2、只读数据(常量字符串)

字符串文字(例如" Hello, World ")和常量字符串通常存储在代码区,因为它们是不可修改的,只在程序中读取。

const greetingMessage = 'Hello, World!'; // 常量字符串

function displayGreeting() {
  console.log(greetingMessage); // 输出存储在代码区的字符串
}

displayGreeting(); // 输出:Hello, World!

greetingMessage 是一个常量字符串,它的值 'Hello, World!' 存储在代码区,因为字符串是只读的,在程序执行过程中不可被修改。而 greetingMessage 作为常量在栈上进行访问。

3、字符串常量和字面量存储

JavaScript 字符串常量是不可变的,并且通常会被优化为存储在代码区,以减少内存的使用并提高性能。

let str1 = 'OpenAI';
let str2 = 'OpenAI';

console.log(str1 === str2); // 输出 true,表示这两个字符串指向相同的内存位置

str1 和 str2 被存储在代码区的只读区域。在许多 JavaScript 引擎中,这两个变量会指向同一块内存,因为它们的值相同且不可变。这体现了 JavaScript 引擎的优化机制,它会共享常量字符串,减少内存占用。

4.2 优缺点

1、优点

  • 访问效率高:代码区中的数据和指令是程序的一部分,通常被频繁访问,处理速度较快。
  • 只读保护:由于代码区是只读的,防止程序在运行时修改自己的代码,有助于避免潜在的安全问题。

2、缺点

  • 不可修改:代码区的数据不可修改,因此无法进行动态代码生成或修改。
4.3 为什么存放在代码区?

程序的指令是不可修改的,它们必须存储在一个只读区域,以保证程序的执行不被篡改并避免错误。

5. 数据存储位置的总结对比

存储区域 存储内容 生命周期 访问速度 管理方式 优点 缺点
栈(Stack) 局部变量、函数参数、返回地址 函数调用期间 自动分配和回收 快速、简单 空间有限、生命周期短、栈溢出
堆(Heap) 动态分配的内存、对象 程序运行期间 较慢 手动管理 灵活、大空间 慢、内存碎片、需要手动管理
静态存储区 全局变量、静态变量、常量 程序运行期间 编译时分配 生命周期长、快速访问 固定大小、不可调整
代码区 程序指令、只读数据 程序运行期间 程序自带 不可篡改 不能修改

每种存储方式有其适用场景,通过合理利用不同的内存区域,可以优化程序性能和内存使用效率。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐