Memory leak
内存泄漏是指:程序在动态分配内存后,由于某种原因未能释放或无法释放这些内存,导致系统内存的浪费。
产生内存泄露的原因
上述定义表示了一种现象,没有定义原因。要避免这种现象,就要探究产生现象的原因。
内存泄漏是在程序运行过程中产生的,程序运行依赖的是我们的指令,即程序的源代码。
不同的编程语言其对内存使用方式的表现不同。有的是通过指针,依赖指针来申请与释放内存;有的语言有内存管理器,通过对象的引用数是否为 0 来决定是否自动回收该对象内存。
产生内存泄漏的原因,从内存指针的角度来说,是申请内存之后的代码逻辑失去了该内存的指针而不能释放内存,或者指针都在但忘记释放内存。
char *p1 = (char *) malloc(10); // 指针 p1 指向 内存地址 1
char *p2 = (char *) malloc(10); // 指针 p2 指向 内存地址 2
p1 = p2; // p1 与 p2 军指向内存地址 2,导致当前逻辑失去了之前申请的内存地址 1 的指针
// 由于我们编写的程序往往运行在主循环中,当前逻辑执行完,程序并不会结束运行。
// 所以在当前逻辑结束前如果没有释放内存,就会产生内存泄漏的问题。
从对象引用的角度来说,是当前逻辑失去了先前定义对象的引用。
具体表现是定义了一个对象,然后将它的引用交给变量或成员属性。在之后又将它的引用传递给其他的对象,但其他对象只是接收它并在内部使用,不向外部提供任何访问它或者取消对它的引用的方式。当该我们的代码逻辑不再需要这个对象时,将变量或成员属性置空,但此时其他对象仍持有它的引用,导致该对象所使用的内存无法回收。
Data data = new Data(); // 创建对象,赋值给变量,引用数为 1
DoSth doSth = new DoSth(); // 其他对象
doSth.addData(data); // 传递引用,引用数为 2
data = null; // 取消引用,引用数为 1
// 当前逻辑不能访问 Data 对象,也不再使用它,但是内存无法释放
如果 DoSth 对象的功能的实现依赖 Data 对象,而将 data 变量置空后,之后的逻辑依然依赖使用 DoSth 通过的功能,即使不能使 DoSth 对象取消其对 Data 对象的引用,也是合理的。
但是,如果将 data 变量置空的意图是完全不再使用该对象,那 DoSth 对象也不应该使用它,此时不能使 DoSth 对象取消其对 Data 对象的引用就是不合理的。逻辑上完全不被使用的数据依然占据的内存,导致内存管理器无法回收,这就产生了内存泄漏。
造成这个问题的原因是,两个对象的生命周期不一致,被引用对象的生命周期比引用它的对象的生命周期短。
上述情况在 GUI 程序设计中,使用观察者模式来设计响应式数据,并通过它使视图和数据交互时,如果使用不当,就会产生:
视图对象作为观察者向数据中传递其引用,数据对象在变化时通知视图更新,看起来很棒。但是当原来的视图被新的视图替换,不再被使用时,若数据不移除对视图的引用,会产生内存泄漏。
所以在应用观察者模式设计类似响应式数据这种观察者对象的时候,同时提供注册观察者与注销观察者的功能。
另外,引入生命周期的概念来保持逻辑上的同步能很好地解决内存泄漏的问题。
避免内存泄漏的办法
时刻牢记:
- 当还需要使用内存或对象时,始终保持其指针或引用。
- 当内存不再使用时,立即释放。
- 当对象不再使用时,取消所有依赖它的对象对它的引用。这也就意味着若一个对象要引用其他对象,就同时要有取消引用的功能接口。