1. 程序内存浅析
一个软件要运行,需要将数据加载到内存中,通过cpu进行内存数据的读写,完成数据的运算。
1.1内存的目的:
由于系统内存和 CPU 的资源非常有限,所以有效的分解软件中的各项数据,将不同的数据加载到不同的内存部分以有效的运行程序,同时可以达到在一个计算机中有效 运行更多软件的目的
1.2 python中内存的分配
Python程序在运行过程中,解释器会将系统分配的内存空间分为以下几个部分
栈内存区[stack]: 用于直接分配数据,存取速度较快,数据存储不稳定,适用于小数据块的快速存取,一般在程序中用于存储变量数据
方法区[data]:主要用于加载程序中使用的代码数据、二进制数据、方法数据等等程序运行需要的预加载数据
静态区[static]:主要用于加载存储程序中的一些静态数据、常量数据等等,在 PYTHON中的不可变数据类型的数据,也会存储在静态常量区内存中
堆内存[heap]:存储数据稳定持久,一般用于存储加载较为重量级的数据,如程序运行过程中的对象都是存在堆内存中的
1.3 程序中创建的变量及对象在内存中的表示
class Person:
'''自定义类型'''
pass
# 创建了两个对象
p = Person()
print(p)
运行结果如下
<main.Person object at 0x000001DB46018048>
8048是person()在堆内存区中的内存地址的首地址
Person()赋值给p ,相当于将自己的首地址存储于在栈内存储区的p中
1.4 不可变数据类型 VS 可变数据类型
PYTHON 中根据数据是否可以进行修改提供了两种不同的数据类型
⚫ 不可变数据类型:对象在内存地址中存储的数据可变
一般基本数据类型都是不可变数据类型
⚫ 可变数据类型:对象在内存地址中存储的数据不能变
一般组合数据类型或者自定义数据类都是可变数据类型
简单来说:
可变数据: 在内存不变的情况下,数据可以变化
不可变数据 : 在内存不变的情况下,数据无法改变
不可变数据若要改变变量内的数据,需要使内存地址指向新的地址,原来的数据保留。
1.5 代码和代码块
在实际开发过程中,需要注意的是 python 有两种操作方式
⚫ 交互模式
⚫ IDE 开发模式
在交互模式下,每行命令是一个独立运行的代码块,每个代码块运行会独立申请一次内存,在操作过程中交互模式没有退出的情况下遵循 PYTHON 官方操作标准 。
IDE 开发模式下,代码封装在模块中,通过 python 命令运行模块时,模块整体作为一个代码块向系统申请内存并执行程序,执行过程中对于基本数据类型进行缓存优化操作
2. 程序内测代码检测
memory_profiler
自我的理解 就是在python代码中可以检测出每一个最小单元运行时内存的消耗
具体指向代码如下
# 面试题
''''''
'''
内存检测模块:memory profiler
pip install memory_profiler
测试内存:
在测试函数前面,添加一个检测注解@profile
'''
from memory_profile import profile
@profile
def chg_nums(n):
n = 20
def chg_list(l):
l.append(20)
a = 18
chg_nums(a)
b = [1,2]
chg_list(b)
print(a) # ?18 20 18 20
print(b) # ?[1,2] [1,2,20] [1,2,20] [1,2]
3. 操作符号
跟对象内存相关的三个操作符号:
- a is b:判断两个变量a/b,他们指向的对象是否同一个对象
- a == b:判断两个变量a/b,他们指向的对象的数据内容是否一致【不做深层判断】
- isinstance(a, b) 判断a对象是否属于b类型
4. 引用、浅拷贝、深拷贝
4.1 对象的内存分配
对象的创建,依赖于申请的内存空间中数据的加载,对象在内存中的创建过程依赖于三部分
内存处理:对象分配内存地址、引用变量分配内存地址、对象和引用变量之间的关联
PYTHON 中对于这样的情况,有三种不同的操作方式
引用
⚫ 如果程序中多个不同的地方都要使用同一个对象,通过对象的引用赋值,将同一个对象赋值给多个变量
浅拷贝
⚫ 如果程序中多个不同的地方都要使用相同的对象数据,通过对象的拷贝完成数据的简单复制即可,对象中的包含的数据要求必须统一
深拷贝
⚫ 如果程序中多个不同的地方使用相同的而且独立的对象数据,通过对象的深层次的复制将对象的数据完整复制成独立的另一份即可
4.2 引用
# 1. 对象的引用赋值:将同一个对象的引用,赋值给多个变量
# 多个变量,指向的是同一个对象
# 注意:引用赋值并不会产生新的对象,而是让多个变量可以共同指向一个对象
# 通过多个变量都可以操作同一个对象的数据
a = Person("tom", ["LOL", "OW"])
b = c = a
print(id(a))
print(id(b))
print(id(c))
print("------------------------")
引用相当于赋值,就是把对象的地址给了多个变量
4.3 浅拷贝
# 2. 对象的拷贝:创建一个新的对象
import copy
# 浅拷贝:复制一个对象,复制对象中的属性数据的引用
x = copy.copy(a) # 拷贝了a对象,产生了一个对象x
x.fav.append("PUBG")
print(id(a)) # 1609072504784
print(id(x)) # 1609072525496
print(a.fav) # ['篮球', '足球', '球', 'PUBG']
print(x.fav) # ['篮球', '足球', '球', 'PUBG']
x.name = "jerry"
print(a.name) # tom
print(x.name) # jerry
浅拷贝是又创建了一个对象,复制了对象中的属性数据的引用,共用之前的数据如下图
4.4 深拷贝
# 对象的深拷贝:拷贝创建一个新对象,同时拷贝对象属性的数据[而不是引用]
m = Person("tom", ["LOL", "OW"])
y = copy.deepcopy(m)
print(id(m))
print(id(y))
y.fav.append("PUBG")
print(m.fav) # ['篮球', '足球', '球']
print(y.fav) # ['篮球', '足球', '球', 'PUBG']
深拷贝是又创建了一个对象,数据也复制了一份,并且可以独立处理
如下图
5 垃圾回收机制
在 PYTHON 中的垃圾回收机制主要是以引用计数为主要手段 以标记清除和隔代回收机制作为辅助操作手段 完成对内存中无效数据的自动管理操作的!
5.1 引用计数
5.1.1 什么是引用计数
引用计数算法的核心思想是:当一个对象被创建或者拷贝时,引用计数就会+1,当这个对象的多个引用变量,被销毁一个时该对象的引用计数就会-1,如果一个对象的引用计数为 0 则表示该对象已经不被引用,就可以让垃圾回收机制进行清除并释放该对象占有的内存空间了。
引用计数算法的优点是:操作简单,实时性能优秀,能在最短的时间获得并运算对象引用数
引用计数算法的缺点是:为了维护每个对象的引用计数操作算法,PYTHON 必须提供和对象对等的内存消耗来维护引用计数,这样就在无形中增加了内存负担;同时引用计数对于循环应用/对象之间的互相引用,是无法进行引用计数操作的,所以就会造成常驻内存的情况。
5.1.2引用计数
PYTHON是一个面向对象的弱类型语言,所有的对象都是直接或者间接继承自object类型,object 类型的核心其实就是一个结构体对象
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
在这个结构体中,ob_refcnt 就是对象的引用计数,当对象被创建或者拷贝时该计数就会增加+1,当对象的引用变量被删除时,该计数就会减少-1,当引用计数为 0 时,对象数据就会被回收释放了。在 python 中,可以通过 sys.getrefcount()来获取一个对象的引用计数
5.2 标记清除
标记清除算法核心思想:首先找到 PYTHON 中的一批根节点对象,如 object 对象,通过根节点对象可以找到他们指向的子节点对象,如果搜索过程中有这个指向是从上往下的指向,表示这个对象是可达的,否则该对象是不可达的,可达部分的对象在程序中需要保留下来,不可达部分的对象在程序中是不需要保留的
5.3 分代回收
PYTHON 定义的这三个链表,主要是针对我们在程序中创建的对象,首先会添加到 0 代链表
随后 0 代链表数量达到一定的阈值之后,触发 GC 算法机制,对 0 代对象进行符合规则的引用计数运算,避免出现对象的延迟或者过早的释放
最终,触发 GC 机制将已经没有引用指向的对象进行回收,并将有引用继续指向的对象移动到第 1 代对象链表中;第 1 代对象链表的对象,就是比第 0 代对象链表中的对象可能存活更久的对象,GC 阈值更大检测频率更慢,以提高程序执行效率。 以此类推直到一部分对象存活在第 2 代对象链表中,对象周期较长的可能跟程序的生命周期一样了。
简单理解 0代对象数量到达阈值时,就会触发GC算法机制(先引用计数,再标记清除),然后将剩余的对象放到2代对象,直到3代对象。
5.4 垃圾回收处理
PYTHON 中的 gc 模块提供了垃圾回收处理的各项功能机制,必须 import gc 才能使用
gc.set_debug(flags):设置 gc 的 debug 日志,一般为 gc.DEBUG_LEAK
gc.collect([generation]):显式进行垃圾回收处理,可以输入参数~参数表示回收的对象代数,0 表示只检查第 0 代对象,1 表示检查第 0、1 代对象,2 表示检查 0、1、2 代独对象,如果不传递参数,执行 FULL COLLECT,也就是默认传递 2
gc.set_threshold(threshold0 [, threshold2 [, threshold3]]):设置执行垃圾回
收机制的频率
gc.get_count():获取程序对象引用的计数器
gc.get_threshold():获取程序自动执行 GC 的引用计数阈值
以上是 PYTHON 中垃圾回收机制的基本操作,在程序开发过程中需要注意:
⚫ 项目代码中尽量避免循环引用
⚫ 引入 gc 模块,启用 gc 模块自动清理循环引用对象的机制
AUTHOR:大牧莫邪 DATETIME:2018/8/12
⚫ 将需要长期使用的对象集中管理,减少 GC 资源消耗
⚫ gc 模块处理不了重写del方法导致的循环引用,如果一定要添加该方法,需要显式调用 gc 模块的 garbage 中对象的del方法进行处理