python基础之内存机制

python内存

对于内存的理解

对象的内存使用,对python而言,万物皆可对象:

  1. 赋值语句 ,引用和对象,在python中可以使用python内置函数id(),用于返回对象的身份
a = 1 #a引用指向对象1;
print(id(a)) #这里是十进制,返回4536868624
print(hex(id(a))) #这里是十六进制,0x10e6b1f10
  1. 引用和指针,当a和b同时指向一个int或str类型时,python会缓存这些对象,以便重复使用。当我们创建多个等于1的引用时,实际上是让这些引用指向同一个对象,举个栗子:
a = 1
b = 1

print(id(a))
print(id(b))#打印出的内存地址可见a,b指向同一个内存地址
  1. 校验是否指向同一个对象,为了检验两个引用是否指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同
a = 2
b = 2

#True
print(a is b)

#True
a = "objgigmsfs"
b = "objgigmsfs"
print(a is b)

#True
a = "very good morning,asf"
b = "very good morning,asf"
print(a is b)

#False
a = [1,2,3,4]
b = [1,2,3,4]
print(a is b)

#False
a = float(9)
b = float(9)
print(a is b)

上述总结:python中,整数和短小的字符指向内存地址相同,以便重复使用,而其他类型的内存地址均存到独立内存地址中,调用时也会调用不同的内存地址

引用计数

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。

我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。
因为调用函数的时候传入a,这会让a的引用计数+1
举个栗子:

from sys import getrefcount
ff = [1,2,3]
print(getrefcount(ff)) #返回引用计数为2
mm = ff #这里赋值后,均指向同一个对象(内存地址),下面会讲到
print(getrefcount(mm))#返回引用计数为3

上面对应返回的为2和3,而不是1和2,是因为在调用getrefcount(),是调用的它的镜像,所以它会把它自己的引用也会算进去

此时引用的对象[1,2,3]已经在python内存中了,可以使用python内置函数globals()方法查看
举个栗子:
在这里插入图片描述

增加引用计数

当对象创建并赋值(将它引用)给变量时,该对象当引用计数被设置为1。
举个栗子:

from sys import getrefcount
#对象被创建:
x = 3.14
print(getrefcount(x)) #返回引用计数为4
#另外的别名被创建:
y = x
print(getrefcount(y))#返回引用计数为5
#对象成为容器对象的一个元素:
myList = [123, x, 'xyz']#返回引用计数为6
print(getrefcount(x))

引用计数减少

对象的引用计数减少的情况:

一个本地引用离开了其作用范围。如fooc()函数结束时,func函数中的局部变量(全局变量不会)
对象的别名被显式销毁:del y
对象的一个别名被赋值给其他对象:x = 123
对象被从一个窗口对象中移除:myList.remove(x)
窗口对象本身被销毁:del myList
del语句,Del语句会删除对象的一个引用,它的语法如下:del obj[, obj2[, ...objN]]

例如,在上例中执行del y会产生两个结果:

从现在的名称空间中删除y
x的引用计数减1
举个栗子:

from sys import getrefcount
#对象被创建:
x = 3.14
print(getrefcount(x)) #返回引用计数为4
#另外的别名被创建:
y = x
print(getrefcount(y)) #返回引用计数为5
del y
print(getrefcount(x)) #返回引用计数为4

垃圾回收

垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。
当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数,当两者当差值高于某个阀值时,垃圾回收才会启动

可以通过gc模块的get_threshold()方法,查看该阈值
举个栗子:

import gc

print(gc.get_threshold())#返回(700, 10, 10),后面的两个10是与分代回收相关的阈值,后面会讲到。700即是垃圾回收启动的阈值。

可以通过gc中的set_threshold()方法重新设置,也可以手动进行垃圾回收

import gc

a=1
gc.collect()

分代回收

Python同时采用了分代(generation)回收的策略

这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率

python将所有对象分为0,1,2三代。所有新建对象都是0代对象。当某一代对象经历垃圾回收,依然存活,那么它就被归入下一代对象。
垃圾回收启动时,一定会扫描所有0代对象。如果0代对象经历过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历过一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描,即上面get_threshold()返回的(700, 10, 10)返回的两个10。也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收

举个栗子:

'''可使用set_threshold()来调整,比如对二代对象扫描更频繁'''
print(gc.get_threshold())#查看当前垃圾回收阀值
gc.set_threshold(700,10,5)#重新设定阀值
print(gc.get_threshold())#再查看

孤独的引用环

引用环的存在会对垃圾回收带来更大的困难,因为引用环可能构成无法使用,但引用计数不为0的对象

举个栗子:

a = []
b = [a]
a.append(b)

上面b引用a对象,a又通过python内置方法引用a,会造成a,b互相引用,这就会造成垃圾回收时,引用计数不会为0

析构函数

什么是析构函数?
del”就是一个析构函数了,当使用del 删除对象时,会调用他本身的析构函数,另外当对象在某个作用域中调用完毕,在跳出其作用域的同时析构函数也会被调用一次,这样可以用来释放内存空间。 
举个栗子:

class Test(object):
    def __init__(self, name):
        self.name = name
        print('这是构造函数')

    def say_hi(self):
        print('hell, %s' % self.name)

    def __del__(self):
        print('这是析构函数')

obj = Test("beijing")
obj.say_hi()

上面的__del__()也是可选的,如果不提供,则Python 会在后台提供默认析构函数

如果要显式的调用析构函数,可以使用del关键字
举个栗子:

#删除上面调用对象
del obj
print(getrefcount(obj)) #同del,删除对象,释放内存空间

Python内存管理规则:del的时候,把list的元素释放掉,把管理元素的大对象回收到py对象缓冲池里

posted @ 2021-05-12 18:33  happy-winds  阅读(144)  评论(0编辑  收藏  举报