当前位置: 首页 > 科技观察

今天给大家分享一篇关于Python内存管理的文章,有兴趣的可以进来看看

时间:2023-03-20 10:26:41 科技观察

语言的内存管理是语言设计的一个重要方面。它是决定语言表现的重要因素。无论是C语言的手工管理,还是Java的垃圾回收,都成为了该语言最重要的特性。这里以Python语言为例,说明动态类型、面向对象语言的内存管理方法。使用赋值语句的对象内存是语言最常见的功能之一。但即使是最简单的赋值语句也可能非常有意义。Python的赋值语句值得研究。a=1整数1是一个对象。而a是一个参考。使用赋值语句,将a指向对象1的引用。Python是一种动态类型语言(请参阅动态类型),其中对象与引用是分开的。就像使用“筷子”一样,Python通过引用来触摸和翻转真实的食物对象。引用和对象要探索对象在内存中的存储位置,我们可以求助于Python的内置函数id()。它用于返回对象的标识。其实这里所谓的标识就是对象的内存地址。a=1print(id(a))print(hex(id(a)))在我的电脑上,它们返回:11246696'0xab9c68'分别是内存地址的十进制和十六进制表示。在Python中,整数和短字符,Python会缓存这些对象以供重复使用。当我们创建多个等于1的引用时,我们实际上是在让所有这些引用都指向同一个对象。a=1b=1print(id(a))print(id(b))上面程序返回1124669611246696,可以看出a和b其实是指向同一个对象的两个引用。要验证两个引用是否指向同一个对象,我们可以使用is关键字。is用于判断两个引用指向的对象是否相同。#Truea=1b=1print(aisb)#Truea="good"b="good"print(aisb)#Falsea="verygoodmorning"b="verygoodmorning"print(aisb)#Falsea=[]b=[]print(aisb)以上注释为对应的运行结果。如您所见,由于Python缓存整数和短字符串,因此每个对象只有一个副本。例如,对整数1的所有引用都指向同一个对象。即使使用赋值语句,也只会创建一个新引用,而不是对象本身。长字符串和其他对象可以有多个相同的对象,可以使用赋值语句创建新对象。在Python中,每个对象都有对该对象的引用总数,即引用计数(referencecount)。我们可以使用sys包中的getrefcount()来查看一个对象的引用计数。需要注意的是,当一个引用被用作参数并传递给getrefcount()时,该参数实际上创建了一个临时引用。因此,getrefcount()得到的结果会比预期的多1。fromsysimportgetrefcounta=[1,2,3]print(getrefcount(a))b=aprint(getrefcount(b))由于上述原因,两个getrefcounts将返回2和3,而不是预期的1和2。对象引用对象Python的容器对象(container),如表、字典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,而是对每个元素对象的引用。我们还可以自定义一个对象,引用其他对象:))print(id(b))可以看出a引用了对象b。对象是指对象,这是Python最基本的形式。就连a=1的赋值,实际上也是让字典的一个键“a”的元素引用了整型对象1。这个字典对象就是用来记录所有全局引用的。字典引用了整数对象1。我们可以使用内置函数globals()查看这个字典。当一个对象A被另一个对象B引用时,A的引用计数会加1fromsysimportgetrefcounta=[1,2,3]print(getrefcount(a))b=[a,a]print(getrefcount(a))由于对象b引用了两次a,所以a的引用计数增加了2。对容器对象的引用可以形成复杂的拓扑结构。我们可以使用objgraph包来绘制它的引用关系,比如x=[1,2,3]y=[x,dict(key1=x)]z=[y,(x,y)]importobjgraphobjgraph.show_refs([z],filename='ref_topo.png')objgraph是Python的第三方包。安装前需要先安装xdot。sudoapt-getinstallxdotsudopipinstallobjgraphObjgraph官网两个对象可能会互相引用,这样就形成了所谓的引用循环。a=[]b=[a]a.append(b)即使是对象,也只需要引用自己就可以形成引用循环。a=[]a.append(a)print(getrefcount(a))引用环会给垃圾回收机制带来很多麻烦,后面会详细介绍。引用减少对象的引用计数可能会减少。例如,可以使用del关键字来删除一个引用:fromsysimportgetrefcounta=[1,2,3]b=aprint(getrefcount(b))delaprint(getrefcount(b))del也可以用来删除容器元素中的元素,如:a=[1,2,3]dela[0]print(a)如果一个引用指向对象A,当这个引用被重定向到某个其他对象B时,对象A的引用计数被递减:fromsysimportgetrefcounta=[1,2,3]b=aprint(getrefcount(b))a=1print(getrefcount(b))垃圾回收吃多了总会发胖,Python也一样。当Python中的对象越来越多时,它们占用的内存也会越来越多。不过你不用太担心Python的体型,它会在合适的时候乖乖“减肥”,开始垃圾回收(garbagecollection),清理无用的对象。垃圾收集可用于多种语言,例如Java和Ruby。虽然最终目的是为了塑造苗条的提醒,但是不同语言的减肥方案还是有很大差异的(这点可以和本文以及Java的内存管理和垃圾回收类比)。原则上,当Python中一个对象的引用计数降为0时,就意味着没有引用指向该对象,该对象就变成了垃圾,需要回收。例如,一个新的对象被赋值给一个引用,这个对象的引用计数就变成了1。如果这个引用被删除,这个对象的引用计数就为0,那么这个对象就有资格进行垃圾回收了。比如下表:a=[1,2,3]deladela之后,没有引用指向之前建立的[1,2,3]表。用户不可能以任何方式触摸或使用此对象。如果这个对象继续留在内存中,它就会变成不健康的脂肪。当垃圾回收开始时,Python扫描引用计数为0的对象并清除其占用的内存。然而,减肥是一项昂贵且费力的工作。在垃圾回收期间,Python不能执行其他任务。频繁的垃圾回收会大大降低Python的生产力。如果内存中的对象不多,就没有必要一直启动垃圾回收。因此,Python只会在特定条件下自动启动垃圾回收。Python在运行时,会记录对象分配和对象释放的次数。只有当两者之间的差异超过某个阈值时,垃圾收集才会开始。我们可以通过gc模块的get_threshold()方法查看阈值:importgcprint(gc.get_threshold())返回(700,10,10),后面两个10是分代回收相关的阈值,后面可以看到.700是垃圾收集开始的阈值。可以通过gc中的set_threshold()方法重新设置。我们也可以手动启动垃圾收集,即使用gc.collect()。分代回收Python也采用了分代回收的策略。该策略的基本假设是对象存活的时间越长,它在程序后期变成垃圾的可能性就越小。我们的程序经常会产生大量的对象,很多对象产生和消失的速度都很快,但有些对象却用了很长时间。为了信任和效率,对于这种“长寿”的对象,我们相信它们的用处,所以在垃圾回收中减少扫描它们的频率。小家伙需要多检查一下。Python将所有的对象分为3代:0、1、2。所有新创建的对象都是0代对象。当某一代对象经过垃圾回收后仍然存活时,就被归类为下一代对象。当垃圾回收开始时,它肯定会扫描所有第0代对象。如果0代已经经历了一定次数的垃圾回收,那么就开始对0代和1代进行扫描清理。当1代也经历了一定次数的垃圾回收之后,就会开始扫描0,1,2,即所有对象。这两次就是上面get_threshold()返回的(700,10,10)返回的两个10。也就是说,每10次第0代垃圾回收都会伴随一次第1代垃圾回收;每10次第1代垃圾回收将伴随第2代垃圾回收。也可以通过set_threshold()进行调整,比如更频繁地扫描2代对象。importgcgc.set_threshold(700,10,5)Isolatedreferencerings引用环的存在会给上面的垃圾回收机制带来很大的困难。这些引用环可能构成了一些不能使用的对象,但是引用计数不为0。参考环。删除了a和b的引用后,这两个对象就不能再从程序中调用了,也就没用了。但是由于引用环的存在,这两个对象的引用计数并没有降为0,也就不会被垃圾回收。孤立的引用环为了回收这样的引用环,Python复制了每个对象的引用计数,表示为gc_ref。假设,对于每个对象i,这个计数是gc_ref_i。Python将迭代所有对象i。对于对象i引用的每个对象j,将对应的gc_ref_j减1。遍历结果遍历后,gc_ref不为0的对象,这些对象引用的对象,以及更下游引用的对象都需要保留。其他对象被垃圾收集。总结Python作为一种动态类型语言,将对象和引用分开。这与过去的面向过程的语言有很大的不同。为了有效地释放内存,Python内置了对垃圾收集的支持。Python采用了一种比较简单的垃圾回收机制,即引用计数,因此需要解决孤立引用环的问题。Python与其他语言既有共性也有特殊性。了解这种内存管理机制是提高Python性能的重要一步。