python外篇(内存泄露)

Posted Mr.Joden

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python外篇(内存泄露)相关的知识,希望对你有一定的参考价值。

目录

了解

循环引用造成的内存泄露

大量创建对象造成的内存泄漏

全局对象造成的内存泄露

不适当缓存造成的内存泄露

内存分析工具 


了解

### 以下为Python中可能会出现内存泄露的情况:
    (1) 循环引用:当两个或多个对象相互引用,造成的循环引用进而导致内存泄露
    (2) 大量创建对象:当程序中频繁创建大量的对象并没有及时销毁,也会导致内存泄露。
    (3) 全局变量:当全局变量被创建后一直存在,即使它们不再被使用,也会占用内存空间,可能导致内存泄露。
    (4) 不适当的缓存使用:如果在缓存中存储了大量数据,而没有适当地删除旧数据,会导致内存泄漏。
    (5) C扩展模块:使用C扩展模块编写的代码可能会出现内存泄漏问题,因为C扩展代码不受Python的垃圾回收机制控制。


    

### 以下为避免这些情况导致的内存泄漏的措施:
    (1) 尽可能避免循环引用,使用weakref库避免对象之间相互引用。
    (2) 及时销毁不需要的对象,特别是大量创建的对象。
    (3) 减少全局变量的使用,只在必要时使用。
    (4) 注意缓存的使用,需要定期清理缓存,避免缓存数据过多导致内存泄漏。
    (5) 尽可能避免使用C扩展模块,如果必须使用,需要仔细检查代码,确保它没有导致内存泄漏问题。
    (6) 使用生成器和迭代器:生成器和迭代器是Python中用于处理大型数据集合的高效工具。它们可以逐个返回数据,而不是一次性返回整个数据集合。使用生成器和迭代器可以减少对内存的需求,避免内存泄漏。
    (7) 使用with语句:with语句是Python中用于管理资源的一种语法,它可以自动关闭文件和数据库连接等资源,避免资源未被正确释放导致的内存泄漏。
    (8) 使用内存分析工具:Python中有一些内存分析工具可以帮助我们检测和解决内存泄漏问题,例如pympler和memory_profiler等工具。使用这些工具可以定位内存泄漏的代码,并进行优化。

循环引用造成的内存泄露

### 介绍:    
    在python中,都知道当一个变量不被引用的时候(引用计数为0)就会触发垃圾回收机制从而被从内存中删除,但如果在两个对象间去互相引用(你引用我,我也引用你),这样就会形成一个无限循环引用(你引用我,我引用你,你又引用我,我又引用你...),那么此时两个对象一直处于被引用状态,意味着逃避了垃圾回收机制的回收,在内存中一直循环引用占用着内存资源,从而导致这些内存资源将不能被再次使用。这就是循环引用造成的内存泄露问题。
    
    
### 代码问题阐述:

class Person:
    def __init__(self, name):
        self.name = name
        self.cars = []

    def add_car(self, car):
        self.cars.append(car)
        car.owner = self


class Car:
    def __init__(self, kind):
        self.kind = kind
        self.owner = None


person = Person("Joden")
car = Car("Tesla")
person.add_car(car)
# car.owner = self
# self.cars = [car]

### 代码解决阐述:

(1) 以下为解决阐述:
    使用弱引用,弱引用不会增加一个变量或对象的引用计数
    
(2) 以下为解决代码阐述:

import weakref


class Person:
    def __init__(self, name):
        self.name = name
        self.cars = []

    def add_car(self, car):
        self.cars.append(weakref.ref(car))
        car.owner = weakref.ref(self)


class Car:
    def __init__(self, kind):
        self.kind = kind
        self.owner = None


person = Person("Joden")
car = Car("Tesla")
person.add_car(car)
# car.owner = weakref.ref(self)
# self.cars = [weakref.ref(car)]

大量创建对象造成的内存泄漏

### 介绍:
    当创建大量对象,并且不能及时被垃圾回收机制回收,从而导致的内存泄露问题
    
    
### 代码问题阐述:

class MyClass:
    def __init__(self, data):
        self.data = date


objs = []
for i in range(1000000):
    obj = MyClass(i)
    objs.append(obj)


    阐述:在这个示例中,我们构建了一个耗时的循环创建对象,并且不断将对象添加到列表中,这样在循环的过程中列表就无法被短时间内释放,就会在此期间一直占用内存,从而造成内存泄露问题
            
        
### 代码解决阐述:

class MyClass:
    def __init__(self, data):
        self.data = date


def create_objs(num):
    for i in range(num):
        obj = MyClass(i)
        yield obj


    阐述:在这个示例中,我们将创建对象过程放在了函数内并且用生成器来返回我们创建的对象,这样只有需要时才会去创建对象就避免了,大量创建对象造成的内存泄露问题

全局对象造成的内存泄露

### 介绍:
    在了解这个全局变量造成的内存泄露之前还需要了解两个知识:深拷贝和浅拷贝、全局变量。
    深拷贝和浅拷贝:在python中对基本数据类型的引用都是浅拷贝(实际上就是对内存id的引用,本质上为指向同一个内存地址的引用),而深拷贝就不一样了,比如对象、list、dict等等,它们被引用时就会开辟新的内存空间来存储这些数据。
    全局变量:很简单,创建一个py文件(模块),直接在py文件内定义的变量就是全局变量(而非在函数内等局部作用域中定义的变量)。
    那么,现在就很好理解全局变量造成的内存泄露问题了。当某些模块中的全局变量被程序中多次引用,那么每次引用就会重新开辟内存空间来存储,这样就会占用大量的内存空间从而造成内存泄露问题。
    对于如何解决这个问题,方案是单例模式,单例模式可以确保类在内存中只有一个实例然后在整个程序中都会对其引用,这样就避免了全局变量被多次在内存中创建从而导致的内存泄露问题。、

### 代码问题阐述:

my_list = [...]


    这个my_list在程序中被多次引用,而且倘若my_list占用较大空间

### 代码解决阐述:

class Singleton:
    __instance = None

    def __new__(cls, *args, **kwargs):
        if not cls.__instance:
            cls.__instance = super().__new__(cls)
            cls.__instance.my_list = []
        return cls.__instance


def add_data(new_data):
    obj = Singleton()
    obj.my_list.append(new_data)


def get_data():
    obj = Singleton()
    return obj.my_list


add_data(1)
add_data(2)
add_data(3)
get_list = get_data()

不适当缓存造成的内存泄露

### 了解:
    对于这个“不适当的缓存造成的内存泄露”是我们使用缓存原理要注意的,如果我们的缓存中数据量过大时就有一定的风险了,如果这个缓存在短时间内没被清除且一直变大就会占用越来越多的内存就有可能造成内存泄露,而且如果程序运行过程中突然崩溃强制关闭,此时内存中的数据也可能无法清理总而造成内存泄露。
    那么,对于解决“不适当的缓存造成的内存泄露”问题呢?其实本质上也是由长时间引用没被释放造成的,所以可以使用弱引用来解决。

### 问题代码阐述:

class Calculator:
    __cache = 

    @classmethod
    def add(cls, a, b):
        key = f"add_a_b"
        if key in cls.__cache:
            return cls.__cache[key]
        result = a + b
        cls.__cache[key] = result
        return result

    在这个示例中我们使用字典作为加法计算的缓存,如果已经计算过了就直接引用,否则将添加到缓存字典中,考虑假若它是我们python底层实现的“加法”,我们在一个大型项目程序中将会多少次调用它,所以如果不能及时释放缓存,缓存反而会弄巧成拙

### 解决代码阐述:

import weakref


class Calculator:
    __cache = weakref.WeakValueDictionary()

    @classmethod
    def add(cls, a, b):
        key = f"add_a_b"
        result = cls.__cache.get(key)
        if result is None:
            result = a + b
            cls.__cache[key] = result
        return result

内存分析工具 

### 工具介绍(需要自己去了解)  
    gc:python内置的内存分析模块
    objgraph:用于生成 Python 对象引用图,可以帮助我们找出引用计数不正确的对象。
    memory_profiler:用于逐行分析 Python 代码的内存使用情况,可以帮助我们找出内存占用过高的代码段。
    pympler:包含了多个子模块,可以帮助我们分析 Python 中的内存使用情况,包括对象分配、垃圾回收、内存泄露等。
    heapy:基于 Guppy,可以用于分析 Python 中的内存使用情况,包括内存分配情况、对象大小分布、内存泄露等。
    我们可以在程序中使用这些工具,来查看内存使用情况、对象数量等信息,以及跟踪内存泄露和内存占用过高的问题。具体使用方法可以参考对应工具的官方文档。

Python内存泄露问题

什么是内存泄露?

内存泄露是那些使用过后,应该被清理却没有被清理的内存一直占据着系统资源,通过长时间的累积导致系统崩溃。

Python的垃圾回收机制

1、引用计数

原理:当一个对象的引用被创建或者复制时,对象的引用计数+1,当一个对象的引用被销毁时,对象的引用计数-1,当对象的引用计数变为0时,就意味着已经没有再被使用了,可以将其内存释放掉。

优点:引用计数的优点是实时性,任何对象只要没有被引用就会被释放。

缺点:1.维护引用计数需要额外的操作,花费时间和空间。2.引用计数无法解决循环引用问题,当两个对象互相引用对方时,每个对象的计数都不是0,这些对象永远不会被引用计数机制回收。

2、标记-清除

标记清除只关注那些可能会产生循环引用的对象,一般都是容器对象,比如列表、字典、类等等。

过程:

1.当触发标记清除机制时,首先将所有对象及引用计数复制出一份副本,对副本进行以下操作

2.假设AB互相引用对方,遍历每个对象,如A,如果A中引用了B,则先将B的引用计数-1,然后去看B,如果B引用了A,则将A的引用计数也-1,这样就将对象的真实有效引用数暴露了出来。

3.遍历完成后,如果有效引用为0则说明需要回收。

缺点:效率低

3.分代回收

分代回收是用空间换时间,认为如果一个对象存活的时间越长越有可能不是垃圾,把那些在第一次垃圾回收后没有被清除的对象放入二代,减少标记清除检查的次数。一共分为三代。

以上是关于python外篇(内存泄露)的主要内容,如果未能解决你的问题,请参考以下文章

python爬虫番外篇进程,线程的初步了解

Python内存泄露问题

使用gcobjgraph干掉python内存泄露与循环引用!

如何解决python训练数据内存泄露问题?

python性能优化内存优化内存泄露;与其他语音比较效率如何?

python内存泄露的诊断(转)