Python是如何管理内存的?

共 3633字,需浏览 8分钟

 ·

2020-12-14 15:27

1. 内存管理关我屁事?

内存管理是指在程序的运行过程中,分配内容和回收内存的过程。如果只分配,不回收,电脑上那点内存很快就被用光。

好的程序能够高效的使用内存,不好的程序会造成过多的内存消耗,内存泄露,栈溢出,程序死翘翘。

幸运的是,Python和Java等高级语言会自动管理内存的分配和回收。

但程序员仍然必须具有一定内存管理知识!

小白需要内存管理知识避免犯低级错误,高手需要内容管理知识来优化程序性能,就像赛车手调教汽车的性能。

举个例子:生成包含1亿个随机字符串的序列,小白可能用list,而有点经验的会用generator。内存的使用效率可能差了一亿倍。

generator出现的一个重要原因就是省内存。

2. 可变数据类型和不可变数据类型

当我们调用函数的时候,我们需要传递参数,这时候变量从一个函数传递到了另一个函数。这个传递过程发生了什么?

被传递的对象是被复制了一份呢?还是就是同一份呢?

我们得先理解变量的存储结构,尤其是list等容器类的存储。

看下面的代码:

name = '麦叔'
print(id(name))  #打印内存地址:140628480727248
name = '张三'
print(id(name))  #打印内存地址:140628480727056

当我们定义了一个变量name = '麦叔',在内存中有两个部分:

  • 一个是name这个变量名
  • 一个是真正存储'麦叔'这个字符串的对象

上面的代码在内存中的过程是这样的:

  1. 开始变量名name指向了“麦叔”。
  2. 后来变量名name指向了“张三”。

注意:这时候“麦叔”不再有变量使用了,可能会被垃圾回收器销毁掉。

对于一个复杂的数据类型,比如list,原理是相同的但是更复杂:

cities = ['北京''上海''广州''深圳']
print(id(cities))
cities[3]='雄安'
print(id(cities))

cities一直指向内存中的列表对象,列表中的值改变不会改变cities在内存中的地址。改变的是列表指向的另外一个对象的地址。

这里的要点:

  • name等字符串是不可变的,当变量的值变化了,实际上生成了一个新的变量,内存地址变化了。

    基本数据类型都是不可变的,还有整数,小数等都是不可变。当一个变量的值发生了变化,实际上是创建了一个新的对象。

  • 而cities是个list,是可变的,里面的值会发生变化,内存地址没有发生变化。可变还包括dict等。

3.参数传递

理解了变量的内存存储结构,来看参数传递的问题,在函数调用过程中,参数传递到底是传递了什么?是复制了一份吗?

简单说:函数参数和返回值的传递都是传递的变量指向的内存地址

import random
name = '麦叔'
print(f'name的地址:{id(name)}')
 
def shuai_score(person):
    print(f'name的地址:{id(person)}')
    score = random.randint(110)
    print(f'score的地址:{id(score)}')
    return score
 
mscore = shuai_score(name)
print(f'mscore的地址:{id(mscore)}')

对于普通对象(不可变类型)和容器类变量可能看起来效果不一样,但实际上原理是一样的。如下面的代码:

  • 全局变量name指向张三,这一直都没有变过。
  • 局部变量name本来是指向张三的,但是后来指向了李四,这并不影响全局变量还是指向张三。
name = 'zhangsan'
 
def hello(name):
    print('传进来的是:'+ name)
    name = 'lisi'
    print('name被改成了' + name)
 
print(name)

对于列表,改变列表的值会影响全局变量。

cities = ['北京''上海''广州''深圳']
print(f'全局cities的地址:{id(cities)}')
 
def change_city(cities):
    print(f'局部cities的地址:{id(cities)}')
    cities[0] = '雄安'
    print(f'局部cities的地址:{id(cities)}')
 
change_city(cities)
print(cities)
print(f'全局cities的地址:{id(cities)}')

4. 引用次数和垃圾回收器

上面的知识和内存管理有什么关系?当然有!

内存管理的基本原理:回收掉没用的内存!

怎么判定有用没用呢:如果还有变量在使用就不要回收,没变量使用了就干掉它。

好可怕,做个打工人也一样吧?如果你还有用就继续打工,没用了就干掉。

上面的例子中:'麦叔'这个对象没用了,因为变量name指向了新的对象'张三'。

Python用引用次数,英文叫做reference count来表示有几个变量在使用这个对象。

通过一个叫做垃圾回收器(英文是Garbage Collector)的后台线程定期查看是否有些变量的引用次数为0,并清理掉引用次数为0的对象。

引用次数是如何产生的?

  • 当对象被赋值给新的变量,引用次数就会增加
  • 当变量作为参数传递给其他函数,引用次数就会增加

引用次数如何减少?

  • 当函数执行结束,引用对象的变量不再有效,引用次数减少

我们可以通过sys.getrefcount查看一个对象的引用次数:

import sys
name = '麦叔'
print(sys.getrefcount(name)) #打印4

上面的代码会打印出4次!

这是怎么回事?明明只有一次啊!这4个引用是:

  1. name变量
  2. getrefcount:当name被传递给getrefcount函数的时候,函数的参数也指向了它。
  3. Python解释器:为了执行这个脚本,Python解释器也保留了一个引用,直到脚本结束。只针对脚本全局变量。
  4. 编译优化器:当执行脚本的时候,优化器会尝试优化字节码,所以也产生了一次引用。这个引用是临时的,很快就会消失。

如果不在脚本中运行,直接在交互式Python下运行,refcount是2,因为没有后面两个引用:

>>> import sys
>>> name = '麦叔'
>>> print(sys.getrefcount(name))  #打印2

再来看一段代码:

import sys
name = '麦叔'
print(sys.getrefcount(name))  #打印4
name2 = name
print(sys.getrefcount(name))  #打印5
name3 = name 
print(sys.getrefcount(name))  #打印6

因为name2和name3都指向了'麦叔',所以引用次数不断增加。

注意:getrefcount函数执行完,它产生的引用就消失了,所以不会因为调用了3次而增加3个。

5. 手工回收

正常情况下,回收垃圾这事儿都是Python的垃圾回收器的活。

就像赛车手要自己调教汽车,必要的时候我们也可以自己动手。

import sys, gc

# 这个函数创建一个自己指向自己的列表
def create_cycle():
    list = [8910]
    list.append(list)

print("创建垃圾...")
for i in range(8):
    create_cycle()

print("我们来强制回收...")
n = gc.collect()
print("清理掉的无头尸体:", n)
print("没清理的垃圾:", gc.garbage)

执行结果如下:

创建垃圾...
因为与引用,所以不会被自动回收...
我们来强制回收...
清理掉的无头尸体: 8
没清理的垃圾: []

运送垃圾的车有自己的时间点,比如每天早上6点。但如果垃圾太多了,也可以打电话让他们马上来过清理垃圾。

垃圾回收器有它自己的运行节奏。我们可以调用gc.collect()让它马上执行回收操作。

6. 要点

  1. 知道可变数据类型,不可变数据类型,以及参数传递原理
  2. 理解list等可变数据类型作为参数传递后修改的是同一个对象
  3. 合理使用对象,避免占用太多内存,比如大数据情况下使用generator而不是list
  4. 避免变量循环引用,造成引用数永远不为零,可能造成不可回收而引起内存泄露。

今天这些对大部分人可能够用了。其实内存管理和垃圾回收是比较高级的话题,属于编程的深水区。有兴趣的建议多研究一下,深水区里去游一下。

浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报