程序员应如何理解内存:中篇

https://mp.weixin.qq.com/s/x52e0aGl0fC1KhXhai6MPg 本节是操作系统系列教程的第三篇文章,属于操作系统第一章即基础篇,在真正开始操作系统相关章节前在这一部分回顾一些重要的主题,以下是目录,由于本文篇幅较多因此会按上篇、中篇、下篇三次发布,目录中黑体为本篇内容。


什么是内存

C/C++ 内存模型

堆区与栈区的本质

Java、Python 等内存模型

Java 内存模型

Jave 中的堆区与栈区是如何实现的

Python 内存模型

指针与引用

进程的内存模型

幻想大师 - 操作系统

总结


堆与栈的本质是什么

在编程语言中,堆区和栈区本质上都是内存,因此二者在本质上没有任何区别,只不过这两块内存的使用方式是不一样的。

在数据结构与算法中,我们也有堆和栈的概念,但那里指的不是内存,而是两种数据结构。

你可能会想,我们为什么要费尽心力的提出堆和栈这两个概念呢?之所以需要区分两种内存用法,根源在于:内存是有限的

如果计算机内存是无限的,那么我们根本就不用这么麻烦的给内存划分两个区域,在其中的一个区域中这样使用内存,另一区域那样使用内存,这些都是不需要的。即使在今天 PC 内存普遍都在 8G、16G,这依然是不够的,因此我们需要合理的来安排内存的使用,堆和栈就是为达到这一目的而采用的技术。

你会发现栈其实是一种非常巧妙的内存使用方法。函数调用完成后,函数运行过程中占用的内存就会被释放掉,这样,只要程序员代码写的合理 (栈帧不至于过大),那我们程序就可以一直运行下去,而不会出现内存不足的现象。程序员在栈区不需要担心内存分配释放问题,因为这一切都是自动进行的。而如果程序员想自己控制内存,那么可以选择在堆上进行内存分配。因此这里提供了两种选择,一种是 “自动的”,一种是 “手动的”,目的都是在合理使用内存的同时提供给程序员最大的灵活性。

堆和栈是计算机科学中很优秀的设计思想,这种设计思想充分的体现了计算机如何合理且灵活的使用有限资源。

堆区和栈区对 C/C++ 程序员来说就是实实在在的内存,而对于 Java、Python 等语言的程序员来说又该如何理解内存呢?

Java、Python 等内存模型

当 Java、Python 等语言的程序在执行时其解释器的内存布局同样如下图所示,我们之前讲过,解释器也是一个 C/C++ 程序,因此这里的代码段包含的是解释器的实现代码而不是 Java、Python 等代码,这一点大家一定要注意。

“C/C++ 程序员面对的是实实在在的物理内存,Java、Python 等程序面对的是解释器。”

C/C++ 分配内存是直接在物理内存中进行的,而 Java、Python 等程序是将内存分配请求交给解释器,解释器再去物理内存上进行分配。希望大家务必理解这一点。

Java、Python 等程序员是看不到如下图所示的内存布局的,因为这一切都是解释器才能看到的,解释器对 Java、Python 等程序员屏蔽了这些。Java、Python 等程序员也无需关心解释器的内存布局。

Java、Python 等程序的一大优点就是内存的自动化管理,而 C/C++ 程序员需要自己来管理从堆上分配的内存。内存管理这一项工作在 Java、Python 等程序中被解释器接管了,解释器的这项功能被称为 “垃圾回收器”。

在非 C/C++ 语言中,我们来看两个有代表性的语言,首先我们看一下 Java。

Java 内存模型

Java 的内存模型中同样有栈和堆这样的概念,如下图所示,在 Java 函数中我们定义的内置数据类型比如 int a = 0,是直接存放在栈上的,引用类型,也就是用 new 关键字定义的变量是分配在堆上的。和 C/C++ 一样,每个 Java 函数在执行时都有自己的栈帧。随着函数的调用,栈不断的扩大。当函数调用完毕后栈帧被回收,在堆上分配的变量依然可以被后续函数使用。Java 程序员无需像 C/C++ 程序员一样需要关心内存回收的问题,这一切都是 Java 的解释器 JVM 来管理的。

在用法上 Java 中的堆和栈和 C/C++ 是一样的,只不过 Java 程序员无需关心内存的释放问题。但是好奇的同学可能会问,C/C++ 中的堆和栈我已经清楚了,因为 C/C++ 程序运行时在内存中的样子已经在《C/C++ 内存模型》这一小节中详细的讲述了,那么 Java 中的堆和栈在内存中是什么样子的呢,就是和上图一样吗?要回答这个问题,就要涉及到 Java 中的堆和栈是如何实现的。

Java 中的堆和栈是如何实现的

如果你自己设计过一门语言的话,你应该会很清楚这个问题。

我们先回答上一节中提到的问题,那就是 Java 中的堆和栈就是如上图所示的那样吗?是这样的,作为 Java 程序员在写代码时脑海里有上面这张图基本上就够用了。但是,Java 中的堆和栈不同于 C/C++ 当中的堆和栈。

我们已经知道 Java 中的内存管理其实是解释器 JVM 来搞定的,作为 C/C++ 程序,JVM 的内存布局就如下图所示。

一般情况下,当 JVM 运行一个 Java 函数时需要在堆上创建出 Java 函数的栈帧,然后把这些栈帧放入栈中 (这里的栈指的是具有先进后出性质的数据结构)。希望大家不要被这句话绕晕,这里出现了两个 “栈”,但是含义完全不同。

  • Java 栈帧:指的是上图中我们看到的栈。
  • 栈帧放入到栈:我们在数据结构课程中都学过栈,栈有 push 和 pop 两种操作,把栈帧放入栈指的是把栈帧 push 到 JVM 所持有的栈这种数据结构当中,以此来模拟 C/C++ 程序执行过程中函数栈帧先进后出的这种性质,当一个 Java 函数被执行完毕后,JVM pop 掉该函数的栈帧。 如果你想在代码级别来理解这个过程,大体上可以参考下面的代码,注意 JVM 是 C/C++ 程序,这里的代码是一个极其简单的描述。你可以看到如何组织栈帧完全是 JVM 设计者来决定的,只要栈帧具备先进后出的性质就可以。
void RunJavaFunction(JVM* jvm string javaFunction) {


    stackFrame* frame = (stackFrame*) malloc(sizeof(stackFrame));


    jvm->stack->push(frame);


    run(javaFunction, frame);


    jvm->stack->pop();
}

JVM 会在自己的堆中为用 new 修饰的对象创建内存,这里的堆就是如上图所示的堆,是可以要记住 JVM 是一个 C/C++ 程序,JVM 看到的堆才是如上图所示的那样。所以你会发现,一般情况下,Java 中的栈和 Java 对象都是 JVM 在自己的堆上分配出来的,这就是 Java 中堆和栈是如何实现的。

在讲解完 Java 的内存模型后,我们来看一下 Python 的。

Python 内存模型

Python 的内存模型和 Java 其实是类似的,Java 程序员脑海中的那张图同样适用于 Python 程序员。

Python 语言中的解释器比较多,比如 CPython,PyPy 等,在这里我们以 Python 默认的解释器 CPython 为例来说明,我们已经知道了解释器其实也是一个 C 程序,CPython 也不例外,下图左侧就是我们已经熟悉的 C/C++ 内存布局,我们把堆区放大,如下图右侧所示。我们可以看到 Python 的解释器把自己的堆区划分成了两部分,分别是 Object-specific memory 区域,以及 Python core 区域:

Object-specific memory 这个区域专门用来存放 PyObject。你也许已经知道了,Python 中所有的数据类型比如 int,dict,str 等都是一个对象,叫做 PyObject。当我们在 Python 中创建一个变量比如 dict 时,CPython 就会在堆区的上半部分 (Object-specific memory) 中分配一块内存,创建一个 PyObject,这个 PyObject 用来存放我们的 dict。

Python core:所有非 PyObject 的内存请求都在这里分配的。

所以你会发现,Python 中所有的内存同样是解释器在自己的堆上分配的。

本文最后一部分将在《程序员应如何理解内存:下篇》中继续。


操作系统系列 https://mp.weixin.qq.com/s/x52e0aGl0fC1KhXhai6MPg

最后修改 April 15, 2023: upgrade (dbd415b4)