内存管理
python的内存管理机制:引用计数、垃圾回收,内存池机制
接口:
gc.disable() # 暂停自动垃圾回收.
gc.collect() # 执行一次完整的垃圾回收, 返回垃圾回收所找到无法到达的对象的数量.
gc.set_threshold() # 设置Python垃圾回收的阈值.
gc.set_debug() # 设置垃圾回收的调试标记. 调试信息会被写入std.err.
一、变量与对象
关系图:
- python中万物皆对象,所以python的存储问题是对象的存储问题,并且对于每个对象,python会分配一块内存空间去存储它。
- 对于整数和短小的字符等,python会执行缓存机制,将这些对象进行缓存,不会为相同的对象分配多个内存空间。
- 容器对象,如列表、元组、字典等,存储的其他对象,仅仅是其他对象的引用,即地址,并不是这些对象本身。
二、引用计数
在python中,每个对象都有指向该对象的引用总数——称为“引用计数”。
查看对象的引用计数:sys.getrefcount()
计数引用机制:一个对象会记录着引用自己的对象的个数,每增加一个引用,个数加一,每减少一个引用,个数减一。在检测到兑现引用个数为0时,对普通的对象进行释放内存。
引用计数增加:
- 对象被创建:x=4
- 另外的变量被创建:y=x
- 被作为参数传递给函数:foo(x)
- 作为容器对象的一个元素:a=[1, x, '33']
引用计数减少:
- 一个本地引用离开了它的作用域。比如上面的foo(x)函数结束时,x指向的对象引用减1。
- 对象的别名被显示的销毁:del x
- 对象的一个别名被赋值给其他对象:x=789
- 对象从一个窗口对象中移除:myList.remove(x)
- 窗口对象本身被销毁:del myList,或者窗口对象本身离开了作用域
循环引用问题:循环引用即对象之间进行互相引用,出现循环引用后,利用上述引用计数机制无法对循环引用中的对象进行释放空间,这就是循环引用问题。
class Person(object):
pass
class Dog(object):
pass
p = Person()
d = Dog()
p.pet = d
d.master = p
这里对象p中的属性引用d,而对象d中属性同时来引用p,从而造成仅仅删除p和d对象,也无法释放其内存空间,因为他们依然在被引用。深入解释就是,循环引用后,p和d被引用个数为2,删除p和d对象后,两者被引用个数变为1,并不是0,而python只有在检查到一个对象的被引用个数为0时,才会自动释放其内存,所以这里无法释放p和d的内存空间。
三、垃圾回收
当python中的对象越来越多,占据越来越大的内存,系统会启动垃圾回收(garbage collection),将没用的对象清除。
垃圾回收的作用:从经过引用计数器机制后还没有被释放掉内存的对象中,找到循环引用对象,并释放掉其内存。
自动回收
当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。
import gc
gc.get_threshold() #gc模块中查看阈值的方法
(700, 10, 10)
阈值分析:700即是系统垃圾回收启动的阈值;每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。
手动回收
手动启动垃圾回收gc.collect()
分代回收
- python将所有的对象分为0、1、2三代;
- 所有的新建对象都是0代对象;
- 当某一代对象经历过垃圾回收,依然存活,就被归入下一代对象。
四、内存池机制
Python引用了一个内存池(memory pool)机制,即Pymalloc机制(malloc:n.分配内存),用于对小块内存的申请和释放管理。
Python的内存池分为大内存和小内存:(256K为界限)
大内存使用malloc进行分配
小内存使用内存池进行分配
Python的内存池(金字塔)
- 第3层:最上层,用户对Python对象的直接操作。
- 第1层和第2层:内存池,有Python的接口函数PyMem_Malloc实现——若请求分配的内存在1~256字节之间就用内存池管理系统进行分配,调用malloc函数分配内存,但是每次只会分配一块大小为256K的大块内存,不会调用free函数释放内存,将该内存块留在内存池中以便下次使用。
- 第0层:大内存-----若请求分配的内存大于256K,malloc函数分配内存,free函数释放内存。
- 第-1,-2层:操作系统进行操作
参考:https://blog.csdn.net/ChaoFeiLi/article/details/100518277
五、内存优化
手动垃圾回收
对python的垃圾回收进行调优的一个最简单的手段便是关闭自动回收,根据情况手动触发。例如在用python开发游戏时,可以在一局游戏的开始关闭GC,然后在该局游戏结束后手动调用一次GC清理内存。这样能完全避免在游戏过程中因此GC造成卡顿。但是缺点是在游戏过程中可能因为内存溢出导致游戏崩溃。
调高垃圾回收阈值
相比完全手动的垃圾回收,一个更温和的方法是调高垃圾回收的阈值。例如一个游戏可能在某个时刻产生大量的子弹对象(假如是2000个)。而此时Python的垃圾回收的threshold为1000。则一次垃圾回收会被触发,但这2000个子弹对象并不需要被回收。如果此时Python的垃圾回收的threshold0为10000,则不会触发垃圾回收。若干秒后,这些子弹命中目标被删除,内存被引用计数机制自动释放,一次(可能很耗时的)垃圾回收被完全避免了。
调高阈值的方法能在一定程度上避免内存溢出的问题(但不能完全避免),同时可能减少客观的垃圾回收开销。根据具体项目不同,甚至是程序输入的不同,合适的阈值也不同。因此需要反复测试找到一个合适的阈值,这也算调高阈值这种手段的一个缺点。
避免循环引用(手动解循环引用和使用弱引用)
一个可能更好的方法是使用良好的编程习惯尽可能的避免循环引用。两种常见的手段包括:手动解循环引用和使用弱引用。
手动解循环引用:
手动解循环引用值在编写代码时写好解开循环引用的代码,在一个对象使用结束不再需要时调用。例如:
import objgraph
class A(object):
def __init__(self):
self.child = None
def destroy(self):
self.child = None
class B(object):
def __init__(self):
self.parent = None
def destroy(self):
self.parent = None
def test3():
a = A()
b = B()
a.child = b
b.parent = a
a.destroy()
b.destroy()
test3()
print('Object count of A:', objgraph.count('A'))
print('Object count of B:', objgraph.count('B'))
#Object count of A: 0
#Object count of B: 0
使用弱引用
弱引用是指当引用一个对象时,不增加该对象的引用计数,当需要使用到该对象的时候需要首先检查该对象是否还存在。弱引用的实现方式有多种,Python自带一个弱引用库weakref,这里使用weakref改写我们的代码:
def test4():
a = A()
b = B()
a.child = weakref.ref(b)
b.parent = weakref.ref(a)
test4()
print('Object count of A:', objgraph.count('A'))
print('Object count of B:', objgraph.count('B'))
#Object count of A: 0
#Object count of B: 0
除了使用Python自带的weakref库以外,通常我们也可以根据自己项目的业务逻辑实现弱引用。例如在游戏开发中,通常很多对象都是有唯一的ID的。在引用一个对象时我们可以保存其ID而不是直接引用该对象。在需要使用该对象的时候首先根据去检查对象是否存在。
六、内存泄露与内存溢出
内存泄露
是指程序在申请内存后,无法释放已申请的内存空间就造成了内存泄露,一次内存泄露似乎不会有大的影响,但内存泄露堆积后的后果就是内存溢出。
有del()函数的对象间的循环引用是导致内存泄露的主凶。不使用一个对象时使用:del object来删除一个对象的引用计数就可以有效防止内存泄露问题。通过Python扩展模块gc来查看不能回收的对象的详细信息。可以通过sysy.getrefcount(obj)来获取对象的引用计数,并根据返回值是否为0来判断是否内存泄露。
内存泄露分类(按发生方式分类):
- 常发性内存泄漏:发生内存泄露的代码会被多次执行到,每次被执行的都会导致一块内存泄露。
- 偶发性内存泄露:发生内存泄露的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄露至关重要。
- 一次性内存泄露:发生内存泄露代码只会被执行一次,由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄露。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄露只会发生一次。
- 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄露,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄露为隐式内存泄漏。
内存泄露解决方法:
- 内存泄露也许是因为活动已经被使用完毕,但是仍然在其他地方被引用,导致无法对其进行回收。我们只需要对活动进行引用的类独立出来或者将其变为静态类,该类顺着活动的结束而结束,也就没有了当活动结束但仍然还被其他类引用的情况。
- 资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉。
- 集合容器中的内存泄露,我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。需要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
- WebView造成的泄露,当我们不使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。我们应该为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁。
内存溢出
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了一块存储int类型数据的存储看空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出,简单来说就是自己所需要的使用的空间比我们拥有的内存大内存不够使用所造成的的内存溢出。
内存溢出原因:
- 内存中加载的数据量过于庞大,如一次从数据库中取出过多数据。
- 集合类中有对对象的引用,使用完后未清空,产生了堆积,使得JVM不能回收。
- 代码中存在死循环或循环产生过多重复的对象实体。
- 使用的第三方软件中的BUG。
- 启用参数内存值设定的过小。
内存溢出解决:
- 第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xms参数一定不要忘记加)
- 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其他异常或错误。
- 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
- 重点排查以下几点:
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查List、Map等集合对象是否有使用完后,未清除的问题。List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
- 重点排查以下几点:
- 第四步,使用内存查看工具动态查看内存使用情况