Python 和C++ 中的内存管理
文章目录
介绍Python 和C++ 中的内存管理。Python中的内存管理分为内存分配、引入计数和垃圾回收。C++ 中的内存管理主要介绍五个不同的分区和new delete malloc free等四个关键字。
Python的内存管理机制
(1)Python变量、对象、引用、存储
变量:通过变量指针引用对象,变量指针指向具体的内存空间,取对象的值。对象:每个对象都包含一个头部信息(类型标识符和引用计数器)
常用的两个函数:
|
|
(2)内存分配
Python中变量有三个存储区,事先分配的静态内存,事先分配可重复利用的内存以及需要通过 malloc 和free 控制的自由存储区。Python中分为大内存和小内存(256K为分界线),大内存使用malloc 分配,小内存使用内存池分配。
(3)引用计数
使用 sys.getrefcount()
查看对象的引用计数
普通引用 和容器引用
|
|
容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。
引用计数的增加
- 对象被创建
a=[123]
- 被别人引用
(b =a)
- 作为容器对象的一个元素
c =[123, 23]
引用计数减少
- 对象别名被显式销毁
del m
- 对象被赋值成其他的元素
(4)垃圾回收
在Python 中,当某个对象的引用计数降为0, 说明没有任何引用指向该对象,那么就要被回收。
|
|
当回收启动时,python检测到这个引用计数为0,那么其占据的内存被清空。但是,垃圾清理时候,Python是不能进行其他任务,频繁的垃圾清理大大降低Python的工作效率。
分代回收
分代回收(generational GC trigger)的基本假设:存活时间越久的对象,越不可能在后面的程序中变成垃圾。Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
|
|
As you see, here we have a threshold of 700 for the first generation, and 10 for each of the other two generations.
|
|
In the above example, we have increased the threshold value for all the 3 generations. Increasing the threshold value will decrease the frequency of running the garbage collector. 数值越大,那么进行该 generation的频率就越低
引用环问题
|
|
这些引用环可能无法使用,但是引用计数不为0。
在 Python 中,每个对象除了引用计数 之外,还保存着一个 实际引用计数 的值,这个值在 GC Tracing 开始时是和 引用计数 一致的,假设检测到 a -> b 这个引用,那么就将 b 的 实际引用计数 减少1,当遍历完之后,找到那些 实际引用计数 不为 0 的元素,他们的关联对象都是需要保留的,其他无关的对象都是应该删除的。
除了依赖于上述的自动的 gc,还可以进行手动的gc操作
|
|
实战
- 使用联接将项目添加到列表是高效Python代码的最佳做法
无需将line1,line2分别添加到mymsg,而是使用list和join。
|
|
最好写成这个样子
|
|
- 避免对字符串使用+运算
如果可以避免,请不要使用+运算符进行串联。由于字符串是不可变的,因此每次将元素添加到字符串时,Python都会创建一个新的字符串和一个新的地址。这意味着每次更改字符串时都需要分配新的内存。
不要这样做:
|
|
更好的写法
|
|
使用 generators 生成器可以一次返回一个项目,而不是一次返回所有项目。这意味着如果是大型数据集,可以减少内存和时间的访问。
|
|
将函数功能的编译部分放到公共部分。如果要遍历数据,则可以使用正则表达式的缓存版本。
|
|
Python访问局部变量要比全局变量有效得多。将函数分配给局部变量,然后使用它们。 (这种思路和上面基本相同,如果是循环反复调用或者遍历数据,那么可以参考以下的结构)
|
|
尽可能使用内置函数和库。因为内置函数通常使用最佳内存使用方法进行实现。
|
|
这里的 Counter 表示的意思最后的 a 出现 1次,g 会出现8次。
|
|
通过使用itertools摆脱不必要的循环
|
|
C++ 中的内存管理机制
(1)内存分配方式
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
- 栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 堆:就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
- 自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
- 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
- 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
样例分析:
|
|
a、代码区(code area) 存放函数体(类成员函数、全局函数)的二进制代码。 b、全局区(data area) 静态存储区,存放全局变量、静态变量,初始化变量的在一块区域(低地址区域),未初始化变量在另一块区域(高地址区域BSS)。文字常量、字符串常量,程序结束后由系统释放。 c、堆区(heap area) 由低地址向高地址增长。一般new、malloc分配,由程序员分配和释放,分配方式类似于链表。 e、栈区(stack area) 由高地址向低地址增长。存放函数形参、局部变量、返回值等,由编译器自动分配、释放。
(堆和自由存储区其实不过是同一块区域,new底层实现代码中调用了malloc,new可以看成是malloc智能化的高级版本)
(2)堆和栈的区别?
(这里和数据结构中的堆栈是不一样的)
- **空间大小:**一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(可以修改)。
- 生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
- 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列
- 管理方式不同 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
(3)malloc 和free
malloc(memory allocation,动态内存分配)与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。
对象的动态内存分配
|
|
如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
在C++ STL中,vector是用内建(build-in)的动态数组(dynamic array)实现的。动态数组是相对于静态数组(数组大小不变)而言的,在C++中普通的数组的大小是固定的,在初始化的时候就确定了,使用的时候不能超过其范围。
静态数组是在栈上分配的空间
|
|
动态数组是在堆上分配的空间
|
|
一般来说,如果重新分配,那么是之前的二倍。
|
|
注意事项: 1). 申请内存空间后,必须检查是否分配成功; 2). 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,以防后面的程序不小心使用了野指针 3). malloc和free应该配对使用;释放只能释放一次,若释放两次或更多会出现错误,(释放空指针例外,释放空指针其实等于啥都没做,释放空指针多少次都没有问题) 4). malloc从堆里面获得内存;函数返回的指针指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表,当操作系统收到程序的申请时,就会遍历链表,然后寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点分配给程序。
(4)new和delete
c++中,用new和delete动态创建和释放数组或者单个对象。动态创建对象时,只需要指定其数据类型,而不必为该对象命名。
1). new运算符返回指向该创建对象的指针。
我们可以通过指针来访问此对象。int *pi = new int ;此new表达式在堆区中分配创建了一个整型对象,并返回此对象的地址,并用该地址初始化指针pi。
2). 可以对动态创建的对象做值初始化:
|
|
3). delete撤销动态创建的对象
delete表达式释放指针指向的地址空间;
|
|
如果指针指向的不是new分配的内存地址,则使用delete是不合法的。
4). delete之后,重设指针的值
delete p;执行完该语句后,p变成了不确定的指针,尽管p值没有明确定义,但仍然存放了它之前所指对象的地址,然后p所指的内存已经被释放了,所以p不再有效。此时,该指针变成了悬垂指针(悬垂指针指向曾经存放对象的内存,但该对象已经不存在了)。悬垂指针往往导致程序错误,而且很难检测出来。
一旦删除了指针所指的对象,立即将指针置为0,这样就可以非常清楚的指明指针不再指向任何对象。(零值指针:int * ip =0;)
(5)malloc和new的区别
1). new 返回指定类型指针,并且可以自动计算所需要的大小;malloc需要手动计算字节数,并且在返回后强制类型转换为实际类型的指针。 2). malloc只管分配内存,并不能对所得到的内存进行初始化,所以得到的一片新内存中,其值将是随机的;new不仅分配内存,还对内存中的对象进行初始化;free只管释放内存;delete不仅释放内存,还会调用对象的析构函数,销毁对象。 3). malloc/free是c++/c的标准库函数,头文件为stdlib.h;而new/delete是c++的运算符。他们都可用于申请动态内存和释放内存。 4). 对于非内部数据结构的对象而言,光用malloc/free无法满足动态对象的要求,对象在创建的同时,还要自动执行构造函数,对象在消亡之前要自动执行析构函数,由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free;因此,c++语言需要一个能够完成动态内存分配和初始化的运算符new,以及一个能完成清理与释放内存工作的运算符delete;我们不要企图用malloc和free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型没有构造和析构过程,对他们而言malloc/free和new/delete是等价的。
文章作者 jijeng
上次更新 2020-01-01