Python 垃圾收集

在这篇文章中,我们将详细介绍 Python 中的垃圾收集。首先,我们将回顾有关内存管理的基础知识以及我们为什么需要垃圾回收。然后我们将看看 Python 是如何实现垃圾收集的。最后,我们将实际看看在编写 Python 应用程序时应该如何考虑垃圾收集。

什么是 Python 垃圾收集以及我们为什么需要它?

如果 Python 是您的第一门编程语言,那么垃圾收集的整个概念对您来说可能是陌生的。让我们从基础开始。

内存管理

编程语言在其程序中使用对象来执行操作。对象包括简单的变量,如字符串、整数或布尔值。它们还包括更复杂的数据结构,如列表、散列或类。

程序对象的值存储在内存中以便快速访问。在许多编程语言中,程序代码中的变量只是指向内存中对象地址的指针。当程序中使用变量时,进程将从内存中读取该值并对其进行操作。

在早期的编程语言中,大多数开发人员负责他们程序中的所有内存管理。这意味着在创建列表或对象之前,您首先需要为变量分配内存。处理完变量后,您需要释放它以“释放”其他用户的内存。

这导致了两个问题:

  1. 忘记释放你的记忆。如果在使用完内存后不释放内存,可能会导致内存泄漏。随着时间的推移,这可能会导致您的程序使用过多的内存。对于长时间运行的应用程序,这可能会导致严重的问题。
  2. 释放你的记忆太快了。第二种问题包括在内存仍在使用时释放内存。如果它试图访问内存中不存在的值,这可能会导致您的程序崩溃,或者它可能会损坏您的数据。引用已释放内存的变量称为悬空指针。

这些问题是不可取的,因此 较新的语言 添加了自动内存管理。

自动内存管理和垃圾收集

 

通过自动内存管理,程序员不再需要自己管理内存。相反,运行时为他们处理了这个问题。

自动内存管理有几种不同的方法。流行的使用引用计数。通过引用计数,运行时会跟踪对对象的所有引用。当一个对象对它有零个引用时,它就不能被程序代码使用并且可以被删除。

对于程序员来说,自动内存管理增加了许多好处。在不考虑低级内存细节的情况下开发程序会更快。此外,它可以帮助避免代价高昂的内存泄漏或危险的悬空指针。

然而,自动内存管理是有代价的。您的程序将需要使用额外的内存和计算来跟踪其所有引用。此外,许多具有自动内存管理功能的编程语言使用“stop-the-world”进程进行垃圾收集,在垃圾收集器查找和删除要收集的对象时,所有执行都会停止。

随着摩尔定律在计算机处理方面的进步  以及较新计算机中更大容量的 RAM,自动内存管理的好处通常超过了缺点。因此,大多数现代编程语言(如 Java、Python 和 Golang)都使用自动内存管理。

对于性能 至关重要的长时间运行的应用程序 ,某些语言仍然需要手动内存管理。这方面的经典示例是 C++。我们还在Objective-C 中看到了手动内存管理 ,这是用于 macOS 和 iOS 的语言。对于较新的语言,  Rust 使用手动内存管理。

现在我们大致了解了内存管理和垃圾收集,让我们更具体地了解 Python 垃圾收集的工作原理。

 

Python 是如何实现垃圾回收的

在本节中,我们将介绍垃圾收集在 Python 中的工作原理。

本节假设您使用的是 Python的 CPython 实现。CPython 是使用最广泛的实现。但是,还有其他 Python 实现,例如 PyPy、  Jython  (基于 Java)或 IronPython  (基于 C#)。

要查看您使用的是哪种 Python,请在终端 (Linux) 中运行以下命令:

import platform

print(platform.python_implementation())

CPython 中的内存管理和垃圾收集有两个方面:

  • 引用计数
  • 分代垃圾收集

让我们在下面逐一探讨。 

 

CPython 中的引用计数

CPython 中的主要垃圾收集机制是通过 引用计数。每当您在 Python 中创建对象时,底层 C 对象都具有 Python 类型(例如列表、字典或函数)和引用计数。

在非常基础的层面上,Python 对象的引用计数在对象被引用时递增,在对象被取消引用时递减。如果对象的引用计数为 0,则释放该对象的内存。

您的程序代码无法禁用 Python 的引用计数。这与下面讨论的分代垃圾收集器形成对比。

有些人声称 引用计数是穷人的垃圾收集器。它确实有一些缺点,包括无法检测下面讨论的循环引用。但是,引用计数很好,因为 当对象没有引用时,您可以立即删除它

在 Python 中查看引用计数

您可以使用 Python 标准库中的 sys模块来检查特定对象的引用计数。有几种方法可以增加对象的引用计数,例如 

  • 将对象分配给变量。
  • 将对象添加到数据结构,例如附加到列表或添加为类实例的属性。
  • 将对象作为参数传递给函数。

让我们使用 Python REPL 和 sys 模块来看看如何处理引用计数。

首先,在您的终端中,键入 python 以进入 Python REPL。

其次,将 sys 模块导入您的 REPL。然后,创建一个变量并检查其引用计数:

import sys

a = 'my-string'
print(sys.getrefcount(a))

请注意,我们的变量a有两个引用 。一是从创建变量。第二个是当我们将变量a传递  给 sys.getrefcount() 函数时。

如果将变量添加到数据结构(例如列表或字典)中,您将看到引用计数增加:

import sys

a = 'my-string'
b = [a]  # Make a list with a as an element.
c = {'key': a}  # Create a dictionary with a as one of the values.
print(sys.getrefcount(a))

如上所示,将a的引用计数  添加到列表或字典时会增加。

在下一节中,我们将了解分代垃圾收集器,这是 Python 用于内存管理的第二个工具。

分代垃圾收集

除了用于内存管理的引用计数策略外,Python 还使用了一种称为分代垃圾收集器的方法。

理解为什么我们需要分代垃圾收集器的最简单方法是通过示例。

在上一节中,我们看到将对象添加到数组或对象会增加其引用计数。但是如果你向自身添加一个对象会发生什么?

class MyClass(object):
    pass


a = MyClass()
a.obj = a
del a

在上面的例子中,我们定义了一个新类。然后我们创建了该类的一个实例,并将该实例指定为它自身的一个属性。最后,我们删除了实例。

通过删除实例,它在我们的 Python 程序中不再可用。但是,Python 并没有从内存中销毁该实例。该实例的引用计数不为零,因为它具有对自身的引用。

我们称这种类型的问题为引用循环,你不能通过引用计数来解决它。这是分代垃圾收集器的重点, 标准库中的gc 模块可以访问它 。

分代垃圾收集器术语

对于分代垃圾收集器,有两个关键概念需要理解。

  1. 第一个概念是一代。
  2. 第二个关键概念是阈值。 

垃圾收集器会跟踪内存中的所有对象。新对象在第一代垃圾收集器中开始其生命周期。如果 Python 在一代上执行垃圾收集过程并且一个对象存活下来,它就会向上移动到第二个更老的一代。Python 垃圾收集器总共有三代,只要对象在当前一代的垃圾收集过程中幸存下来,就会进入老一代。

对于每一代,垃圾收集器模块都有一个阈值数量的对象。如果对象数量超过该阈值,垃圾收集器将触发收集过程。对于在该过程中幸存下来的任何对象,它们都被移到了老年代。

与引用计数机制不同,您可以在 Python 程序中更改分代垃圾收集器的行为。这包括更改触发代码中垃圾收集过程的阈值。此外,您可以手动触发垃圾收集过程,或完全禁用垃圾收集过程。

让我们看看如何使用 gc 模块检查垃圾收集统计信息或更改垃圾收集器的行为。

使用 GC 模块

在您的终端中,输入 python 以进入 Python REPL。

将 gc 模块导入您的会话。然后,您可以使用get_threshold() 方法检查垃圾收集器的配置阈值 :

import gc

print(gc.get_threshold())

默认情况下,Python 对最年轻一代的阈值为 700,对两个老一代的阈值分别为 10。

您可以使用get_count() 方法检查每一代中的对象数量 :

import gc

print(gc.get_count())

在这个例子中,我们最年轻的一代有 596 个对象,下一代有两个对象,最老一代有一个对象。

如您所见,Python 在您开始执行程序之前默认创建了许多对象。您可以使用gc.collect() 方法触发手动垃圾收集过程 :

import gc

print(gc.get_count())
gc.collect()
print(gc.get_count())

运行垃圾收集进程会清理大量对象——第一代有 577 个对象,老一代有 3 个。

您可以使用 gc 模块中的set_threshold()方法更改触发垃圾收集的阈值 :

import gc

print(gc.get_threshold())
gc.set_threshold(1000, 15, 15)
print(gc.get_threshold())

在上面的例子中,我们增加了每个阈值的默认值。增加阈值将降低垃圾收集器运行的频率。这将在您的程序中减少计算成本,但代价是将死对象保留更长时间。

现在您知道引用计数和垃圾收集器模块的工作原理,让我们讨论在编写 Python 应用程序时应该如何使用它。

 

作为开发人员,Python 的垃圾收集器对您意味着什么

我们花了很多时间讨论内存管理及其在 Python 中的实现。现在是时候让它有用了。作为 Python 程序的开发人员,您应该如何使用这些信息?

一般规则:不要改变垃圾收集器的行为

作为一般规则,您可能不应该过多地考虑 Python 的垃圾收集。Python 的主要优势之一是它可以提高开发人员的生产力。部分原因是因为它是一种高级语言,可以为开发人员处理许多低级细节。

手动内存管理与受限环境更相关。如果您发现自己的性能限制可能与 Python 的垃圾收集机制有关,那么增加执行环境的能力可能比手动更改垃圾收集过程更有用。在摩尔定律、云计算和廉价内存的世界中,更容易获得更多功率。

鉴于 Python 通常不会将内存释放回底层操作系统,这甚至是现实的。您为释放内存而执行的任何手动垃圾回收过程可能不会给您想要的结果。有关该领域的更多详细信息,请参阅有关Python 内存管理的这篇文章 。

禁用垃圾收集器

除了这个警告之外,在某些情况下您可能想要管理垃圾收集过程。请记住,引用计数是 Python 中的主要垃圾收集机制,不能被禁用。您可以更改的唯一垃圾收集行为是 gc 模块中的分代垃圾收集器。

改变分代垃圾收集器的一个更有趣的例子来自 Instagram 完全禁用垃圾收集器

Instagram在其 Web 应用程序中使用 了流行的 Python Web 框架Django。它在单个计算实例上运行其 Web 应用程序的多个实例。这些实例使用主子机制运行,其中子进程与主进程共享内存。

Instagram 开发团队注意到,在产生一个子进程后,共享内存很快就会急剧下降。当进一步挖掘时,他们发现垃圾收集器是罪魁祸首。

Instagram 团队通过将所有代的阈值设置为零来禁用垃圾收集器模块。这一变化使他们的 Web 应用程序的运行效率提高了 10%。

虽然这个例子很有趣,但在遵循相同的路径之前,请确保您处于类似的情况。Instagram 是一个为数百万用户提供服务的网络级应用程序。对他们来说,使用一些非标准行为来从他们的 Web 应用程序中榨取每一寸性能是值得的。对于大多数开发人员来说,Python 围绕垃圾收集的标准行为就足够了。

如果您认为您可能想要在 Python 中手动管理垃圾收集,请确保您首先了解问题所在。一旦你完全理解了问题,然后采取措施来解决它。

 

 

概要

在这篇文章中,我们了解了 Python 垃圾收集。我们首先介绍了内存管理的基础知识和自动内存管理的创建。然后,我们通过自动引用计数和分代垃圾收集器查看了在 Python 中如何实现垃圾收集。最后,我们回顾了这对您作为 Python 开发人员的重要性。

虽然 Python 为您处理了内存管理的大部分困难部分,但了解幕后发生的事情仍然很有帮助。通过阅读这篇文章,您现在知道应该避免 Python 中的引用循环,并且如果您需要更好地控制 Python 垃圾收集器,您应该知道去哪里寻找。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值