文章目录
- 一、什么是内存?
- 1.1、RAM简介
- 1.2、RAM容量
- 1.3、查看电脑内存
- 1.4、监控电脑内存
- 二、内存管理
- 2.1、python是如何分配内存的?
- 2.2、python采用自动内存管理机制
- 2.3、python自动内存管理机制的缺点
- 2.4、python内存优化的方法
- 三、项目实战
- 3.1、查看对象的引用计数
- 3.2、内存池:设置垃圾回收的第 i 代阈值
- 3.3、手动释放内存 + 获取当前进程的内存占用情况
一、什么是内存?
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、监控电脑内存
当前内存的使用情况
当前内存的使用占比
二、内存管理
2.1、python是如何分配内存的?
Python如何分配内存:当Python需要分配内存时,它会根据对象的大小选择一块足够大的内存块。Python中的内存管理器会将这块内存划分为两个部分,一个用于存储对象数据,另一个用于存储对象的引用。
(1)在Python中,使用对象时自动分配内存,并当不再使用对象时自动释放内存。
(2)在Python中,对象都是动态类型(即声明变量时不需要指定数据类型,python会自动确定),且都是动态内存分配。
2.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使用。
2.3、python自动内存管理机制的缺点
- (1)内存泄漏:程序在分配内存后,无法正常释放不再使用的内存。最终可能导致程序运行变得缓慢或崩溃。 常见的几种情况如下:
- 如果程序分配了内存,但未在不再需要时释放它,内存将泄漏。
- 如果数据结构设计不正确,可能在不再需要时保留对对象的引用,导致内存泄漏。
- 如果循环引用或不正确的引用计数,可能发生内存泄漏。
- 如果打开文件却未在使用后正确关闭它们,内存将泄漏。
- (2)性能下降:需要同时分配与释放内存,程序可能会变慢。
- (3)内存不足:当程序需要的内存大于系统空间的内存,则会出现内存不足的问题。
2.4、python内存优化的方法
【python如何优化内存1】 + 【python如何优化内存2】
- 降低全局变量的使用率:全局变量会一直存在到程序结束,因此会始终占用内存。若非必要,请尽可能地使用局部变量,并在不需要时尽快将其释放。
- 避免创建非必要的对象:在Python中,创建对象是分配内存的一种方式。因此,尽量避免创建不必要的对象,并通过复用对象的方式来减少内存分配的次数。
- 手动释放非必要的对象:采用gc.collect()进行手动强制执行垃圾回收,用来在内存敏感时(例如:内存不足)立即释放不再需要的内存。
三、项目实战
3.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 对象】 使用del语句将对象从命名空间中删除,内存将被立即释放,但Python的内存池机制导致不会立即释放内存给计算机,而是等待复用。
【对象=None】 将对象设置为None,该对象的引用计数变为零,但不会立即释放,而是等待自动垃圾回收机制进行内存释放。
"""
3.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,则触发三代的垃圾回收。
"""
3.3、手动释放内存 + 获取当前进程的内存占用情况
- (1)无效对象的内存释放:通过自定义函数memory_usage对整个项目进行拆分查看,采用粗定位 + 精定位的方式锁定内存占用的原因,最后进行相关优化处理。
- (2)对象与函数间的内存释放:
1、变量的数据类型 + 函数的数据类型
2、变量与函数间的相互影响。比如:输入变量是int16,但是函数默认使用int64进行计算。虽然输出结果可能还是int16,但在进程计算时,占用的内存扩大到了4倍。
image_stack[np.where(image_stack <= gray_value)] = 0
##############################################################################
# 原因分析:若image_stack与gray_value类型不一致,则进程以最大类型为主进行内存占用。
# 参数如下:
# image_stack.dtype == uint16 # 表示一个16位无符号整数数据类型
# type(gray_value) == np.uint64 # 表示一个64位无符号整数数据类型
##############################################################################
# 方法1:保持gray_value与image_stack同类型
# gray_value = np.uint32(gray_value) # 指定整数的数据类型为int32
# 方法2:image_stack[image_stack <= gray_value] = 0 # 速度更快且可以解决占用内存问题
import time
import gc
import numpy as np
def memory_usage():
"""用于获取当前程序的内存占用情况(单位:MB)"""
import psutil
process = psutil.Process() # 创建一个 psutil 进程对象,用于获取当前程序的内存信息。
mem_info = process.memory_info() # 使用 memory_info 方法获取当前进程的内存信息。
memory_usage_mb = mem_info.rss / (1024 * 1024) # 计算当前程序的内存占用情况。rss表示进程使用的实际物理内存大小(单位:字节Bytes)
return memory_usage_mb
# 字节(Bytes) + 千字节(Kilobytes,KB) + 兆字节(Megabytes,MB) + 吉字节(Gigabytes,GB) + 千兆字节(Terabytes,TB)
# 1 TB = 1024 GB = [1024^2] MB = [1024^3] KB = [1024^4] Bytes
if __name__ == "__main__":
# (1)查看系统初始的内存占用情况
final_memory_usage1 = memory_usage()
print(f"总内存使用情况(MB): {final_memory_usage1:.2f}")
######################################################
# (2)创建一个形状为(3, 4)的包含0到100范围内随机整数的数组
low = 0
high = 20000
array_shape = (1000, 500, 800)
array = np.random.randint(low, high, size=array_shape)
final_memory_usage1 = memory_usage() # 获取整个程序运行期间的内存使用情况
print(f"总内存使用情况(MB): {final_memory_usage1:.2f}")
######################################################
# (3)查看系统进程的内存使用情况
array[array <= 2800] = 0 # 灰度强度滤波
final_memory_usage2 = memory_usage()
print(f"总内存使用情况(MB): {final_memory_usage2:.2f}")
######################################################
# (4)查看(手动垃圾回收)系统进程的内存使用情况
array = None
gc.collect() # 手动垃圾回收:加速自动垃圾回收
# time.sleep(2) # 自动垃圾回收:需要一定的周期
final_memory_usage3 = memory_usage()
print(f"总内存使用情况(MB): {final_memory_usage3:.2f}")
"""
总内存使用情况(MB): 31.90
总内存使用情况(MB): 1557.88
总内存使用情况(MB): 1557.89
总内存使用情况(MB): 32.01
"""
"""
################################################################################
【del 对象】: 使用del语句将对象从命名空间中删除,内存将被立即释放,但Python的内存池机制导致不会立即释放内存给计算机,而是等待复用。
【对象=None】: 将对象设置为None,该对象的引用计数变为零,但不会立即释放,而是等待自动垃圾回收机制进行内存释放。
################################################################################
函数: gc.collect(): 手动垃圾回收管理
功能:
若使用gc.collect(): (1)不能保证立即释放内存,但可以加速自动垃圾回收的执行速度。
(2)其本身也会引起额外的开销,不建议频繁触发手动垃圾回收。
若不使用gc.collect(): 垃圾回收器是自动定期检测并回收内存,但有一定延迟(定期)。
################################################################################
"""