垃圾回收及性能分析

常见的参数错误

概念说明

  • 这里通过一串代码阐述一个常见的关于可变对象和不可变对象的参数错误
  • 可变对象即为内部参数可以被重写的对象,例如列表、字典等
  • 不可变对象即内部参数不能被更改,例如字符串、整数、元组等
  • 当一个可变对象被重新赋值时,它的id地址并没有改变,而是value发生了变化
  • 而当一个不可变对象被重新赋值时,其实是重新创建了一个新的对象,并将新的value赋给了这个对象,即它的id地址发生了变化

实例展示

  • 下面通过一个实例,来展示可变对象和不可变对象的内部参数发生变化时的区别
def add(a,b):
    a += b
    return a

a = 1
b = 2
c = add(a,b)
print(c)       
print(a,b)      

a = [1,2]
b = [3,4]
c = add(a,b)
print(c)        
print(a,b)      

a = (1,2)
b = (3,4)
c = add(a,b)
print(c)        
print(a,b)   
  • 通过pycharm运行上述代码,得到的结果为在这里插入图片描述
  • 我们可以看出,由于整数和元组是不可变对象,当进行a+=b的操作时,其实是新建了一个对象,并返回,在函数调用时被c接收,故当类型为不可变对象时,c和a的值不同,其id也不同,而当为可变对象时,例如列表,a+=b只是对a的值进行了改变,并没有新建一个对象,因此c和a的值相等,id地址也相等,为同一个对象

内存的基本知识

内存的概念

  • 计算机存储器的作用来讲,存储器可以分为主存储器,辅助存储器和缓冲存储器。
  • 其中主存储器,也称主存、内存或可执行存储器,是与 CPU 直接进行信息交换的存储器,它的读写速度相对较快,容量相对较小,通常用来保存进程运行时的程序和相应数据以供 CPU 使用;
  • 而辅存不能与 CPU 直接进行信息交换,它的容量较大,读写速度相对较慢。例如硬盘、磁盘、光盘、软盘、U盘等。
  • 缓冲存储器常用于两个速度不同的部件之间,比如 CPU 和主存之间设置的高速缓冲存储 Cache(也可将内存的概念扩展为主存和高速缓冲存储器的合集)。
  • 内存(即主存)是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。内存(Memory)也被称为内存储器,其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。 内存是由内存芯片、电路板、金手指等部分组成的(来源于百度百科)。
  • 说白了内存就是用来存放CPU的临时运算数据。内存往细了分又有:堆、栈等,任何优秀程序中都有良好内存的划分。
  • 速度比较
    • CPU > 缓存 > 内存 > 辅存

内存管理

  • 由于内存的容量有限,很难承载系统以及用户进程所需的全部信息,所以操作系统需要对内存空间进行存储管理。在操作系统层面上内存管理的实现相当复杂,其大致功能包括内存空间的划分与动态分配、回收、扩充,以及存储保护,地址转换等等。

进程内的内存管理

  • 不单是操作系统需要对内存空间进行存储管理,我们同样需要对编写的程序所对应进程内的内存进行管理,从可用内存中申请内存并且具有足够内存来进行相关操作,以及在适当的时间释放内存,这些都是我们的程序和系统能够正常运行的前提。
  • 在 C / C++ 中,我们需要手动的进行内存管理,比如 C 语言中通过 malloc 和 free 函数来申请给定字节数的内存以及释放对应的内存。
  • 但在 Python 中,我们无须手动进行内存的申请和释放,Python 在内部帮我们完成了大量涉及到内存管理的操作,包括内存分配及垃圾回收。

内存的分配

内存池机制
  • 在 Python 中,有很多常用的数据结构,包括列表、字典等。比如在列表中,我们不仅可以保存其他不同类型的对象,而且可以非常方便的使用 append、extend 等方法对其进行动态的扩充。针对这些常用对象的一系列操作,会在 Python 中造成内存的频繁分配和释放,同时像 int、list 等 Python 对象的分配和释放通常涉及到的数据量相对较小,因此 Python 在内部引入了内存池机制,实现了小块内存的管理器(称为 PyMalloc )用于提高处理小块内存的效率,这样避免了在底层频繁的 malloc 和 free 操作对效率带来的影响。
分配策略
  • 在内存分配中,Python 以 512 bytes 为界限对大内存和小内存进行划分,不超过 512 bytes 的内存申请,会通过 PyMalloc 管理器进行处理,超过 512 bytes 的内存申请,则会通过 C 中的 malloc 来进行处理。在管理器的内部,主要包括 block、pool、arena 层级, 其中 block是 Python 内存管理中的最小单元,一个 pool 中包含多个 block,多个 pool 构成一个 arena。 同时,由于内存池机制,Python 并不会将释放的内存立即归还操作系统。
    缓冲池机制
缓冲池机制
  • 在内存池机制的基础之上,Python 为了提高常用对象的创建和释放效率,又进一步对整数、字符串等对象建立了对象缓冲池。比如对于 [-5, 256] 内的小整数,Python 已经在内部创建好了对应的小整数对象的缓冲池。

垃圾回收

垃圾回收机制

  • 我们知道,Python 程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数据量过大,内存空间管理不善就很容易出现 OOM(out of memory),俗称爆内存,程序可能被操作系统中止。
  • Python 中一切皆对象。因此,你所看到的一切变量,本质上都是对象的一个指针。
  • 那么,怎么知道一个对象,是否永远都不能被调用了呢?
    就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。

引用计数机制

  • 我们可以通过sys.getrefcount()这个函数,来了解Python内部的引用计数机制。
import sys

a = [1,2,3]

print(sys.getrefcount(a))  

在这里插入图片描述

  • 这里需要注意的是虽然a只是被赋值了一次,但结果为2,这是因为sys.getrefcount()这个函数本身也会引入一次计数

手动回收

  • 如果我们可以手动删除完对象的引用,然后强制调用gc.collect()清除没有引用的对象,其实也就是手动的启动对象的回收。

实例演示

  • 首先我们可以通过引入os模块和psutil模块,来具体的展示我们程序所消耗的内存。
  • OS模块
    与操作系统交互的库
  • psutil模块
    与系统交互的库,能够轻松实现获取系统运行的进程和系统利用率(包括CPU、内存、磁盘、网络等)信息。它主要用来做系统监控,性能分析,进程管理。
  • 通过以下代码检测程序在运行时内存的消耗
import os
import psutil

def show_info(start):
    pid = os.getpid()

    p = psutil.Process(pid)

    info = p.memory_full_info()

    memory = info.uss/1024./1024
    print(f"{start}一共占用{memory:.2f}MB")


def func():
    show_info("initial")
    a = [i for i in range(1000000)]   
    show_info("created")


func()
show_info("finished")


  • 运行结果如下在这里插入图片描述
  • 通过结果我们可以看到,在程序运行结束后,python自动启用了垃圾回收机制,将指针计数为0的对象删除,这里虽然在函数内部,变量a的引用计数并不是0,但它依然被python自动回收,原因如下:
    • 当a是局部变量时,在返回到函数调用处时,局部变量的引用会注销。这时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此之前占用的内存被收回了。
    • 当a是全局变量的时,即使函数体内代码执行完毕,返回到函数调用处时,对列表a的引用仍然是存在的,所以对象不会被垃圾回收,依然占有大量内存。
    • 为了证明上述观点,我们将上面代码里面的a,改为全局变量在这里插入图片描述
    • 其运行结果为:在这里插入图片描述
    • 通过显示引入计数我们也可以看到,将a定义为全局变量后,即使函数内部调用结束,依然存在对a的引用,因此并没有被python自动回收

循环引用

  • 如果有两个对象,它们互相引用,但不再被其他的变量引用,它们就该被垃圾回收,但由于它们之间互相引用,因此python并不会自动将其回收在这里插入图片描述
    图中a和b相互引用,其结果为在这里插入图片描述
    由于其互相引用,引用计算并不是0,因此python并没有自动将其回收,这是我们可以手动将其回收在这里插入图片描述
    运行结果为在这里插入图片描述

代码调试

  • 当我们在编写代码时,有时会出现返回一些错误,当代码很长,我们无法一下找出错误的所在,这是就需要进行代码调试

print调试

  • print调试顾名思义就是通过print语句打印来进行程序的调试,通过添加print语句逐步缩小出错范围在这里插入图片描述
    通过添加print语句,我们可以从结果中看到1和2成功打印,说明print(2)前面的代码没有问题,而3没有打印出来,故而可以将出错范围确定在d=c/0这一行

pdb调试

  • 首先,要启动pdb调试,只需在程序中加入import pdb和pdb.set_trace()即可
a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)

  • 运行上述代码,会出现下列结果在这里插入图片描述
    代码停在了pdb.set_trace的下面一行,接下来我们可以通过一些命令,控制程序的运行,进行代码的调试
  • 比如打印,语法是"p "在这里插入图片描述
  • 除了打印,常见的操作还有“n”,表示继续执行代码到下一行在这里插入图片描述
  • 除了这些还有l,表示列举出当前代码行上下11行的源代码,和s,表示进入相对应的代码内部,当然,除了这些常用命令,还有许多其他的命令可以使用
  • 具体可参考对应的官方文档:https://docs.python.org/3/library/pdb.html#module-pdb)

性能分析

cProfile进行性能分析

  • 除了要对程序进行调试,性能分析也是每个开发者的必备技能。
    日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟高,占用的资源多,但却不知道是哪里出了问题。
  • 这时,对代码进行 profile 就显得异常重要了。
  • 这里所谓的 profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。
  • 比如我们定义求阶乘的函数,并通过cProfile来对其进行性能分析在这里插入图片描述
    运行结果如下:在这里插入图片描述
  • 参数介绍:
    • ncalls:函数被调用的次数。如果这一列有两个值,就表示有递归调用,第二个值是原生调用次数,第一个值是总调用次数。
    • tottime:函数内部消耗的总时间。(可以帮助优化)
    • percall:是tottime除以ncalls,一个函数每次调用平均消耗时间。
    • cumtime:之前所有子函数消费时间的累计和。
    • filename:lineno(function):被分析函数所在文件名、行号、函数名。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值