python的内存划分

1、python
python 在定义变量时,变量名和变量值都是需要存储的,分别对应内存的两块区域:堆区和栈区

1、变量名与变量值内存地址的关联关系存在于栈区
2、变量值存在堆区,内存管理回收的则是堆区的内容

定义变量 x=10 ,y=20
在这里插入图片描述
当我们执行x=y时,内存中的栈区与堆区变化如下
在这里插入图片描述

  • id()函数可返回对象的内存地址
    python中会为每个对象分配内存,哪怕他们的值完全相等。id(object)函数是返回对象object在其生命周期内位于内存中的地址,id函数的参数类型是一个对象。如下例子:c, d 和 2.0 地址不同,但值相等。
c = 2.0
d = 2.0
print(id(c),id(d),id(2.0))
print('c == d:',c==d)
print('c is d:',c is d)
#24E CB2C 83D8   ‭24E CB2C 83A8‬    2 5374 3939 6800‬
2537439396824 2537439396776 2537439396800
c == d: True
c is d: False
  • 为了提高内存利用效率对于一些简单的对象,如一些数值较小的int对象,字符串对象等,python采取重用对象内存的办法.

    (1)如指向a=2,b=2时,由于2作为简单的int类型且数值小,python不会两次为其分配内存,而是只分配一次,然后将a与b同时指向已分配的对象。

a = 2
b = 2
print(id(a),id(b),id(2))
print('a == b:',a==b)
print('a is b:',a is b)
1683941872 1683941872 1683941872
a == b: True
a is b: True
e = 'test'
f = 'test'
print(id(e),id(f),id('test'))
print('e == f:',e==f)
print('e is f:',e is f)
2537402630528 2537402630528 2537402630528
e == f: True
e is f: True

在这里插入图片描述

(2)如对于数值较大的int对象,python会为aa和bb分别申请一块内存,来存储1999839。

aa = 1999839
bb = 1999839
print(id(aa),id(bb),id(1999839))
print('aa == bb:',aa==bb)
print('aa is bb:',aa is bb)

2537439576464 2537439576368 2537438627472
aa == bb: True
aa is bb: False
  1. 内存分配理解

    x=3这句的执行过程并不是先获取x原来指向的对象的地址,再把内存中的值更改为3,而是新申请一段内存来存储对象3,再让x去指向对象3,所以两次id(x)的值不同。
    
x=2
print(id(2))
print(id(x))
x=3
print(id(3))
print(id(x))
 
1683941872
1683941872
1683941904
1683941904

3、改列表的值并不会改列表的地址

L=[1,2,3]
M=L
print(id(L))
print(id(M))
print(id(L[2]))
L[0]=2
print(id(L))
print(M)
 
2537439413000
2537439413000
1683941904
2537439413000
[2, 2, 3]

L[0] 和 L[1] 同时指向数值2,所以地址相同 (如果是2.0则地址不同)
在这里插入图片描述
在这里插入图片描述
3、直接引用和间接引用
直接引用是指从栈区出发直接引用到的内存地址
间接引用是从栈区出发引用到堆后,再进一步引用才能到达的内存地址

  • l2 = [20, 30] # 列表本身被变量名l2直接引用,包含的元素被列表间接引用
  • x = 10 # 值10被变量名x直接引用
  • l1 = [x, l2] # 列表本身被变量名l1直接引用,包含的元素被列表间接引用
    在这里插入图片描述
x = 10
print(id(x))
print("-------")

l2 = [20, 30]
l1 = [10, l2]
print(id(l1))
print(id(l1[0]))
print(id(l1[1]))
print("-----------")

print(id(l2))
print(id(l2[0]))
print(id(l2[1]))

print("---------")
a = 20
b = 30
print(id(a))
print(id(b))

5、垃圾回收机制原理分析
Python的GC模块主要运用了【引用计数】(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过【标记-清除】(mark and sweep)解决容器对象可能产生的循环引用的问题,并且通过【分代回收】(generation collection)以空间换取时间的方式来进一步提高垃圾回收的效率。
5.1、引用计数
引用计数就是:变量值被变量名关联的次数
age=18,变量值18被关联了一个变量名age,称之为引用计数为1
在这里插入图片描述
引用计数增加:m=age (把age的内存地址给了m,此时,m,age都关联了18,所以变量值18的引用计数为
在这里插入图片描述
引用计数减少:

age=10(名字age先与值18解除关联,再与10建立了关联,变量值18的引用计数为1)

del m(del的意思是解除变量名m与变量值18的关联关系,此时,变量18的引用计数为0)
在这里插入图片描述
5.2、引用计数的问题与解决方案
5.2.1、问题一:循环引用
引用计数机制存在着一个致命的弱点,即循环引用(也称交叉引用)
如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2

l1=[‘xxx’] # 列表1被引用一次,列表1的引用计数变为1
l2=[‘yyy’] # 列表2被引用一次,列表2的引用计数变为1
l1.append(l2) # 把列表2追加到l1中作为第二个元素,列表2的引用 计数变为2
l2.append(l1) # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2

# l1与l2之间有相互引用
# l1 = ['xxx'的内存地址,列表2的内存地址]
# l2 = ['yyy'的内存地址,列表1的内存地址]
>>> l1
['xxx', ['yyy', [...]]]
>>> l2
['yyy', ['xxx', [...]]]
>>> l1[1][1][0]
'xxx'

在这里插入图片描述
循环引用会导致:值不再被任何名字关联,但是值的引用计数并不会为0,应该被回收但不能被回收,什么意思呢?试想一下,请看如下操作

>>> del l1 # 列表1的引用计数减1,列表1的引用计数变为1
>>> del l2 # 列表2的引用计数减1,列表2的引用计数变为1

但此时两个列表的引用计数均不为0,但两个列表不再被任何其他对象关联,没有任何人可以再引用到它们,所以它俩占用内存空间应该被回收,但由于相互引用的存在,每一个对象的引用计数都不为0,因此这些对象所占用的内存永远不会被释放,所以循环引用是致命的,这与手动进行内存管理所产生的内存泄露毫无区别。所以Python引入了“标记-清除” 与“分代回收”来分别解决引用计数的循环引用与效率低的问题.
在这里插入图片描述
5.2.2、解决方案:标记-清除
容器对象(比如:list,set,dict,class,instance)都可以包含对其他对象的引用,所以都可能产生循环引用。而“标记-清除”计数就是为了解决循环引用的问题。

标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除

#1、标记
通俗地讲就是:
栈区相当于“根”,凡是从根出发可以访达(直接或间接引用)的,都称之为“有根之人”,
有根之人当活,无根之人当死。

具体地:标记的过程其实就是,遍历所有的GC【Garbage Collection】 Roots对象
(栈区中的所有内容或者线程都可以作为GC Roots对象),然后将所有GC Roots的对
象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被
清除。

#2、清除
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

基于上例的循环引用,当我们同时删除l1与l2时,会清理到栈区中l1与l2的内容以及直接引用关系
在这里插入图片描述
这样在启用标记清除算法时,从栈区出发,没有任何一条直接或间接引用可以访达l1与l2,即l1与l2成了“无根之人”,于是l1与l2都没有被标记为存活,二者会被清理掉,这样就解决了循环引用带来的内存泄漏问题。

循环引用内存溢出的例子:

class AAA(object):
    def __init__(self):
        print("object:born at:%s" % hex(id(self)))

    def __new__(cls, *args, **kwargs):
        print("new")
        return super(AAA, cls).__new__(cls)

    def __del__(self):
        print("bye bye")


def start():
    # 一次循环加两个对象引用,而且关闭了gc 会导致内存满

    while True:
        a = AAA()
        b = AAA()
        # 给a添加成员变量

        a.v = b  # 每执行一回多两个无法释放的对象,第一条线上的对象数会增加
        # 给b添加成员变量   gc会把同代 相互引用的给删除  剩下的到下一代

        b.v = a

        # 无法删除del
        del a
        del b
        print(gc.get_count())
        print(gc.get_threshold())  # 打印回收的标准,刚才的三条线  三条线上闲置的对象分别是多少
        time.sleep(0.1)


# gc.collect 手动垃圾回收
gc.disable()  # 垃圾回收机制一开,就不可能会有这么多闲置对象了, 三条线上都不会了
# gc.set_threshold(100, 5, 5)  # 设置垃圾回收机制  第一条线上到100个闲置对象就回收一次 筛选到第二条线上
start()
# 出生的多,删除的少,用引用计数就删不掉了
# 没有被回收的对象达到了700回收一次
# 把相互引用的都给删除了删除了后这一条上有剩余的,剩余的回到下一代
# 第二条线上的变多了后也开始清理 活下来的又去第三行
# 700个闲置对象删除一回
# 10个闲置对象删除一回

5.2.3 问题二:效率问题
基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。

5.2.4 解决方案:分代回收
分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc对其扫描的频率会降低,具体实现原理如下:

# 分代:指的是根据存活时间来为变量划分不同等级(也就是不同的代)
新定义的变量,放到新生代这个等级中:
    假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,
    当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),
    假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,
    接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低  

回收依然是使用引用计数作为回收的依据
在这里插入图片描述
虽然分代回收可以起到提升效率的效果,但也存在一定的缺点:
#例如一个变量刚刚从新生代移入青春代,该变量的绑定关系就解除了,该变量应该被回收,但青春代的扫描频率低于新生代,这就到导致了应该被回收的垃圾没有得到及时地清理。

没有十全十美的方案:
毫无疑问,如果没有分代回收,即引用计数机制一直不停地对所有变量进行全体扫描,可以更及时地清理掉垃圾占用的内存,但这种一直不停地对所有变量进行全体扫描的方式效率极低,所以我们只能将二者中和。

综上:
垃圾回收机制是在清理垃圾&释放内存的大背景下,允许分代回收以极小部分垃圾不会被及时释放为代价,以此换取引用计数整体扫描频率的降低,从而提升其性能,这是一种以空间换时间的解决方案

遇到的问题

1 把 list1 存到 list2 后,list1 用 clear 清空后,list2 也被清空了,但是用 [] 清空 list2 就没有被清空。

1、列表属于可变对象,当然列表,集合,字典都是可变对象。

  • 可变对象编辑:属于原地(原内存地址)操作
  • 可变对象新建:是产生新的对象,新的内存地址

编辑 list1 内存地址不变
在这里插入图片描述
新建list1、list2 内存地址不同
在这里插入图片描述
2、列表的 clear() 把原内存地址的 list1 清空了
在这里插入图片描述
3、列表的 [] 是新建了一个列表,原内存地址一直存在
在这里插入图片描述

  • 50
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当在Python程序处理大量数据时,可能会遇到内存溢出的问题。内存溢出意味着程序试图分配内存超过了系统所能提供的限制,导致程序无法正常执行。这种情况可能会发生在读取大文件、处理大型数据集或者运行需要大量内存的模型时。 解决内存溢出问题的方法有几种。首先,可以尝试使用分块读取的方式来读取大文件。通过将文件划分为多个较小的块,每次只读取一部分数据,可以避免一次性读取导致内存不足。使用pandas库的read_csv函数,并指定参数chunksize可以实现这一目的。例如,可以设置chunksize=10,以每次读取10行数据作为一个块进行处理。 其次,可以在处理模型时,使用torch.no_grad()上下文管理器来释放不需要的计算图节点和间结果,以减少内存占用。这样可以在验证和测试过程减少内存使用。 此外,通过使用try-except语句,可以在出现内存溢出错误时捕捉异常并采取相应的措施。例如,可以在except块使用torch.cuda.empty_cache()方法来释放GPU缓存内存。 最后,对于大文件,不建议使用readlines()函数将所有内容读取到一个大列表,因为这可能会导致内存不足。可以考虑按行迭代处理文件,以减少内存使用。 总结起来,解决Python内存溢出问题的方法包括分块读取大文件、释放不需要的内存、捕捉异常并释放GPU缓存等。根据具体情况选择适合自己的方法来解决内存溢出问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值