跳至主要內容

内存管理

blacklad大约 6 分钟PythonPython

内存管理

一、介绍

内存简介

内存(RAM,随机存取存储器)是计算机用来存储数据和机器代码的硬件设备。它是一个易失性存储器,数据在计算机关闭时会丢失。在程序运行过程中,内存用于存储变量、对象

为什么需要内存管理

系统资源是有限的,程序运行过程中是会不停的创建对象、变量,占用内存。如果不进行管理一段时间后就会没有内存可以使用。

内存管理的主要目的是高效地利用内存资源,防止内存泄漏,避免内存碎片化,确保程序在内存资源有限的情况下能够稳定运行。

内存管理通过垃圾回收,即自动释放不再使用的内存空间,以防止程序占用过多的内存资源。

二、从赋值语句开始

在 Python 中,赋值语句将变量名绑定到对象,而不是复制对象本身。这种机制称为“引用计数”。

  • a 引用了列表对象 [1, 2, 3]
  • b 被赋值为 a,即 b 也引用了同一个列表对象 [1, 2, 3]
a = [1, 2, 3]
b = a

id()is

  1. id(object):返回对象的唯一标识符,即内存地址。
  2. is 运算符:用于判断两个对象是否是同一个对象(即内存地址是否相同)。
print(id(a))  # 输出对象 a 的内存地址
print(id(b))  # 输出对象 b 的内存地址
print(a is b) # 检查 a 和 b 是否引用同一对象
# 每次运行的内容在改变,但两个print的内容是相同的
140544253178336
140544253178336
True

可以看到 ab 虽然是两个变量,但他们引用了相同的地址,说明是同一个对象。

值传递与引用传递

函数的传递参数一般有两种主要的方式:值传递(pass by value)和引用传递(pass by reference)

  1. 值传递是指在函数调用时,将实参的值拷贝一份传递给形参。此时,形参和实参是两个独立的变量,修改形参不会影响实参。
  2. 引用传递是指在函数调用时,将实参的引用(即内存地址)传递给形参。此时,形参和实参引用的是同一个对象,修改形参会直接影响实参。

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

上述代码中,ab 互相引用,形成一个循环。Python 的垃圾回收器可以检测到这种情况,并在适当的时候回收这些对象。

四、内存管理机制

Python 使用内存池来管理小对象的分配和回收。

  • 小对象(小于 512 字节)由内存池管理器管理。
  • 大对象(大于 512 字节)直接从操作系统获取内存。

内存池将内存分块管理,以减少频繁的内存分配和释放操作,提高内存管理效率。

五、扩展

感兴趣的同学可以去了解其他语言的内存管理,如 Java。

也有语言并没有内存的管理,需要用户来进行内存的回收管理,如C++。

上次编辑于:
贡献者: blacklad