掘金 后端 ( ) • 2024-04-20 16:48

你将得到:

  • 完整的面试回答
  • 一些结合业务的面试问题
  • 结合参考文献可以更容易理解原理

图什么的后续会补的啦~

面试的时候可以这么回答~

首先,在内存分配上,如果超过256KB的大变量由c的malloc分配,如果没有超过256kb的小变量则使用内存池技术由pymalloc分配。

内存池技术是指预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。

这样做最显著的优势就是能够减少内存碎片,提升效率

其次,新建变量分配新的名字或者放到容器时,引用计数增加,使用del、重新赋值或者容器销毁时引用计数减少。引用计数为0时启动析构函数,该析构函数__del__()同样使用内存池技术,避免频繁申请和释放内存。

再次,当存在循环引用时,del无法使引用计数归零从而造成内存泄漏,此时则引入垃圾回收的标记-清除机制Mark-Sweep

它会从根集(如全局命名空间、调用栈等)出发,遍历所有活动对象,标记通过一系列引用所能到达的对象为可达(reachable)的对象。

然后启动清除机制,再次遍历,将所有未被标记为可达的对象视为不可达(unreachable)的对象,它们将会被free释放内存。

但是由于启动标记-清除机制时应用程序是暂停的,引入了垃圾回收的分代回收Generational Collection机制,以空间换时间的方式提高回收效率。分代回收的思想是,存活时间越长的对象越有可能继续存活,因此随着代的增加,回收的频率也逐渐降低。分代回收可以减少垃圾回收的总体开销,因为频繁回收的主要集中在生命周期短的对象(第0代)

具体地,

新建变量都被列为第0代,

如果第一次gc扫描时没有被清除则进入第1代,

同理,在对第1代gc扫描时没有被清除的进入了第2代。

而gc扫描的启动是根据分代回收阈值参数设置,

当 $新建变量-被释放的变量\geq 第0代gc扫描的阈值 $ 时,则会启动第0代的gc扫描,

当第0代的gc扫描启动的次数达到第1代回收阈值时,则会启动第1代的gc扫描,

同理,当第1代的gc扫描启动的次数达到第2代回收阈值时,则会启动第2代的gc扫描,即全代扫描。

某段时间内如何使特定对象不被垃圾回收?

除了使用gc之外,还可以:

当需要某段时间内某些对象不被垃圾回收,那么在循环引用的基础上可以使用另一个变量去引用它们其中之一,则时间段内三者均不会被垃圾回收.也即

$c \rightarrow a \leftrightarrow b$

在时间段结束后,删除或给另一个变量重新赋值等方法减少引用计数,循环引用过的变量则会在后续gc扫描时被垃圾回收。

$c \nrightarrow a \leftrightarrow b$

如何手动触发垃圾回收?

import gc
# 可以手动触发全代垃圾回收
gc.collect() 

# 只触发0代的垃圾回收
collected_gen0 = gc.collect(0)
print(f"Collected {collected_gen0} objects from generation 0.")

其他面试问题

  1. 性能优化问题

    • 在处理大数据集时,如何通过Python的内存管理策略来优化你的程序性能?
    • 描述一种场景,你需要手动控制垃圾回收。你会如何实施,并解释为什么这样做有助于提升应用性能?
  2. 内存泄漏定位

    • 如果你怀疑一个Python应用有内存泄漏,你会如何定位问题源头?请描述你的步骤和使用的工具。
    • 你能否给出一个例子,说明如何使用gc模块来识别和解决循环引用导致的内存泄漏问题?
  3. 内存分配策略

    • 在设计一个需要高频率创建和销毁大量小对象的应用时,你会如何优化内存使用?
    • Python中有哪些机制可以帮助减少内存碎片?你在实际开发中是如何应用这些机制的?
  4. 垃圾回收机制对业务的影响

    • 描述一种业务场景,其中Python的自动垃圾回收可能会导致性能问题。你会如何预防或解决这些问题?
    • 在实时数据处理系统中,垃圾回收可能引起的延迟是一个问题。讨论你可以采用的几种策略来最小化这种延迟。
  5. 分代垃圾回收的具体应用

    • 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等函数

参考文献

面试必备:Python内存管理机制