
编程时,我们需要分配内存,然后释放它。如果你用 C 语言编程,你会习惯使用 malloc/free 函数。但遗憾的是,这会让你容易受到内存泄漏(即无法回收的内存)的影响。如今,大多数流行的编程语言都使用自动内存管理机制,例如 Java、JavaScript、Python、C#、Go、Swift 等等。
自动内存管理主要有两种类型。最简单的方法是引用计数。它跟踪每个对象的引用数。当一个对象不再被任何对象引用时,就可以释放与其关联的内存。Swift 和 Python 都使用引用计数。引用计数的缺点是循环引用。例如,主程序可能引用了对象 A,然后添加了对象 B,对象 B 又引用了对象 A,并且对象 A 也引用了对象 B。这样,对象 B 就有一个引用,而对象 A 有两个引用。如果主程序释放了对对象 A 的引用,对象 A 和对象 B 的引用计数仍然为 1。然而,它们本应被释放。为了解决这个问题,可以遍历所有对象来检测哪些对象不可达,包括对象 A 和对象 B。但是,这样做很耗时。因此,另一种流行的自动内存管理方法是分代垃圾回收。它利用了大多数内存会在分配后很快被释放的特性。因此,需要跟踪年轻的对象并定期访问它们。然后,在极少数情况下,进行一次全内存扫描。分代垃圾回收的缺点在于,典型的实现方式会暂停整个程序进行内存扫描。在很多情况下,整个程序都会停止运行。经过数十年的研究,分代垃圾回收的实现方式有很多种。
常见的 Python 实现同时包含引用计数和分代垃圾回收两种机制。分代垃圾回收机制可能会触发服务暂停。很多服务器都是用 Python 编写的,这意味着你的服务可能会暂时不可用。我们通常把这种暂停称为“世界停止”暂停。那么,这种暂停会持续多久呢?
为了验证这一点,我编写了一个 Python 函数来创建一个经典的链表:
class Node : def __init__ ( self , value ): self . value = value self . next = None def add_next ( self , node ): self . next = node def create_linked_list ( limit ): """ create a linked list of length 'limit' """ head = Node ( 0 ) current = head for i in range ( 1 , limit ): new_node = Node ( i ) current . add_next ( new_node ) current = new_node return head然后我创建一个大的链表,然后,在一个紧密的循环中,我们创建一些小的链表,这些小的链表会立即被丢弃。class Node : def __init__ ( self , value ): self . value = value self . next = None def add_next ( self , node ): self . next = node def create_linked_list ( limit ): """ create a linked list of length 'limit' """ head = Node ( 0 ) current = head for i in range ( 1 , limit ): new_node = Node ( i ) current . add_next ( new_node ) current = new_node return head
x = create_linked_list ( 50_000_000 ) for i in range ( 1000000 ): create_linked_list ( 1000 )我的代码的一个关键特点是使用了包含 5000 万个元素的链表。该链表直到程序结束才会释放,但垃圾回收器仍有可能对其进行检查。x = create_linked_list ( 50_000_000 ) for i in range ( 1000000 ): create_linked_list ( 1000 )
我记录循环中两次迭代之间的最大延迟(使用time.time() )。
情况会变得多糟糕?答案取决于 Python 版本,而且每次运行结果都不一样。所以我只运行了一次,并选取了当时的结果。我以毫秒为单位表示延迟时间。
| Python 版本 | 系统 | 最大延迟 |
|---|---|---|
| 3.14 | macOS(Apple M4) | 320毫秒 |
| 3.12 | Linux(Intel Ice Lake) | 2200毫秒 |
几乎所有的延迟(例如 320 毫秒)都是由垃圾回收造成的。创建一个包含 1000 个元素的链表只需要不到一毫秒的时间。
320毫秒有多长?它是三分之一秒,所以人眼完全可以察觉到。作为参考,一款每秒刷新60次的视频游戏,每次刷新屏幕所需的时间不到17毫秒。而2200毫秒的延迟,从用户的角度来看,可能看起来像是服务器崩溃,并且很可能会触发超时(请求失败)。
我把 Python 程序移植到了 Go 语言。算法是一样的,但直接比较可能不太公平。不过,它仍然可以作为参考。
| go 版本 | 系统 | 最大延迟 |
|---|---|---|
| 1.25 | macOS(Apple M4) | 50毫秒 |
| 1.25 | Linux(Intel Ice Lake) | 33毫秒 |
因此,Go 的暂停时间比 Python 短好几倍,不会出现灾难性的 2 秒暂停。
这些停顿需要担心吗?大多数 Python 程序不会同时在内存中创建这么多对象。因此,如果您使用的是简单的 Web 应用或脚本,则不太可能遇到这些长时间的停顿。Python 提供了一些选项,例如gc.set_threshold和gc.freeze ,可以帮助您调整程序的行为。
原文: https://lemire.me/blog/2026/02/15/how-bad-can-python-stop-the-world-pauses-get/