也谈谈 Python 的内存管理

简介: 内存是计算机中重要的部件之一,它是外存与 CPU 进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。

什么是内存

买电脑的配置“4G + 500G / 1T”,这里的 4G 就是指电脑的内存容量,而电脑的硬盘 500G / 1T。


内存(Memory,全名指内部存储器),自然就会想到外存,他们都硬件设备。


内存是计算机中重要的部件之一,它是外存与 CPU 进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。

内存就像一本空白的书

首先,您可以将计算机的存储空间比作一本空白的短篇小说。页面上还没有任何内容。最终,会有不同的作者出现。每个作者都需要一些空间来写他们的故事。


由于不允许彼此书写,因此必须注意他们能书写的页面。开始书写之前,请先咨询书籍管理员。然后,管理员决定允许他们在书中写什么。


如果这书已经存在很长时间了,因此其中的许多故事都不再适用。当没有人阅读或引用故事时,它们将被删除以为新故事腾出空间。


本质上,计算机内存就像一本空书。实际上,调用固定长度的连续内存面块是很常见的,因此这种类比非常适用。


作者就像需要将数据存储在内存中的不同应用程序或进程。决定作者在书中书写位置的管理员就像是各种存储器管理的角色,删除旧故事为新故事腾出空间的人是垃圾收集者(garbage collector)。

内存管理:从硬件到软件

为什么 4G 内存的电脑可以高效的分析上 G 的数据,而且程序可以一直跑下去。


在这 4G 内存的背后,Python 都帮助我们做了什么?


内存管理是应用程序读取和写入数据的过程。内存管理器确定将应用程序数据放置在何处。


由于内存有限,类比书中的页面一样,管理员必须找到一些可用空间并将其提供给应用程序。提供内存的过程通常称为内存分配。


其实如果我们了解内存管理机制,以更快、更好的方式解决问题。


看完本篇文章,带您稍微了解 Python 内存管理的设计哲学。

Cpython 下 Python 对象的实现

可能我们听过,Python 鼎鼎有名的那句“一切皆对象”。是的,在 Python 中数字是对象,字符串是对象,任何事物都是对象,而它们的核心就是一个结构体--PyObject。

typedef struct_object{
  int ob_refcnt;
  struct_typeobject *ob_type;
}PyObject;


PyObject 是每个对象必有的内容,可以说是 Python 中所有对象的祖父,仅包含两件事:


  • ob_refcnt:引用计数(reference count)
  • ob_type:指向另一种类型的指针(pointer to another type)


所以,所以 CPython 是用 C 编写的,它解释了 Python 字节码。这与内存管理有什么关系?

好吧,C 中的 CPython 代码中存在内存管理算法和结构。要了解 Python 的内存管理,您必须对 CPython 本身有一个基本的了解。其他我们也不深究,感兴趣的同学自行了解。

赋值语句内存分析

我们可以通过使用id()函数来查看某个对象的内存地址,每个人的电脑内存地址不一样。

a = 1
id(a)  # Output: 4566652048
b = 2
id(b)   # Output: 4566652080
c = 8
id(c)  # Output: 4566652272
d = 8
id(d)  # Output: 4566652272


使用 ==来查看对象的值是否相等,is判断对象是否是同一个对象

c == d  # Output: True
c is d  # Output: True
e = 888888888
id(e)  # Output: 4569828784
f = 888888888
id(f)  # Output: 4569828880
e == f  # Output: True
e is f  # Output: False


解释:我们可以看到,


  • c == d输出 True 和 c is d也输出 True,这是因为,对一个小一点的 int 变量赋值,Python 在内存池(Pool)中分配给 c 和 d 同一块内存地址,
  • e == f为 True,值相同;e is f输出 False,并不少同一个对象。


这是因为 Python 内存池中分配空间,赋予对象的类别并赋予其初始的值。从-5 到 256 这些小的整数,在 Python 脚本中使用的非常频繁,又因为他们是不可更改的,因此只创建一次,重复使用就可以了。


e 和 f 数字比较大,所以只能重新分配地址来。其实-5 到 256 之间的数字,Python 都已经给我安排好了。

>>> i = 256
>>> j = 256
>>> i is j
True
>>> i = 257
>>> j = 257
>>> i is j
False
>>> i = -5
>>> j = -5
>>> i is j
True
>>> i = -6
>>> j = -6
>>> i is j
False


Java 也有这样的机制 缓存范围是 -128 ~ 127


** Cache to support the object identity semantics of autoboxing for values between*** -128 and 127 (inclusive) as required by JLS.*


接着,看对象的内存分析:

li1 = []
li2 = []
li1 == li2  # Output: True
li1 is li2  # Output: False
x = 1
y = x
id(x)  # Output: 4566652048
id(y)  # Output: 4566652048
y = 2
id(y)  # Output: 4566652080
x == y  # Output: False
x is y  # Output: False

CPython 的内存管理

下图的深灰色框现在归 Python 进程所有。


网络异常,图片无法展示
|


Python 将部分内存用于内部使用和非对象内存。另一部分专用于对象存储(您的 int,dict 等)。请注意,这已被简化。如果您需要全貌,则可以看 CPython 源代码,所有这些内存管理都在其中进行。


CPython 有一个对象分配器,负责在对象内存区域内分配内存。这个对象分配器是大多数魔术发生的地方。每当新对象需要分配或删除空间时,都会调用该方法。


通常,为 list 和 int 等 Python 对象添加和删除数据一次不会涉及太多数据。因此,分配器的设计已调整为可以一次处理少量数据。它还尝试在绝对需要之前不分配内存。


现在,我们来看一下 CPython 的内存分配策略。首先,我们将讨论这三个主要部分以及它们之间的关系。


Arenas 是最大的内存块,并在内存中的页面边界上对齐。页面边界是操作系统使用的固定长度连续内存块的边缘。Python 假设系统的页面大小为 256 KB。


Arenas 内有内存池,池是一个虚拟内存页(4 KB)。这些就像我们书中类比的页面。这些池被分成较小的内存块。


给定池中的所有块均具有相同的“大小等级”。给定一定数量的请求数据,大小类定义特定的块大小。


给定池中的所有块均具有相同的“大小等级”。给定一定数量的请求数据,大小类定义特定的块大小。


例如,如果请求 42 个字节,则将数据放入 48 字节大小的块中。

垃圾回收机制

总体看一下 Python 中的垃圾回收技术:


  • 引用计数为主
  • 标记清除和分代回收为辅


如果一个对象的引用计数为 0,Python 解释器就会回收这个对象的内存,但引用计数的缺点是不能解决循环引用的问题,所以我们需要标记清除和分代回收。

什么是引用计数

  • 每个对象都有存有指向该对象的引用总数
  • 查看某个对象的引用计数sys.getrefcount()
  • 可以使用 del 关键字删除某个引用
import sys
l = []
print(sys.getrefcount(l))  # Output: 2
l2 = l
l3 = l
l4 = l3
print(sys.getrefcount(l))  # Output: 5
del l2
print(sys.getrefcount(l))  # Output: 4
i = 1
print(sys.getrefcount(i))  # Output: 140
a = i
print(sys.getrefcount(i))  # Output: 141

当对象的引用计数达到零时,解释器会暂停,来取消分配它以及仅可从该对象访问的所有对象。即满足引用计数为 0 的时候,会启动垃圾回收。


但是引用计数不能解决循环引用的问题,就如下的代码不停跑就能把电脑内存跑满:

>>> a = []
>>> b = []
>>> while True:
...     a.append(b)
...     b.append(a)
...
[1]    31962 killed     python

标记清除

标记清除算法作为 Python 的辅助垃圾收集技术主要处理的是一些容器对象,比如 list、dict、tuple,instance 等,因为对于字符串、数值对象是不可能造成循环引用问题。标记清除分代回收就是为了解决循环引用而生的。


标记清除会使用垃圾收集监控对象,讲对象放到链表上,被垃圾收集监控的对象并非只有垃圾收集机制才能回收,正常的引用计数就能销毁一个被纳入垃圾收集机制监控的对象。


它分为两个阶段:第一阶段是标记阶段,GC 会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。


对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。


在上图中,可以从程序变量直接访问块 1,并且可以间接访问块 2 和 3。程序无法访问块 4 和 5。第一步将标记块 1,并记住块 2 和 3 以供稍后处理。第二步将标记块 2,第三步将标记块 3,但不记得块 2,因为它已被标记。扫描阶段将忽略块 1,2 和 3,因为它们已被标记,但会回收块 4 和 5。


标记清除算法作为 Python 的辅助垃圾收集技术,主要处理的是一些容器对象,比如 list、dict、tuple 等,因为对于字符串、数值对象是不可能造成循环引用问题。Python 使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。


700 是垃圾回收启动的阈值,后面两个 10 和分代回收有关,也就是新增对象与释放对象的差值为 700 时,进行一次垃圾回收,主要目标是循环引用,这个时候会造成卡顿

分代回收(自动)

分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。


  • Python 将所有的对象分为 0,1,2  三代
  • 所有的新建的对象都是 0 代对象
  • 当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。


同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为 Python 的辅助垃圾收集技术处理那些容器对象。


Python 运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。


当两者的差值高于某个阈值时,垃圾回收才会启动


  • 查看阈值 gc.get_threshold()
import gc
print(gc.get_threshold())  # Output: (700, 10, 10)


get_threshold()返回的(700, 10, 10)返回的两个 10。也就是说,每 10 次 0 代垃圾回收,会配合 1 次 1 代的垃圾回收;而每 10 次 1 代的垃圾回收,才会有 1 次的 2 代垃圾回收。理论上,存活时间久的对象,使用的越多,越不容易被回收,这也是分代回收设计的思想。

手动回收

  • gc.collect()手动回收
  • objgraph 模块中的 count()记录当前类产生的实例对象的个数
import gc
result = gc.collect()
print(result)

import objgraph

class Person(Object):
  pass
class Cat(object):
  pass
p = Person()
c = Cat()
p.name ='yuzhou1su'
c.master = p
print(sys.getrefcount(p))
print(sys.getfefcount(c))
del p
del c
gc.collect()
print(objgraph.count('Person'))
print(objgraph.count('Cat'))


当定位到哪个对象存在内存泄漏,就可以用 show_backrefs 查看这个对象的引用链。

内存池(memory pool)机制

频繁 申请、消耗 会导致大量的内存碎片,致使效率变低。


内存池的概念就是在内存中申请一定数量的,大小相等的内存块留作备用。


内存池池由单个大小类的块组成。每个池维护一个到相同大小类的其他池的双向链接列表。这样,即使在不同的池中,该算法也可以轻松找到给定块大小的可用空间。


当有新的内存需求时,就会先从内存池中分配内存留给这个需求。内存不够再申请新的内存。


内存池本身必须处于以下三种状态之一:


  • 已使用
  • 已满
  • 或为空。


优点:减少内存碎片,提高效率。


Pymalloc


  • 针对小对象(<= 512 bytes),Pymalloc 会在内存池中申请内存空间
  • > 512bytes,则会 PyMem_RawMalloc()和 PyMem_RawRealloc()来申请新的内存空间
相关文章
|
1月前
|
监控 算法 Java
如何确保Python的内存管理机制能够有效地工作?
【2月更文挑战第19天】【2月更文挑战第57篇】如何确保Python的内存管理机制能够有效地工作?
|
1月前
|
存储 Java Python
谈谈你对 Python 的内存管理机制的理解。
【2月更文挑战第19天】【2月更文挑战第55篇】谈谈你对 Python 的内存管理机制的理解。
|
2月前
|
缓存 监控 Python
在Python中,如何检测和处理内存泄漏?
【2月更文挑战第7天】【2月更文挑战第18篇】在Python中,如何检测和处理内存泄漏?
|
2月前
|
缓存 Python
给我一些具体的例子,说明如何在Python中使用缓存和释放来避免内存溢出。
给我一些具体的例子,说明如何在Python中使用缓存和释放来避免内存溢出。
14 0
|
2月前
|
数据采集 存储 分布式计算
如何在Python中处理大规模数据集,以避免内存溢出?
如何在Python中处理大规模数据集,以避免内存溢出?
21 1
|
2月前
|
Python
什么是Python中的内存池(Memory Pool)?
什么是Python中的内存池(Memory Pool)?
34 0
|
2月前
|
数据库 数据库管理 Python
解释Python中的内存视图(Memory View)。
解释Python中的内存视图(Memory View)。
|
3月前
|
存储 数据可视化 C++
提高代码效率的6个Python内存优化技巧
当项目变得越来越大时,有效地管理计算资源是一个不可避免的需求。Python与C或c++等低级语言相比,似乎不够节省内存。 但是其实有许多方法可以显著优化Python程序的内存使用,这些方法可能在实际应用中并没有人注意,所以本文将重点介绍Python的内置机制,掌握它们将大大提高Python编程技能。
95 0
|
2月前
|
监控 Python
推荐一些Python的内存分析工具。
【2月更文挑战第7天】【2月更文挑战第19篇】推荐一些Python的内存分析工具。
|
22天前
|
存储 监控 异构计算
【Python】GPU内存监控脚本
【Python】GPU内存监控脚本