python函数多次调用内存溢出——python内存泄漏和内存溢出的解决方案


一、 内存泄漏

python 本身虽然也有垃圾回收的功能, 但是同样也会产生内存泄漏问题;
对于一个python实现的,长期运行的后台服务进程来说,如果内存持续增长,则很可能是有了 “内存泄漏” 。

内存泄漏原因:

有以下三种原因:

1 所用到C语言开发的底层模块中出现了内存泄漏;
2 代码中用到了全局的list, dict或者其他容器, 不停的往这些容器中插入对象, 而忘记了在使用完之后进行删除回收
3 代码中有“引用循环”, 并且被引用的对象定义了 __del__ 方法, 就会发生内存泄漏;

question:
1、 为什么循环引用的对象定义了__del__ 方法之后, collect就不起作用了呢?
gc模块最常用的方法,就是gc.collect(), 使用collect() 方法对循环引用的对象进行垃圾回收
如果我们在类中重载了__del__方法,__del__方法定义了在del语句删除对象时,除了释放内存空间以外的操作。
一般而言, 在使用del语句时,解释器会查看被删除对象的引用计数, 如果为0,则释放内存,并执行del方法;
循环引用,首先del语句出现时, 本身引用计数就不为0(因为循环引用存在), 所以解释器不释放内存
再者,执行collect方法时,会清除循环引用所产生的无效引用计数,从而达到del的目的,对于这两个循环引用对象而言
python无法判断调用它们的del方法会不会要用到对方那个对象,比如在进行b.del()时,可能会用到b.a也就a, 如果
在那之前a已经被释放,则无法使用。
为了避免这种情况, collect方法默认不对重载了del方法的循环引用,进行对象回收,而它们的状态会从unreachable转变为
uncollectable。 由于是uncollectable的,自然就不会被collect,从而进入garbage表。
2、 内存泄漏诊断思路
无论哪一种方式的内存泄漏, 最终的表现形式都是python对象不停的增长;因此,首先需要找到这些异常对象。
3、 诊断步骤
工具: gc模块和objgraph模块
gc模块是python 垃圾收集器模块, gc使用标记清楚算法回收垃圾
objgraph 诊断内存问题工具
1、 在服务程序循环逻辑中,选择诊断点
2、 在诊断点,插入如下诊断语句
```python
import gc

import objgraph 

### 强制进行垃圾回收 

gc.collect() 

### 打印出对象数目最多的 50 个类型信息 

objgraph.show_most_common_types(limit=50) 
```

4、 检查统计信息,找到异常对象

运行加入诊断语句的服务程序,并将打印到屏幕上的统计信息重定向到日志中。 
运行一段时间后,就可以来分析日志,看看哪些对象在不停的增长。
比如,排查结果可能是:

  一个多线程程序,多个线程作为生产者,一个线程作为消费者,通过将一个 tuple 对象送入异步队列进行通信。 
  由于消费者的处理速度跟不上生产者的速度,又没有进行同步, 导致异步队列中的对象越来越多。

二、内存溢出

1、内存溢出原因

a 内存中加载的数据量过于庞大,如一次从数据库取出过多数据

b 集合类中有对对象的引用,使用完后未清空,产生了堆积,使得JVM不能回收

c 代码中存在死循环或循环产生过多重复的对象实体

d 使用的第三方软件中的BUG

e 启动参数内存值设定的过小

2、内存溢出的解决方案

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

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

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

第四步,使用内存查看工具动态查看内存使用情况

重点排查以下几点:

  a 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存, 

  就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,

  上线后,数据库中数据多了,一次查询就有可能引起内存溢出。 

  因此对于数据库查询尽量采用*分页的方式查询*。

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

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

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

三、内存泄漏和内存溢出的区别

内存溢出: 是指向JVM申请内存空间时没有足够的可用内存了,就会抛出OOM即内存溢出。

内存泄漏: 是指向JVM申请了一块内存空间,使用完后没有释放,由于没有释放,这块内存区域其他类加载的时候无法申请,

  同时当前类又没有这块内存空间的内存地址了也无法使用,相当于丢了一块内存,这就是内存泄漏。

值得注意的是内存泄漏最终会导致内存溢出,很好理解,内存丢了很多最后当然内存不够用了。