堆栈和堆:我的代码位于内存中的哪里?

📖 7min read

贫穷盛行,遗忘记忆

我在学校的操作系统或系统编程课上学习了“内存结构”。我还记得堆栈和堆等术语被用作考试问题。

但说实话,直到毕业之前我很少认真关注记忆。如今,个人笔记本电脑的 RAM 基本上都是 16GB 或 32GB。在做本科水平的作业时,几乎不存在内存不足的情况。而且,在像Java这样的语言中,垃圾收集器(GC)会自动清理空间,所以我不必担心内存地址。

于是,在硬件的丰富性和语言的便利性的陶醉下,我忘记了“记忆感”这一开发者的基本技能,一头扎进了实际工作中。

从实践中吸取的惨痛教训

在学校练习编码测试时,我所能做的就是将一些数字放入一个数组中并旋转它们。输入数据最多不超过10万条。但实践却不同。无论初创公司多么小,数据库中都积累了数以千万计的实际客户数据。

在实践中,一种称为“ORM(对象关系映射)”的技术被用来方便地处理如此大量的数据。它是一个非常有用的工具,允许您像 Java 对象一样处理数据,而无需自己编写 SQL 查询。 (Hibernate是Java阵营中的一个代表性例子。)

问题是这个工具“太方便了”,只要一按按钮,DB中的所有数据就被带入列表中,所以我无法估计其背后隐藏的数据的权重。只需编写一行代码“获取客户信息”,数以万计的人的信息及其附带的订单详细信息就全部加载到内存中。

结果是灾难性的。 32GB内存瞬间就满了。由于 GC(清理器)试图保护内存,服务器遭受“极度滞后”,并最终停止。

熟悉的栈,陌生的堆

当我看着服务器喷出的错误日志时,我的目光被一个字吸引了。

java.lang.OutOfMemoryError:Java 堆空间

其实,“栈”这个词很熟悉。我在数据结构课上学到了它,这也是一个开发人员每天访问一次的网站(Stack Overflow)的名称。众所周知,如果递归函数写得不正确,堆栈就会爆裂。

然而,在实践中,每当服务器死掉时,遇到的罪魁祸首并不是堆栈。错误日志始终指向“堆”。

“不是栈满了,而是堆空间不够了?”

那一刻,我的脑海中闪过一个问题。我明白了栈,但是什么是堆呢?为什么在实践中经常发生爆炸呢?和数据结构中的堆一样吗?为什么我的代码会影响堆而不是堆栈?

这个问题让我回到了尘封已久的主要书籍和谷歌搜索的世界。然后我发现了。事实上,我的代码所在的“内存(RAM)”实际上并不是一个单独的房间,而是一个根据用途彻底划分和操作的“独立空间”。

内存不是一个单独的空间。它分为快速且窄的“栈”和慢速且宽的“堆”。

数字配送中心的工作台和仓库

让我们回到“数字物流中心”的世界观。这里,“RAM(内存)”是工作人员(CPU)分散项目进行工作的空间。然而,为了提高效率,该空间在两个独立的区域中运行。

1. Stack:工人的个人工作台

2.堆:公共仓库

当函数结束时“堆栈”就会消失,而“堆”只有在清理器清理干净时才会消失。

【代码验证】通过错误证明的两个世界

内存真的这么划分吗?通过故意在代码中造成错误,可以清楚地证明这两个空格的存在。

1.堆栈溢出错误

堆栈称为“工作台”。工作台很窄。如果函数没有完成并继续调用自身(递归),文档就会在工作台上堆积到天花板并最终崩溃。

public class StackTest {
    public static void recursiveCall(int depth) {
        // 無限遞迴呼叫:函式不會結束,持續堆積在 Stack 上
        System.out.println("Stack Depth: " + depth);
        recursiveCall(depth + 1);
    }

    public static void main(String[] args) {
        recursiveCall(1);
    }
}

结果:经过几千轮后,它会抛出StackOverflowError。无论堆空间有多空,如果堆栈(工作台)已满,程序就会死掉。

2.堆爆炸(OutOfMemoryError)

这一次,让我们重现我的噩梦吧。类似于由于错误使用ORM而导致一次加载数万个对象的情况,我们将继续创建巨大的列表并将它们塞入堆中。

import java.util.ArrayList;
import java.util.List;

public class HeapTest {
    public static void main(String[] args) {
        List<byte[]> warehouse = new ArrayList<>();
        
        while (true) {
            // 持續產生 1MB 的資料堆積在倉庫(Heap)中
            // 實務範例:從 DB 一次查詢數萬筆資料且未分頁時會發生
            warehouse.add(new byte[1024 * 1024]);
        }
    }
}

结果: java.lang.OutOfMemoryError:Java 堆空间。这不是堆栈问题。出现此错误的原因是仓库中没有更多空间来装载物品。这一刻,我亲眼目睹了我编写的代码如何困扰堆。

垃圾收集器 (GC)

这里有一个重要的区别。当函数结束时,堆栈会“自动”清空。无需担心。然而,如果有人不清理堆,垃圾就会继续堆积。

在C语言等旧语言中,开发人员必须直接使用命令free()来清理它们。如果你忘记了,仓库就会充满垃圾并爆炸(内存泄漏)。另一方面,Java、Python 和 JavaScript (JS) 等现代语言使用称为“GC(垃圾收集器)”的专业清理程序。

“没有人再使用这个对象了?” GC 定期遍历堆,找到无主对象并丢弃它们。因此,我们不必编写内存释放代码。

但是没有什么是免费的。当 GC 进行深度清洁时,物流中心的所有工作都会暂时停止(Stop-the-world)。这个清理时间就是游戏突然滞后或服务器冻结大约一秒钟的原因。

结论:看到无形的眼睛

了解堆栈和堆后,我的显示器上的代码开始看起来不同。过去,当我查看代码 new Student() 时,我只是想,“我创建了一个对象”。但现在我可以看到它了。

“现在有一个盒子进入了称为Heap的仓库。如果我不删除它(或者GC不来),它会继续吃掉内存。”

在获得“看到无形之物的眼睛”之后,我能够将自己从模糊的恐惧中解放出来。即使服务器吐出OutOfMemory,我也不会像以前那样惊慌并按下重新启动按钮。相反,打开分析工具,冷静地问:“哪个对象正在占用堆?”这是因为,如果您知道原因,就可以解决它。

我们现在已经征服了存储代码的空间(内存)。仓库已做好充分准备。那么,在这个仓库中实际运输和组装物品的“工人”是谁呢?单独工作和与多人同时工作(多任务)有什么区别?

下一次,我们一起走进操作系统之花、后端开发者永恒的功课“进程&线程”的世界。

發佈留言