内存管理
内存管理
一、介绍
内存简介
内存(RAM,随机存取存储器)是计算机用来存储数据和机器代码的硬件设备。它是一个易失性存储器,数据在计算机关闭时会丢失。在程序运行过程中,内存用于存储变量、对象。
为什么需要内存管理
系统资源是有限的,程序运行过程中是会不停的创建对象、变量,占用内存。如果不进行管理一段时间后就会没有内存可以使用。
内存管理的主要目的是高效地利用内存资源,防止内存泄漏,避免内存碎片化,确保程序在内存资源有限的情况下能够稳定运行。
内存管理通过垃圾回收,即自动释放不再使用的内存空间,以防止程序占用过多的内存资源。
二、从赋值语句开始
在 Python 中,赋值语句将变量名绑定到对象,而不是复制对象本身。这种机制称为“引用计数”。
a
引用了列表对象[1, 2, 3]
。b
被赋值为a
,即b
也引用了同一个列表对象[1, 2, 3]
。
a = [1, 2, 3]
b = a
id()
和 is
id(object)
:返回对象的唯一标识符,即内存地址。is
运算符:用于判断两个对象是否是同一个对象(即内存地址是否相同)。
print(id(a)) # 输出对象 a 的内存地址
print(id(b)) # 输出对象 b 的内存地址
print(a is b) # 检查 a 和 b 是否引用同一对象
# 每次运行的内容在改变,但两个print的内容是相同的
140544253178336
140544253178336
True
可以看到 a
、b
虽然是两个变量,但他们引用了相同的地址,说明是同一个对象。
值传递与引用传递
函数的传递参数一般有两种主要的方式:值传递(pass by value)和引用传递(pass by reference)
- 值传递是指在函数调用时,将实参的值拷贝一份传递给形参。此时,形参和实参是两个独立的变量,修改形参不会影响实参。
- 引用传递是指在函数调用时,将实参的引用(即内存地址)传递给形参。此时,形参和实参引用的是同一个对象,修改形参会直接影响实参。
Python 中的参数传递机制可以说是“对象引用传递”。所有变量名在 Python 中实际上是对对象的引用。理解这一点有助于解释 Python 中的参数传递行为。
不可变对象(值传递行为)
对于不可变对象(如整数、字符串、元组),虽然看起来是值传递,但实际上是引用传递,但由于不可变性,任何修改都会导致创建新对象,因此表现出类似于值传递的行为。
def modify_number(n):
n = n + 1
print(f"Inside function: n = {n}")
num = 10
modify_number(num)
print(f"Outside function: num = {num}")
Inside function: n = 11
Outside function: num = 10
在函数内对变量的修改并不会影响函数外部的变量。
可变对象(引用传递行为)
对于可变对象(如列表、字典、集合),传递的是对象的引用,函数内部的修改会影响到原对象。
def modify_dict(d):
d['key'] = 'value'
print(f"Inside function: d = {d}")
my_dict = {}
modify_dict(my_dict)
print(f"Outside function: my_dict = {my_dict}")
Inside function: d = {'key': 'value'}
Outside function: my_dict = {'key': 'value'}
在函数内部给字典添加一个 key
,在函数外部的也能获取到。
默认参数值的问题
当一个函数的默认参数是一个可变对象(如列表)时,如果在函数调用中修改了该默认参数对象,那么在后续调用中,这个默认参数对象会保留修改后的状态。这是因为默认参数值在函数定义时被计算并存储一次,而不是在每次函数调用时重新计算。
def append_to_list(value, my_list=[]):
my_list.append(value)
return my_list
# 第一次调用,使用默认参数
print(append_to_list(1))
# 第二次调用,默认参数对象已经被修改
print(append_to_list(2))
# 第三次调用,默认参数对象再次被修改
print(append_to_list(3))
[1]
[1, 2]
[1, 2, 3]
my_list
是一个默认参数,每次调用 append_to_list
函数时,默认参数对象会被修改并保留。
避免可变对象默认参数值
为了避免这个问题,对可变对象的默认参数设置为 None
,并在函数内部创建一个新的对象。
def append_to_list(value, my_list=None):
if my_list is None:
my_list = []
my_list.append(value)
return my_list
# 每次调用时,都会创建一个新的列表
print(append_to_list(1))
print(append_to_list(2))
print(append_to_list(3))
[1]
[2]
[3]
通过将默认值设置为 None
,然后在函数内部检查并创建一个新的列表,我们确保每次调用函数时都会得到一个新的列表,而不是使用同一个默认列表对象。
三、 垃圾回收机制
Python 的垃圾回收机制包括引用计数和分代回收。
引用计数
每个对象都有一个引用计数器,记录有多少个引用指向它。当引用计数变为 0 时,该对象会被立即回收。
getrefcount
可以获取对象的引用
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 输出引用计数(初始值较高,因为传递给 getrefcount 函数时临时增加了引用)
b = a
print(sys.getrefcount(a)) # 引用计数增加
del b
print(sys.getrefcount(a)) # 引用计数减少
2
3
2
分代垃圾回收
Python 的垃圾回收器使用分代回收机制,将对象分为不同的“代”:
- 新生代(Generation 0)
- 青年代(Generation 1)
- 老年代(Generation 2)
新创建的对象首先被放入新生代。每一代都有自己的垃圾回收阈值,当超过该阈值时会触发垃圾回收。频繁回收新生代对象,因为这些对象通常很快变得不再需要。老年代对象回收频率较低,因为这些对象存活时间较长。
分代垃圾回收通过降低频繁对象扫描的次数来提高性能。每代回收时仅检查该代的对象,从而减少了扫描的开销。
循环垃圾回收
Python 的垃圾回收器能够处理循环引用,即对象之间相互引用导致的引用计数无法降到 0 的情况。例如:
class Node:
def __init__(self, value):
self.value = value
self.next = None
a = Node(1)
b = Node(2)
a.next = b
b.next = a
上述代码中,a
和 b
互相引用,形成一个循环。Python 的垃圾回收器可以检测到这种情况,并在适当的时候回收这些对象。
四、内存管理机制
Python 使用内存池来管理小对象的分配和回收。
- 小对象(小于 512 字节)由内存池管理器管理。
- 大对象(大于 512 字节)直接从操作系统获取内存。
内存池将内存分块管理,以减少频繁的内存分配和释放操作,提高内存管理效率。
五、扩展
感兴趣的同学可以去了解其他语言的内存管理,如 Java。
也有语言并没有内存的管理,需要用户来进行内存的回收管理,如C++。