深入理解 Python 内存管理与垃圾回收(下)

简介: 再我们看文章之前,先思考一下:如果是你设计,会怎么进行内存管理?我们一起了解看看 Python 是怎么设计的。

赋值语句内存分析

我们可以通过使用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


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

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


再来看看垃圾回收

image.png

垃圾回收机制

来看一下 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)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。


image.png


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


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


Python 使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

分代回收(自动)

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


  • 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)机制

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


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


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


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


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


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


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


总结

内存管理是计算机的一个非常重要的组成部分。 Python 跟 Java、Go 一样,帮助开发者从语言设计层面解决了这个问题,使得我们不用手动分配和释放内存,这也是这类语言的优势。

本文主要解释了:

  • 什么是内存管理,管理方式的方式
  • Cpython 的内存管理方式
  • 垃圾回收机制
  • Python 的引用计数、标记清楚和分代回收的垃圾自动回收方法。
  • 最后介绍了手动回收的包和为了提高内存有效使用的内存池机制
相关文章
|
9月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
825 55
|
5月前
|
存储 大数据 Unix
Python生成器 vs 迭代器:从内存到代码的深度解析
在Python中,处理大数据或无限序列时,迭代器与生成器可避免内存溢出。迭代器通过`__iter__`和`__next__`手动实现,控制灵活;生成器用`yield`自动实现,代码简洁、内存高效。生成器适合大文件读取、惰性计算等场景,是性能优化的关键工具。
308 2
|
6月前
|
传感器 数据采集 监控
Python生成器与迭代器:从内存优化到协程调度的深度实践
简介:本文深入解析Python迭代器与生成器的原理及应用,涵盖内存优化技巧、底层协议实现、生成器通信机制及异步编程场景。通过实例讲解如何高效处理大文件、构建数据流水线,并对比不同迭代方式的性能特点,助你编写低内存、高效率的Python代码。
274 0
|
9月前
|
算法 Java Python
垃圾回收机制 | Python
Python 的垃圾回收机制采用“引用计数”为主,“分代回收”和“标记-清除”为辅的策略。引用计数通过跟踪对象的引用次数,实时释放无引用对象的内存,但存在循环引用问题。分代回收将对象按存活时间分为三代,优先回收短命对象,减少性能开销。标记-清除技术用于解决容器对象的循环引用问题,通过标记不可达对象并清除它们,但需全量扫描堆内存,效率较低。这三种机制共同确保 Python 内存管理的高效与稳定。
296 30
|
9月前
|
数据可视化 Linux iOS开发
Python测量CPU和内存使用率
这些示例帮助您了解如何在Python中测量CPU和内存使用率。根据需要,可以进一步完善这些示例,例如可视化结果或限制程序在特定范围内的资源占用。
365 22
|
12月前
|
监控 Java 计算机视觉
Python图像处理中的内存泄漏问题:原因、检测与解决方案
在Python图像处理中,内存泄漏是常见问题,尤其在处理大图像时。本文探讨了内存泄漏的原因(如大图像数据、循环引用、外部库使用等),并介绍了检测工具(如memory_profiler、objgraph、tracemalloc)和解决方法(如显式释放资源、避免循环引用、选择良好内存管理的库)。通过具体代码示例,帮助开发者有效应对内存泄漏挑战。
609 1
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
358 3
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
205 0
|
7月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
2261 0
|
7月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
763 0