Python内存管理、垃圾回收机制

一、各语言内存管理

C和C++:没有内存管理,完全是靠程序员手动释放和申请。
java:通过关键字new为对象申请内存空间,由GC决定和执行对象的释放。

二、Python内存管理&术语

python中一切皆对象,这些对象内存在运行时动态分配。
python变量无需事先申明和指定类型,程序员无需关心内存管理,python解释器会自动回收。
这一切由python内存管理器承担复杂的内存管理工作。

1、内存分配

Python解释器在运行时为程序分配内存。当程序创建变量、对象或函数时,解释器会在内存中为其分配空间。Python使用堆来管理内存,这意味着内存分配和释放是动态的。

2、引用计数

Python使用引用计数来跟踪对象的引用数量。每当一个对象被引用时,其引用计数会增加;当一个引用被删除时,其引用计数会减少。当一个对象的引用计数为0时,解释器会将其标记为垃圾并回收其内存。

3、循环引用

在Python中,循环引用是一种常见的问题。当两个或多个对象相互引用时,它们的引用计数永远不会为0,因此无法被垃圾回收。Python通过周期检测来解决循环引用问题。当检测到循环引用时,解释器会将其标记为垃圾并回收其内存。

当两个对象a和b相互引用时,因此引用计数不会归零,对象也不会销毁,从而导致内存泄露。为解决这一问题,解释器会定期执行一个循环检测器,搜索不可访问对象的循环并删除它们。

三、内存管理机制(垃圾回收机制)

python主要采用引用计数机制为主,标记-清除分代收集(分代回收)两种机制为辅的策略。

1、引用计数机制

引用计数是一种垃圾回收机制,当python的某个对象的引用计数为0时,该对象就成为要被回收的垃圾,但是并不是立马回收,而是等到garbage collection (GC)触发时才回收。
注意:
(1)、栈内存、堆内存

  • 栈内存:存放变量和内存地址
    在这里插入图片描述

  • 堆内存:存放值

(2),GC的效率:

在垃圾回收时,python不能进行其他任务,频繁的垃圾回收会大大降低python的工作效率;
原因是GIL的存在,当GC运行的时候,会抢python解释器锁,其他线程处于阻塞状态,会降低python的工作效率,所以GC的回收不要那么频繁。

(3),GC的启动:

当python运行时,会记录其中分配对象和取消对象的次数,当二者的差值高于某个阈值(默认700)时,垃圾回收才会启动。
(4),GC的触发:

  • 主动调用gc.collect()
  • GC达到阈值时自动触发
  • 程序退出时

查看引用计数:

使用sys包中的getrefcount(),来查看某个对象的引用计数。
需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

from sys import getrefcount
a = [1, 2, 3]
print(getrefcount(a))  # 2
b = a
print(getrefcount(b))  # 3
# 由于上述原因,两个getrefcount将返回2和3,而不是期望的1和2。

引用计数增加:

  • 一个对象分配一个新的名称: a=25535
  • 将其放到一个容器中(如列表、元组或字典):

引用计数减少:

  • 使用del语句删除引用:del a
  • 引用超出作用域或被重新赋值:函数执行完函数中的引用计数为0,可以进行回收
def test():
    b=667787
test()

2、标记-清除机制(解决循环引用情形下的缺陷)

引用计数看上去是一种完美的解决方案,但实际上它存在一些潜在的问题,如下所示:

list1 = [1, 2, 3] # list1 的标记 +1
list2 = [4, 5, 6] # list2 的标记 +1
list1.append(list2) # list2 的标记 +1
list2.append(list1) # list1 的标记 +1

上述代码执行完之后,list1 和 list2 的引用计数标记都变成 2,如下图所示:此时如果程序员觉得 list1 和 list2 都无需再继续使用,便执行 del list1 和 del list2,随后 list1 和 list2 的引用计数标记变成 1 不等于 0,但实际上 list1 和 list2 的生命周期已经结束,由于引用计数机制在循环引用情形下的缺陷,list1 和 list2 占用的堆内存并不会被释放;这种现象对于所有可变对象都会发生,就是前文所说的内存泄漏!除了内存泄漏,引用计数还存在浪费额外资源存储引用计数器、二次释放。

当应用程序可用的内存空间被耗尽时,就会停止整个程序,然后进行两项工作,标记和清除。

标记清除算法是一种基于追踪回收 技术实现的垃圾回收算法。它分为两个阶段:

  • 第一阶段是标记阶段,GC会把所有的『活动对象』(有引用的对象)打上标记
  • 第二阶段是把那些没有标记的对象『非活动对象』进行回收。

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

标记清除机制作为python的辅助垃圾收集技术,主要处理一些容器,比如list,dict,tuple等,由于对于字符串,数值对象不可能造成循环引用问题。python使用一个双向链表将这些对象容器组织起来,不过简单粗暴的标记清楚也有明显缺点,清楚非活动对象前需要顺序扫描整个堆内存。

3、分代回收机制(帮我们回收循环嵌套的引用,增加数据回收的效率。是以空间换时间的策略)

因为, 标记和清除的过程效率不高。清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。还有一个问题就是:什么时候扫描去检测循环引用?

为了解决上述的问题,python又引入了分代回收。分代回收解决了标记清楚时什么时候扫描的问题,并且将扫描的对象分成了3级,以及降低扫描的工作量,提高效率。
存活时间越久的对象,越不可能在后面的程序中变成垃圾。

所有的新建对象都是0代对象
当某一代对象经历过垃圾回收,依然存活,那么他就被归入下一代对象了。
垃圾回收启动时,一定会扫描所有的0代对象
如果0代经过一定次数垃圾回收,那么就启动对1代的扫描清理;当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描
0代: 0代中对象个数达到700个(阈值),扫描一次。
1代: 0代扫描10次,则1代扫描1次。
2代: 1代扫描10次,则2代扫描1次。

分代回收就是为了解决循环引用问题,增加数据回收的效率。通过对象存在时间不同,采用不同的算法来回收垃圾。

解释2:
每次进行垃圾回收都扫描一遍堆内存实际上很耗时,Python 在此基础上提出了分代回收机制,就是为所有堆内存的对象设置优先级,每隔一定时间扫描堆内存,为所有标记过的存活对象的阈值 +1,阈值增加到一定数量就会上升一个优先级(代表该堆内存可能接下来还会用到,因此下一轮扫描就不会扫描这些高优先级的内存空间,从而节省时间)

四、Python内存管理技巧,怎么优化内存管理

  • 手动垃圾回收
    先调用del a ; 再调用gc.collect()即可手动启动GC(嵌套的引用删除不了,因为引用计数为1)。用来解决在内存敏感时(例如:内存不足)加速释放不再需要的内存。
  • 调高垃圾回收阈值
    gc.set_threshold 设置垃圾回收阈值(收集频率)。 将 threshold0 设为零会禁用回收。
    gc.set_threshold(800,20,20)
  • 避免循环引用
  • 避免不必要的对象创建
    在Python中,创建对象需要消耗一定的内存。因此,我们应该尽量避免不必要的对象创建。例如,使用列表推导式而不是循环来创建列表,使用字符串拼接而不是字符串重复来创建字符串等。
  • 使用内置数据类型和容器
    Python内置的数据类型和容器(如列表、元组、字典、集合等)通常比自定义数据类型更高效。这是因为内置数据类型和容器经过优化,具有更好的内存使用效率和性能。
  • 避免全局变量和静态变量
    全局变量和静态变量会在程序运行期间一直占用内存。因此,我们应该尽量避免使用全局变量和静态变量。如果必须使用全局变量或静态变量,我们应该考虑其生命周期和作用域,以便及时释放它们所占用的内存。- 使用上下文管理器来管理资源
    上下文管理器是一种用于管理资源的机制,它允许我们在代码块之间进行资源的管理和分配。在Python中,我们可以通过使用with语句来创建上下文管理器,以便更好地管理资源并减少内存泄漏的风险。
  • 使用弱引用和延迟加载
    弱引用是一种指向对象的引用,它不会增加对象的引用计数。通过使用弱引用来代替强引用,我们可以避免循环引用问题。另外,延迟加载也是一种减少内存占用和提高性能的技巧。例如,我们可以使用importlib模块来动态加载模块或函数,以减少不必要的内存占用。

五、内存溢出&&内存泄露

内存溢出 out of memory:

是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak:

是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!

以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

内存溢出的解决方案:

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:

  • 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

  • 检查代码中是否有死循环或递归调用。

  • 检查是否有大循环重复产生新对象实体。

  • 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

  • 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

第四步,使用内存查看工具动态查看内存使用情况。**为主,标记-清除和分代收集(分代回收)两种机制为辅的策略。

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值