你将得到:
- 完整的面试回答
- 一些结合业务的面试问题
- 结合参考文献可以更容易理解原理
图什么的后续会补的啦~
面试的时候可以这么回答~
首先,在内存分配上,如果超过256KB的大变量 由c的malloc分配,如果没有超过256kb的小变量则使用内存池技术由pymalloc分配。
内存池技术是指预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。
这样做最显著的优势就是能够减少内存碎片,提升效率。
其次,新建变量分配新的名字 或者放到容器 时,引用计数增加,使用del
、重新赋值或者容器销毁时引用计数减少。引用计数为0时启动析构函数,该析构函数__del__()
同样使用内存池技术,避免频繁申请和释放内存。
再次,当存在循环引用 时,del无法使引用计数归零从而造成内存泄漏 ,此时则引入垃圾回收的标记-清除机制Mark-Sweep,
它会从根集(如全局命名空间、调用栈等)出发,遍历所有活动对象,标记通过一系列引用所能到达的对象为可达(reachable)的对象。
然后启动清除机制,再次遍历,将所有未被标记为可达的对象视为不可达(unreachable)的对象,它们将会被free释放内存。
但是由于启动标记-清除机制时应用程序是暂停的,引入了垃圾回收的分代回收Generational Collection机制 ,以空间换时间的方式提高回收效率。分代回收的思想是,存活时间越长的对象越有可能继续存活,因此随着代的增加,回收的频率也逐渐降低 。分代回收可以减少垃圾回收的总体开销,因为频繁回收的主要集中在生命周期短的对象(第0代)。
具体地,
新建变量都被列为第0代,
如果第一次gc扫描时没有被清除则进入第1代,
同理,在对第1代gc扫描时没有被清除的进入了第2代。
而gc扫描的启动是根据分代回收阈值参数设置,
当 <math xmlns="http://www.w3.org/1998/Math/MathML"> 新建变量 − 被释放的变量 ≥ 第 0 代 g c 扫描的阈值 新建变量-被释放的变量\geq 第0代gc扫描的阈值 </math>新建变量−被释放的变量≥第0代gc扫描的阈值 时,则会启动第0代的gc扫描,
当第0代的gc扫描启动的次数达到第1代回收阈值时,则会启动第1代的gc扫描,
同理,当第1代的gc扫描启动的次数达到第2代回收阈值时,则会启动第2代的gc扫描,即全代扫描。
某段时间内如何使特定对象不被垃圾回收?
除了使用gc之外,还可以:
当需要某段时间内某些对象不被垃圾回收,那么在循环引用的基础上可以使用另一个变量去引用它们其中之一,则时间段内三者均不会被垃圾回收.也即
<math xmlns="http://www.w3.org/1998/Math/MathML"> c → a ↔ b c \rightarrow a \leftrightarrow b </math>c→a↔b
在时间段结束后,删除或给另一个变量重新赋值等方法减少引用计数,循环引用过的变量则会在后续gc扫描时被垃圾回收。
<math xmlns="http://www.w3.org/1998/Math/MathML"> c ↛ a ↔ b c \nrightarrow a \leftrightarrow b </math>c↛a↔b
如何手动触发垃圾回收?
python
import gc
# 可以手动触发全代垃圾回收
gc.collect()
# 只触发0代的垃圾回收
collected_gen0 = gc.collect(0)
print(f"Collected {collected_gen0} objects from generation 0.")
其他面试问题
-
性能优化问题:
- 在处理大数据集时,如何通过Python的内存管理策略来优化你的程序性能?
- 描述一种场景,你需要手动控制垃圾回收。你会如何实施,并解释为什么这样做有助于提升应用性能?
-
内存泄漏定位:
- 如果你怀疑一个Python应用有内存泄漏,你会如何定位问题源头?请描述你的步骤和使用的工具。
- 你能否给出一个例子,说明如何使用
gc
模块来识别和解决循环引用导致的内存泄漏问题?
-
内存分配策略:
- 在设计一个需要高频率创建和销毁大量小对象的应用时,你会如何优化内存使用?
- Python中有哪些机制可以帮助减少内存碎片?你在实际开发中是如何应用这些机制的?
-
垃圾回收机制对业务的影响:
- 描述一种业务场景,其中Python的自动垃圾回收可能会导致性能问题。你会如何预防或解决这些问题?
- 在实时数据处理系统中,垃圾回收可能引起的延迟是一个问题。讨论你可以采用的几种策略来最小化这种延迟。
-
分代垃圾回收的具体应用:
- Python的分代垃圾回收机制如何影响对象的生命周期管理?在什么情况下,调整这些参数可能会提高程序的效率?
- 你有没有实际例子,你通过调整垃圾回收阈值来解决内存问题或改善性能?
原理
Python 使用一种名为"自动内存管理"的机制,主要包括以下几个方面:
- 引用计数 :Python 内部使用引用计数机制来跟踪每个对象有多少引用指向它
sys.getrefcount(obj)
。当某个对象的引用计数为0时,就列入了垃圾回收队列。- 引用计数增加 的情况:
- 一个对象被分配给一个新的名字(例如:a=[1,2])
- 将其放入一个容器中(如列表、元组或字典)(例如:c.append(a))
- 引用计数减少 的情况:
- 使用del语句对对象别名显式的销毁(例如:del b)
- 对象所在的容器被销毁或从容器中删除对象(例如:del c )
- 引用超出作用域或被重新赋值(例如:a=[3,4])
- 引用计数增加 的情况:
- 垃圾回收 :Python的垃圾回收机制采用引用计数机制为主,标记-清除和分代回收机制为辅的策略。
- 标记-清除机制 用来解决计数引用带来的循环引用而无法释放内存的问题,即两个或更多对象互相引用,导致它们的引用计数永远不会达到零,进而导致内存泄漏的问题。循环引用只有在容器对象才会产生,比如字典,元组,列表等。
- 标记阶段,遍历所有活动对象,并标记所有可达(reachable)的对象。可达的对象即是那些从根集(如全局命名空间、调用栈等)出发,通过一系列引用所能到达的对象
- 清除阶段,所有未被标记为可达的对象被视为不可达(unreachable),这些对象将被垃圾回收器清理。
- 分代回收机制
Generational Collection
是为提升垃圾回收的效率。它是基于这样一种统计事实:"对象存在时间越长,越可能不是垃圾,应该越少去收集 " 这样在执行标记-清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度,是一种以空间换时间的方法策略。-
Python将所有的对象分为年轻代(第0代)、中年代(第1代)、老年代(第2代)三代。所有的新建对象默认是 第0代对象。当在第0代的gc扫描中存活下来的对象将被移至第1代,在第1代的gc扫描中存活下来的对象将被移至第2代。
gc扫描次数(第0代>第1代>第2代)
-
当某一代中被分配的对象与被释放的对象之差达到某一阈值时,就会触发当前一代的gc扫描。当某一代被扫描时,比它年轻的一代也会被扫描,因此,第2代的gc扫描发生时,第0,1代的gc扫描也会发生,即为全代扫描。
比如
gc.get_threshold()
即分代回收机制的参数阈值设置为(700,10,10)
时,代表着当新分配的对象数量减去释放的对象数量等于700时触发第0代gc扫描,如果触发了10次第0 代gc扫描,则会启动1次第1代扫描,进而如果触发了10次第1代gc扫描,则会启动第2代扫描,即全代扫描。
-
- 标记-清除机制 用来解决计数引用带来的循环引用而无法释放内存的问题,即两个或更多对象互相引用,导致它们的引用计数永远不会达到零,进而导致内存泄漏的问题。循环引用只有在容器对象才会产生,比如字典,元组,列表等。
- 内存池
PyMalloc
技术:Python 使用内存池技术来管理小对象 的内存分配。通过预分配内存块来管理小对象,减少系统调用,提高内存分配效率。内存池的作用就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。- Level+3层:对于python内置的对象(比如int,dict等)都有独立的私有内存池,对象之间的内存池不共享,即int释放的内存,不会被分配给float使用
- Level+2层:当申请的内存大小小于256KB时,内存分配主要由 Python 对象分配器(Python's object allocator)实施 , 也就是使用内存池技术
- Level+1层:当申请的内存大小大于256KB时,由Python原生的内存分配器进行分配,本质上是调用C标准库中的malloc/realloc等函数