查看原文
其他

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

码农的荒岛求生 码农的荒岛求生 2020-12-18


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




什么是内存

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) {
// 在堆上申请一块空间,用于存放java栈帧 stackFrame* frame = (stackFrame*) malloc(sizeof(stackFrame)); // 把要使用的栈帧push到JVM的函数调用栈中 jvm->stack->push(frame); // 在申请的栈帧上执行Java函数 run(javaFunction, frame); // 执行完毕后pop掉该函数栈帧 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中所有的内存同样是解释器在自己的堆上分配的。


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




操作系统系列

基础篇
1,什么是程序

2,程序?进程?傻傻分不清

3,程序员应如何理解内存:上篇



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存