深入浅出:Python内存管理机制详解

文章目录

  • 一、什么是内存?
    • 1.1、RAM简介
    • 1.2、RAM容量
    • 1.3、查看电脑内存
    • 1.4、监控电脑内存
  • 二、RAM是CPU的主内存,显存是GPU的专用内存
  • 三、内存管理
    • 3.0、不同数据类型的内存范围
    • 3.1、python是如何分配内存的?
    • 3.2、python采用自动内存管理机制
    • 3.3、python自动内存管理机制的缺点
    • 3.4、python内存优化的方法
  • 四、项目实战
    • 4.1、查看对象的引用计数
    • 4.2、内存池:设置垃圾回收的第 i 代阈值
    • 4.3、获取系统内存 + 获取进程(实际内存 + 峰值内存)
    • 4.4、手动释放内存

一、什么是内存?

1.1、RAM简介

随机存取存储器(Random Access Memory,RAM):是计算机中用于临时存储数据的一种硬件组件。它是计算机的主要内存之一,用于存储正在运行的程序和操作系统所需的数据。

主要特点:

  • 临时存储:RAM 存储的数据是临时的,意味着当计算机关闭或重启时,其中的数据会被清空。这与永久存储设备(如硬盘驱动器)不同,后者可以长期保存数据。
  • 随机存取:RAM 具备随机访问能力,这意味着它可以快速访问存储中的任何数据,而无需按照特定的顺序来读取。这使得 RAM 非常适合用作计算机的工作内存,以快速存取和处理数据。
  • 高速存储:RAM 是一种高速存储设备,数据可以在毫秒或甚至纳秒级别的时间内被读取或写入。这使得计算机能够快速执行任务,提高性能。
  • 容量较小:RAM 的容量通常相对较小,通常以兆字节(MB)或千兆字节(GB)来衡量。计算机的 RAM 容量会影响其多任务处理能力和运行大型程序的性能。

1.2、RAM容量

RAM的容量:表示计算机(当前 / 单次)能够同时加载和计算的数据和程序的总内存上限例如:当前计算机具有64GB的RAM,那么可以在同一时刻加载和运行的数据和程序总共不能超过64GB。否则将导致(RAM)内存不足,并且可能会导致系统变得非常慢甚至系统崩溃。

内存的使用:

  • 操作系统:操作系统需要占用一部分RAM,以便运行系统的核心功能。
  • 正在运行的应用程序:每个打开的应用程序需要分配一部分RAM,以存储其数据和代码。更大的应用程序和多任务处理可能需要更多的RAM。
  • 正在处理的数据:如打开的文档、图像、视频或音频文件,都需要RAM来存储。
  • 缓存和临时数据:操作系统和应用程序通常使用RAM来加速数据的读取和写入,因此一些RAM也会被用于缓存和临时存储。
    当超过RAM容量的数据和程序被加载时,计算机性能会受到影响,因为它将不得不频繁地从硬盘或固态硬盘等永久存储设备中读取数据,这通常较慢,导致性能下降。

1.3、查看电脑内存

1.4、监控电脑内存

当前内存的使用情况

当前内存的使用占比

二、RAM是CPU的主内存,显存是GPU的专用内存

RAM(随机存取存储器)是中央处理器CPU的主内存:用于临时存储正在运行的程序和操作系统使用的数据。它是一种易失性存储器,意味着当计算机关闭时,其中存储的数据会丢失。

  • 功能:CPU通过RAM读取数据和指令来支持计算机运行时的各种任务。
  • 性能:RAM的速度和容量对CPU的性能至关重要。较大且更快的RAM能够更有效地提供所需的数据,从而减少CPU等待数据的时间。

显存(Graphics RAM)是图形处理器GPU的专用内存:用于存储图形数据,如纹理、帧缓冲区、深度缓冲区等。

  • 功能:显存是GPU专用的高速内存,为了提供快速访问和高带宽而优化的。
  • 性能:显存的大小和类型(例如GDDR5、GDDR6等)直接影响了GPU处理大规模图形数据的能力。更大容量和更高带宽的显存通常意味着GPU能够处理更复杂的图形任务。
  • CPU(中央处理器):计算机系统的核心,负责执行系统任务,包括控制计算机的运行、执行算术和逻辑操作、管理系统资源等。CPU具有较少的核心(几个到数十个),适用于通用计算。
  • GPU(图形处理器):最初是为图形渲染而设计的,但由于其并行处理能力,它在科学计算、深度学习等领域也得到广泛应用。GPU具有大量的小型处理核心,适用于大规模并行计算。

协同工作:在深度学习中,CPU负责管理任务和调度工作,而GPU用于加速大规模矩阵运算,提高训练速度。

三、内存管理

3.0、不同数据类型的内存范围

在计算机内存中,图像内存的计算公式:内存大小 = 宽度 × 高度 × 通道数 x 每个像素的字节数内存的基本存储单元是字节而不是位,且是整数表示。

# 在计算机中,最小的存储单元是位(bit),而一个字节(Byte)通常由8个位组成。

1 TB = 1024 GB = [1024^2] MB = [1024^3] KB = [1024^4] Bytes = [1024^4 x 8] bits
位(bits) + 字节(Bytes) + 千字节(Kilobytes,KB) + 兆字节(Megabytes,MB) + 吉字节(Gigabytes,GB) + 千兆字节(Terabytes,TB) + PB + EB + ZB + YB
数据类型说明每个像素的字节数数据范围内存范围
bool布尔类型1位[0,1]2bits
int8有符号8位整数8位(1字节)[-128,127][-16,16]Bytes
uint8无符号8位整数8位(1字节)[0,255][0,32]Bytes
int16有符号16位整数16位(2字节)[-32768,32767][-32,32]KB
uint16无符号16位整数16位(2字节)[0,65535][0,64]KB
int32有符号32位整数32位(4字节)[-2,147,483,648,2,147,483,647][-2,2]GB
uint32无符号32位整数32位(4字节)[0,4,294,967,295][0,4]GB
int64有符号64位整数64位(8字节)[-9.22×1018,9.22×1018][-8,8]EB
uint64无符号64位整数64位(8字节)[0,18,446,744,073,709,551,615][0,16]EB

3.1、python是如何分配内存的?

Python内存管理器如何分配内存:根据对象的大小选择一块足够大的内存块,且将这块内存划分为两个部分,一个用于存储对象的数据,另一个用于存储对象的引用。
(1)在Python中,使用对象时自动分配内存,并当不再使用对象时自动释放内存。
(2)在Python中,对象都是动态类型(即声明变量时不需要指定数据类型,python会自动确定),且都是动态内存分配。

3.2、python采用自动内存管理机制

Python通过” 引用计数 “和” 循环引用检测 “来自动管理内存(即垃圾回收器)。因此在一般情况下,程序员不需要过多关注内存释放。只有在处理大型数据集或需要及时回收内存的特殊情况下,才需要考虑手动内存管理。

自动内存管理机制:又叫垃圾回收器(garbage collector,gc)负责定期地扫描并自动回收不再使用的内存和对象,使得开发者可以专注于程序逻辑,而不必担心内存管理问题。
(1)垃圾回收器:具体释放时机由解释器内部的策略控制,而不是由程序员明确控制的。
(2)垃圾回收器:不是严格按照预定的周期运行的,而是按照分代算法进行回收。

垃圾回收器的触发因素:

  • (1)手动垃圾回收采用gc.collect()进行手动强制执行垃圾回收,用来解决在内存敏感时(例如:内存不足)加速释放不再需要的内存
    • 1、Python采用自动垃圾回收机制,在后台定期自动检测不再被引用的对象并释放它们的内存。自动垃圾回收与gc.collect()相比,其会有一个等待期(定期检测)。
    • 2、在一般情况下,不需要手动使用 gc.collect(),因为Python的垃圾回收机制通常是足够智能的,会在合适的时候自动运行以释放不再需要的内存。
    • 3、gc.collect()是加速自动垃圾回收的执行速度,而不是立即释放内存(但也几乎等同)。
    • 4、gc.collect()本身也会引起额外的开销,故不建议频繁触发手动垃圾回收。
  • (2)引用计数(reference count)垃圾回收机制会记录每个对象被其他对象所引用的次数
    • 1、引用计数从0开始;
    • 2、当对象有新的引用指向它时,则引用次数 +1;当该对象指向它的引用失效时(如:del 对象),则引用 -1。
    • 3、当对象的引用计数为0时(如:对象=None),则列入垃圾回收队列,等待自动垃圾回收,而不是立即释放内存。
      • 【del 对象】: 使用del语句将对象从命名空间中删除,内存将被立即释放,但Python的内存池机制导致不会立即释放内存给计算机,而是等待复用。
      • 【对象=None】: 将对象设置为None,该对象的引用计数变为零,但不会立即释放,而是等待自动垃圾回收机制进行内存释放。
  • (3)循环引用检测若对象之间存在相互引用,则对象间将形成一个环状结构,使得引用计数不会降为零,因此内存无法被自动回收,导致内存泄漏。 分代回收
    • 1、引用链:用于跟踪对象之间的引用关系。当引用计数不为零,但对象之间形成循环引用时。
    • 2、分代回收(generation)将所有对象分为0,1,2三代。所有新创建的对象都是第0代对象;当某一代对象经过垃圾回收后仍然存活,就会升级到下一代。
      • 11、垃圾回收启动时,一定会扫描所有的0代对象。
      • 22、如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。
      • 33、当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
  • (4)内存池用于提高小内存对象的内存分配和释放效率。若频繁的进行小内存对象的分配和释放,可能导致内存碎片化和性能下降。
    • 1、预分配固定大小的内存块:Python会根据对象的大小选择一个合适的内存块。每个内存块包含多个相同大小的小块。通常以8字节为一块。
    • 2、内存块状态:内存块可以有不同的状态,包括空闲、已分配和已释放。Python会维护内存块的状态。
    • 2、对象复用:如果内存块中包含已释放的小块,Python会首先复用这些小块以减少内存分配开销。这意味着相同大小的内存块可以多次分配和释放,而不需要每次都与操作系统进行交互。
    • 3、延迟释放:对于不再使用的内存块不会立即释放回操作系统,而是将其保留在内存池中,以备将来再次使用(对象复用)。
      优点有助于减少频繁的内存分配和释放带来的性能开销。
      缺点可能导致内存泄漏,因为不再使用的内存块不会立即被操作系统回收。因此,开发者应避免长期保留对不再使用的对象的引用,以避免内存泄漏。

内存池机制(类别于金字塔模型): 图形化理解内存池

  • 第-1层,第-2层:由操作系统特定的虚拟内存管理器控制(OS-specific virtual memory manger(VMM))
    (第 -1 层):内核动态存储分配和管理
    (第 -2 层):物理内存(ROM / RAM) + 二级存储

    • ROM(只读存储器,Read-Only Memory):用于存储计算机或其他电子设备的固件和固定数据的存储设备。常用于存储计算机的引导程序(BIOS)。与RAM不同,ROM中的数据通常无法被修改。
    • RAM(随机访问存储器,Random Access Memory):用于存储正在运行的程序和数据的临时内存存储设备。常用于计算机快速读取和写入数据。RAM是易失性的,断电时数据会丢失。
    • 二级存储(交换,Secondary Storage):指非易失性的大容量存储设备,如硬盘驱动器(HDD)或固态驱动器(SSD)。它用于长期存储数据、文件和操作系统。与RAM不同,二级存储的数据在断电时不会丢失。
  • 第0层:由C标准库中底层分配器(underlying general-purpose allocator)的malloc、free进行内存分配和内存释放;

  • 第1层当申请的内存 >256KB 时,内存分配由 Python 原生的内存分配器(raw memory allocator)进行分配,本质上是调用C标准库中的malloc、realloc等函数。

  • 第2层当申请的内存 <256KB 时,内存分配由 Python 对象分配器(object allocator)实施。

  • 第3层:用户使用对象的直接操作层。特点:对于python内置对象(如:int、dict、list、string等),每个数据类型都有独立的私有内存池,对象之间的内存池不共享。如:int释放的内存,不会被分配给float使用。

3.3、python自动内存管理机制的缺点

  • (1)内存泄漏程序在分配内存后,无法正常释放不再使用的内存。最终可能导致程序运行变得缓慢或崩溃。 常见的几种情况如下:
    • 如果程序分配了内存,但未在不再需要时释放它,内存将泄漏。
    • 如果数据结构设计不正确,可能在不再需要时保留对对象的引用,导致内存泄漏。
    • 如果循环引用或不正确的引用计数,可能发生内存泄漏。
    • 如果打开文件却未在使用后正确关闭它们,内存将泄漏。
  • (2)性能下降:需要同时分配与释放内存,程序可能会变慢。
  • (3)内存不足:当程序需要的内存大于系统空间的内存,则会出现内存不足的问题。

3.4、python内存优化的方法

【python如何优化内存1】 + 【python如何优化内存2】

  • 降低全局变量的使用率:全局变量会一直存在到程序结束,因此会始终占用内存。若非必要,请尽可能地使用局部变量,并在不需要时尽快将其释放。
  • 避免创建非必要的对象:在Python中,创建对象是分配内存的一种方式。因此,尽量避免创建不必要的对象,并通过复用对象的方式来减少内存分配的次数。
  • 手动释放非必要的对象采用gc.collect()进行手动强制执行垃圾回收,用来在内存敏感时(例如:内存不足)立即释放不再需要的内存

四、项目实战

4.1、查看对象的引用计数

import sys

def create_objects():
    obj1 = [1, 2, 3]			    # 创建对象      (obj1对象的引用次数=2)
    obj2 = [obj1, 1]		        # 创建对象      (obj2对象的引用次数=1)
    obj3 = {'a': 1, 'b': 2}		    # 创建对象      (obj3对象的引用次数=1)
    print(sys.getrefcount(obj1))  # 获取对象a的引用次数
    print(sys.getrefcount(obj2))  # 获取对象a的引用次数
    print(sys.getrefcount(obj3))  # 获取对象a的引用次数
    #########################################################################
    obj1 = None  		# 将不再使用对象的引用设置为None     (obj2对象的引用次数=0)
    del obj2  		    # 将不再使用对象的引用设置为None     (obj1对象的引用次数=0)
    print(sys.getrefcount(obj1))  # 获取对象a的引用次数
    print(sys.getrefcount(obj3))  # 获取对象a的引用次数
    return

create_objects()  # 创建对象

"""###################################################################
# 函数:sys.getrefcount(a): 返回对象a的引用计数。
# 注意: 函数内部会增加一次临时引用计数来获取对象的引用数,但该函数执行之后会自动减去临时引用计数,以保持对象的引用计数不变。
# 
# 【del 对象】:	(1)使用del语句将对象从命名空间中删除,该对象的内存将被立即释放;
# 				(2)但Python的内存池机制导致该部分占用的内存不会立即释放给计算机,而是等待复用。
# 【对象=None】:	(1)将对象设置为None,该对象的引用计数变为零;
# 				(2)但内存不会立即释放,而是等待自动垃圾回收机制进行内存释放。
###################################################################"""

4.2、内存池:设置垃圾回收的第 i 代阈值

import gc
gc.set_threshold(700, 10, 5)

"""###################################################################
# 函数功能:设置垃圾回收的第i代阈值。
# 函数简介:gc.set_threshold(threshold0, threshold1, threshold2)
# 输入参数:    
#            threshold0      是垃圾回收的第0代阈值。当0代的垃圾数量达到这个值时,触发0代的垃圾回收。
#            threshold1      是垃圾回收的第1代阈值。当0代的垃圾数量达到 threshold0,且0和1两代垃圾的总数达到 threshold1,则触发两代的垃圾回收。
#            threshold2      是垃圾回收的第2代阈值。当0和1两代垃圾的总数达到 threshold1,且同时0/1/2三代垃圾的总数达到 threshold2,则触发三代的垃圾回收。
###################################################################"""

4.3、获取系统内存 + 获取进程(实际内存 + 峰值内存)

def memory_usage():
    import psutil

    # (1)获取系统内存信息
    mem_info = psutil.virtual_memory()
    total_memory = mem_info.total / (1024 ** 3)  # 总内存大小(字节 - GB)
    used_memory = mem_info.used / (1024 ** 3)  # 已使用内存(字节 - GB)
    free_memory = mem_info.available / (1024 ** 3)  # 空闲的内存(字节 - GB)
    print(f"系统总内存RAM: {total_memory} GB")
    print(f"系统已占用内存: {used_memory} GB")
    print(f"系统未占用内存: {free_memory} GB")
    print("*" * 50)

    # (2)获取进程的内存信息
    process = psutil.Process()  # 创建一个进程对象
    mem_info = process.memory_info()  # 获取当前进程在RAM中的内存使用量
    memory_usage = mem_info.rss / (1024 ** 3)  # 表示进程在当前时刻的实际内存使用情况(字节 - GB)
    peak_memory = mem_info.peak_wset / (1024 ** 3)  # 表示进程在任意时间点的内存使用的峰值(字节 - GB)
    print(f"当前进程实际占用内存: {memory_usage_mb:.8f} GB")
    print(f"当前进程最大占用内存: {peak_memory_mb:.8f} GB")

    return memory_usage, peak_memory


if __name__ == "__main__":
    memory_usage()  # 创建对象

"""
系统总内存RAM: 63.74748229980469 GB
系统已占用内存: 8.997417449951172 GB
系统未占用内存: 54.750064849853516 GB
**************************************************
当前进程实际占用内存: 0.01511765 GB
当前进程最大占用内存: 0.01512146 GB
"""

4.4、手动释放内存

"""########################################################################
# 函数: gc.collect(): 手动垃圾回收管理
# 功能:
#     若使用gc.collect():	(1)不能保证立即释放内存,但可以加速自动垃圾回收的执行速度。
#                           (2)其本身也会引起额外的开销,不建议频繁触发手动垃圾回收。
#     若不使用gc.collect():	垃圾回收器是自动定期检测并回收内存,但有一定延迟(定期)。
########################################################################
# 【del 对象】:	(1)使用del语句将对象从命名空间中删除,该对象的内存将被立即释放;
# 				(2)但Python的内存池机制导致该部分占用的内存不会立即释放给计算机,而是等待复用。
# 【对象=None】:	(1)将对象设置为None,该对象的引用计数变为零;
# 				(2)但内存不会立即释放,而是等待自动垃圾回收机制进行内存释放。
########################################################################"""
import gc
import numpy as np


def memory_usage():
    """用于获取当前程序的内存占用情况(单位:MB)"""
    import psutil
    process = psutil.Process()  # 创建一个进程对象,用于获取当前程序的内存信息。
    mem_info = process.memory_info()  # 获取当前进程在RAM中的内存使用量
    memory_usage = mem_info.rss / (1024 ** 2)  # 表示进程在当前时刻的实际内存使用情况(字节 - MB)
    peak_memory = mem_info.peak_wset / (1024 ** 2)  # 表示进程在任意时间点的内存使用的峰值(字节 - MB)
    return memory_usage, peak_memory
    # 1 TB = 1024 GB = [1024^2] MB = [1024^3] KB = [1024^4] Bytes = [1024^4 x 8] bits


if __name__ == "__main__":
    # (1)查看系统初始的内存使用情况
    system_memory, peak_memory = memory_usage()
    print(f"系统初始的内存使用情况(MB): {system_memory:.2f}")
    ######################################################
    # (2)创建一个数组
    array = np.random.randint(0, 100, size=(400, 500, 600))
    print(f"总内存使用情况(MB): {memory_usage()[0]:.2f}, {memory_usage()[1]:.2f}")
    ######################################################
    # (3)查看系统进程的内存使用情况
    array[array <= 2800] = 0  # 灰度强度滤波
    print(f"总内存使用情况(MB): {memory_usage()[0]:.2f}, {memory_usage()[1]:.2f}")
    ######################################################
    # (4)查看(手动垃圾回收)系统进程的内存使用情况
    array = None
    gc.collect()  # 手动垃圾回收:加速自动垃圾回收
    print(f"总内存使用情况(MB): {memory_usage()[0]:.2f}, {memory_usage()[1]:.2f}")

    """
    系统初始的内存使用情况(MB): 29.73
    总内存使用情况(MB): 487.60, 487.61
    总内存使用情况(MB): 487.61, 602.06
    总内存使用情况(MB): 29.85, 602.06
    """

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
上一篇 2023年12月12日
下一篇 2023年12月12日

相关推荐