介绍Python 和C++ 中的内存管理。Python中的内存管理分为内存分配、引入计数和垃圾回收。C++ 中的内存管理主要介绍五个不同的分区和new delete malloc free等四个关键字。

Python的内存管理机制

(1)Python变量、对象、引用、存储

变量:通过变量指针引用对象,变量指针指向具体的内存空间,取对象的值。对象:每个对象都包含一个头部信息(类型标识符和引用计数器)

常用的两个函数:

1
2
id() # 用于返回对象的内存地址
is # 用来判断两个引用所指向的对象是否相同

(2)内存分配

Python中变量有三个存储区,事先分配的静态内存,事先分配可重复利用的内存以及需要通过 malloc 和free 控制的自由存储区。Python中分为大内存和小内存(256K为分界线),大内存使用malloc 分配,小内存使用内存池分配。

(3)引用计数

使用 sys.getrefcount() 查看对象的引用计数

普通引用 和容器引用

1
2
a =[1,2, 3] # 普通引用
b =a  # 容器对象引用

容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

引用计数的增加

  • 对象被创建 a=[123]
  • 被别人引用(b =a)
  • 作为容器对象的一个元素 c =[123, 23]

引用计数减少

  • 对象别名被显式销毁 del m
  • 对象被赋值成其他的元素

(4)垃圾回收

在Python 中,当某个对象的引用计数降为0, 说明没有任何引用指向该对象,那么就要被回收。

1
2
a =[321, 123]
del a

当回收启动时,python检测到这个引用计数为0,那么其占据的内存被清空。但是,垃圾清理时候,Python是不能进行其他任务,频繁的垃圾清理大大降低Python的工作效率。

分代回收

分代回收(generational GC trigger)的基本假设:存活时间越久的对象,越不可能在后面的程序中变成垃圾。Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

1
2
3
import gc
gc.get_threshold()
# (700, 10, 10)

As you see, here we have a threshold of 700 for the first generation, and 10 for each of the other two generations.

1
gc.set_threshold(900, 15, 15)

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的频率就越低

引用环问题

1
2
3
4
5
a = []
b = [a]
a.append(b)
del a
del b

这些引用环可能无法使用,但是引用计数不为0。

12.png

在 Python 中,每个对象除了引用计数 之外,还保存着一个 实际引用计数 的值,这个值在 GC Tracing 开始时是和 引用计数 一致的,假设检测到 a -> b 这个引用,那么就将 b 的 实际引用计数 减少1,当遍历完之后,找到那些 实际引用计数 不为 0 的元素,他们的关联对象都是需要保留的,其他无关的对象都是应该删除的。

除了依赖于上述的自动的 gc,还可以进行手动的gc操作

1
2
3
import gc
n = gc.collect()
print("Number of unreachable objects collected by GC:", n)

实战

  1. 使用联接将项目添加到列表是高效Python代码的最佳做法

无需将line1,line2分别添加到mymsg,而是使用list和join。

1
2
mymsg=line1\n
mymsg+=line2\n

最好写成这个样子

1
2
mymsg=[line1,line2']
\n.join(mymsg)
  1. 避免对字符串使用+运算

如果可以避免,请不要使用+运算符进行串联。由于字符串是不可变的,因此每次将元素添加到字符串时,Python都会创建一个新的字符串和一个新的地址。这意味着每次更改字符串时都需要分配新的内存。

不要这样做:

1
msg=hello+mymsg+world

更好的写法

1
msg=’hello %s world’ % mymsg

使用 generators 生成器可以一次返回一个项目,而不是一次返回所有项目。这意味着如果是大型数据集,可以减少内存和时间的访问。

1
2
3
4
5
6
def __iter__(self):
     return self._generator()

def _generator(self):
     for itm in self.items():
         yield itm

将函数功能的编译部分放到公共部分。如果要遍历数据,则可以使用正则表达式的缓存版本。

1
2
3
4
5
match_regex=re.compile(“foo|bar”)

for i in big_it:
     m = match_regex.search(i)
         ….

Python访问局部变量要比全局变量有效得多。将函数分配给局部变量,然后使用它们。 (这种思路和上面基本相同,如果是循环反复调用或者遍历数据,那么可以参考以下的结构)

1
2
3
myLocalFunc=myObj.func
for i in range(n):
    myLocalFunc(i)

尽可能使用内置函数和库。因为内置函数通常使用最佳内存使用方法进行实现。

1
mylist=map(str.lower, oldlist)

这里的 Counter 表示的意思最后的 a 出现 1次,g 会出现8次。

1
2
3
4
import collections
mycounter = collections.Counter (a = 1, b = 2, c = 3, d = 5, e = 6, f = 7, g = 8)
for i in mycounter.elements():
    print(i)

通过使用itertools摆脱不必要的循环

1
2
from itertools import product, chain
list(chain.from_iterable(function(shape, weight) for weight, shape in product([True, False], range(1, 5))))

C++ 中的内存管理机制

(1)内存分配方式

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

  • 栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆:就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
  • 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

样例分析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include<iostream>
using namespace  std;
int a = 0; //全局初始化区
char *p1; //全局未初始化区
int main()
{
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; // 123456\0在常量区,p3在栈上。
    static int c =0; //全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20); 12: //分配得来得10和20字节的区域就在堆区。
    strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
    return 0;
}

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无法满足动态对象的要求。

对象的动态内存分配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
 
class Box
{
   public:
      Box() { 
         cout << "调用构造函数!" <<endl; 
      }
      ~Box() { 
         cout << "调用析构函数!" <<endl; 
      }
};
 
int main( )
{
   Box* myBoxArray = new Box[4];
 
   delete [] myBoxArray; // 删除数组
   return 0;
}

如果要为一个包含四个 Box 对象的数组分配内存,构造函数将被调用 4 次,同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。

在C++ STL中,vector是用内建(build-in)的动态数组(dynamic array)实现的。动态数组是相对于静态数组(数组大小不变)而言的,在C++中普通的数组的大小是固定的,在初始化的时候就确定了,使用的时候不能超过其范围。

静态数组是在栈上分配的空间

1
int a[100] = {0};

动态数组是在堆上分配的空间

1
auto *p = new int(100); memset(p, 0, sizeof(int));

一般来说,如果重新分配,那么是之前的二倍。

1
2
3
4
5
6
7
function insertEnd(dynarray a, element e)
    if (a.size = a.capacity)
        // resize a to twice its current capacity:
        a.capacity ← a.capacity * 2
        // (copy the contents to the new memory location here)
    a[a.size] ← e
    a.size ← a.size + 1

注意事项: 1). 申请内存空间后,必须检查是否分配成功; 2). 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,以防后面的程序不小心使用了野指针 3). malloc和free应该配对使用;释放只能释放一次,若释放两次或更多会出现错误,(释放空指针例外,释放空指针其实等于啥都没做,释放空指针多少次都没有问题) 4). malloc从堆里面获得内存;函数返回的指针指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表,当操作系统收到程序的申请时,就会遍历链表,然后寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点分配给程序。

(4)new和delete

c++中,用new和delete动态创建和释放数组或者单个对象。动态创建对象时,只需要指定其数据类型,而不必为该对象命名。

1). new运算符返回指向该创建对象的指针。

我们可以通过指针来访问此对象。int *pi = new int ;此new表达式在堆区中分配创建了一个整型对象,并返回此对象的地址,并用该地址初始化指针pi。

2). 可以对动态创建的对象做值初始化:

1
2
3
int *pi = new int ();//初始化为0;
int *pi = new int ;//pi指向一个没有初始化的int
string *ps = new string();//初始化为空字符串(对于提供了默认构造函数的类型,没有必要对其对象进行值初始化)

3). delete撤销动态创建的对象

delete表达式释放指针指向的地址空间;

1
2
delete pi//释放单个对象
delete [ ] pi;//释放数组

如果指针指向的不是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是等价的。