可能你不知道的内存泄漏

原创 2007年07月20日 02:53:00
Delphi 是如何管理 string 的?
 
为了提高 string 的读写性能 Delphi 采用了 Copy-on-Write 机制进行内存管理。简单来说,在复制一个 string 时并不是真的在内存中把原来 string 的内容复制一份到另外一个地址,而是把新的 string 在内存映射表中指向同原 string 相同的位置,并且把那块内存的引用计数加一。这样就省去了复制字符串的时间。只有当 string 的内容发生变化的时候,才真正将改动的内容完整复制一份到新的地址,然后对原地址的引用计数减一,将新地址的引用计数设为一,最后将新 string 在内存映射表中指向这个新的位置。当某个字符串内存块的引用计数为零了,这块内存就可以被其它程序使用了。注意:所有常量 string 会在编译时率先分配内存,其引用计数不会在程序中变化,始终为-1更详细的介绍,可以参考『Pascal 精要』和『标准C++std::string的内存共享和Copy-On-Write技术』。 
 
内存泄漏的发现:
在检查内存泄漏时,无意发现了使用记录过程中产生的内存泄漏。请看如下代码:
type 
  TMyRec = 
record
    S: 
string;
    I: Integer;
  
end;

procedure Test;
var
  ARec: TMyRec;
begin
  FillChar(ARec, SizeOf(ARec), #0);
  ARec.S := 
'abcd';
  ARec.I := 
1234;
  
// ...
  FillChar(ARec, SizeOf(ARec), #0); //<--- A leak!
  
// ...
end;
FillChar 的作用是对一个内存块进行连续赋值,内存泄漏出现在第二次调用 FillChar 的时候。经过调试后发现:如果把记录中的 string 字段改成 Pchar 或者删除,就不再有内存泄漏了。
 
原因分析:
我们现在先了解一下记录在内存中是如何分配的。记录是个不同数据类型的集合体。记录长度就是每个字段的内存长度之和。注意,长度在编译之前就已经是确定的。因此那些长度不定的类型 ( string、对象) 都是以指针形式出现在记录中。我的分析是:由于 FillChar 是低级内存读写操作,它仅仅把记录所占的内存块清掉,但没通知编译器更新字符串的引用计数,因而造成了泄漏。请看如下代码:
function StringStatus(const S: string): string;
begin
  Result :=
    Format(
'Addr: %p, RefCount: %d, Value: %s',
      [Pointer(S),
       PInteger(Integer(S) - 
8)^,
       S]);
end;

procedure BadExample1;
var
  S1: 
string;
  ARec: TMyRec;
begin
  S1 := Copy('string'16); // Force allocates memory for the string
  WriteLn(StringStatus(S1));
  ARec.S := S1;
  WriteLn(StringStatus(ARec.S));
  FillChar(ARec, SizeOf(ARec), 
#0);
  WriteLn(StringStatus(S1));
end;
Addr: 00E249E8, RefCount: 1, Value: string // OK, Allocated as a new string
Addr: 00E249E8, RefCount: 2, Value: string // OK, RefCount increated
Addr: 00E249E8, RefCount:
2, Value: string // WRONG! RefCount should be 1
在执行 FillChar 之前,字符串 S1 的引用计数是2,但是执行 FillChar 之后并没有减1。这段代码验证了我的推测FillChar 操作可能会破坏字符串的 Copy-on-Write 机制使用的时候需要倍加小心
 
进一步分析:
文章开头我提到 “所有有常量 string 会在编译时率先分配内存,其引用计数不会在程序中变化,始终为-1。“ 那么如果我们让 S1 ARec.S 都赋值为一个常量字符串,那么照理说就不用管引用计数,也就没有泄漏问题了。请接着看下面这个例子:
procedure BadExample2;
var
  S1: 
string;
  ARec: TMyRec;
begin
  S1 := 'string'; // Assigns S1 to a const (compiler time allocated) string
  WriteLn(StringStatus(S1));
  ARec.S := S1;
  WriteLn(StringStatus(ARec.S));
  FillChar(ARec, SizeOf(ARec), 
#0);
  WriteLn(StringStatus(S1));

end
;
Addr: 0040CCBC, RefCount: -1, Value: string // OK, RefCount UN-changed
Addr:
00E24B08, RefCount:  1, Value: string // !!! Allocated as a new string
Addr: 0040CCBC, RefCount: -1, Value: string // OK, RefCount UN-changed
是不是很吃惊?对赋值 ARec.S 的时候,结果并不是预期的那样,直接将其指向常量字符串,而是重新分配了一个新的字符串。我个人认为:记录在对字符串赋值上是有问题的!
 
解决方法:
既然知道使用 FillChar 来初始化记录是不安全的,那么我们是不是要回到解放前,手动对记录进行初始化呢?也不用。Delphi 有个保留字 out。它和 var、const 一样,是用来修饰函数参数的。它和 var 的功能相似,不同是,它会对那些以指针形式传入的变量先进行引用计数清理。Delphi 的帮助中解释道:An out parameter, like a variable parameter, is passed by reference. With an out parameter, however, the initial value of the referenced variable is discarded by the routine it is passed to. The out parameter is for output only; that is, it tells the function or procedure where to store output, but doesn't provide any input.
哈哈,这个不正是 FillChar 想要但又做不到的吗?于是我改造了一个 InitializeRecord 来初始化记录。
procedure InitializeRecord(out ARecord; count: Integer);
begin
  FillChar(ARecord, count, 
#0);
end;
仅仅是多了一层函数嵌套,内存泄漏问题就解决了。多亏了这个神奇的 out
我们来仔细看看加了 out 之后,编译器到底做了什么?
mov  edx,[$0040c904]
mov  eax,ebx
call @FinalizeRecord
  //<----- cleanup
mov  edx,
$0000000c
call InitializeRecord 
关键就是第三行调用了 FinalizeRecord。这是 System.pas 中的一个汇编函数,作用就是对记录做一下清理工作。如果你想探个究竟,可以查看一下这个函数是如何实现的。这里就不作详解了。
 
想法总结:
没想到一个偶然的发现,竟可以带出这么多问题,真是因祸得福。我总价一下几点想法:
  1. FillChar 是低级的内存读写,所以在使用之前你要非常清楚要打算干什么。
  2. 在记录类型中慎用 string Widestring。如果记录的结构复杂,不妨尝试封装成类,类可以提供更丰富的特性,扩展性更佳。如果一定要定义带 string 的记录,最好注释一下,以免日后出错。(有时候的确是记录更方便和高效)
  3. 活用 out 保留字可以解决接口类型和带 string 的记录类型的引用计数问题。

点击这里英文版本

可能出现内存泄漏的几种情况

定义    简单来说,内存泄漏就是程序在申请一个内存空间后没有释放,直到程序运行结束后才释放。这样看起来似乎没什么大问题,但是如果程序会持续运行很长时间(例如服务器),并且可能在程每次调用某个部分的...
  • GavinGreenson
  • GavinGreenson
  • 2017年04月05日 23:04
  • 714

没有躲过的坑--指针(内存泄露)

C++被人骂娘最多的就是指针。 夜深人静的时候,拿出几个使用指针容易出现的坑儿。可能我的语言描述有些让人费劲,尽量用代码说话。通过指向类的NULL指针调用类的成员函数 试图用一个null指针调用类...
  • wangshubo1989
  • wangshubo1989
  • 2015年11月04日 23:47
  • 2702

JAVA内存泄漏问题处理方法经验总结

JVM问题,一般会有三种情况,目前遇到了两种,线程溢出和JVM不够用   1.线程溢出:unable to create new native thread 1.1问题描述: 系统在1...
  • only_jing1314
  • only_jing1314
  • 2016年03月26日 14:26
  • 1234

Android 开发使用MVP产生的内存泄露问题

前段时间使用了mvp写了一个项目,发现这个模式很好用,然后用androidstudio自带的内存检查工具检查,发现好几处内存泄露,其实原因很简单,MVP中由于P对V(Activity)的引用可能导致内...
  • djfgduyhgfu
  • djfgduyhgfu
  • 2017年04月20日 00:25
  • 501

关于内存泄漏,还有哪些是你不知道的?

前言 好久没写东西了,因为最近懒了些,且找不到什么好的题材,所以准备对内存泄漏的问题做一篇整理。内存泄漏问题一直是项目开发中的一大问题,本文力求帮助从事过一段时间工作的iOS开发者快速寻找App...
  • zhengang007
  • zhengang007
  • 2017年05月02日 13:28
  • 354

关于内存泄漏,还有哪些是你不知道的?

本文来自简书,原文地址: 前言 好久没写东西了,因为最近懒了些,且找不到什么好的题材,所以准备对内存泄漏的问题做一篇整理。内存泄漏问题一直是项目开发中的一大问题,本文力求帮助从事过一段时间工作的i...
  • qq_30513483
  • qq_30513483
  • 2017年04月28日 14:04
  • 248

关于iOS内存泄漏,还有哪些是你不知道的?

对于iOS开发者,网络请求类AFNetWorking是再熟悉不过了,对于AFNetWorking的使用我们通常会对通用参数、网址环境切换、网络状态监测、请求错误信息等进行封装。在封装网络请求类时需注意...
  • u012371575
  • u012371575
  • 2017年11月20日 17:53
  • 99

Tomcat内存优化4.1 内存泄漏——内存分析工具 MAT 的使用

在eclipse安装、使用MAT插件 简介:  Eclipse提供的一个内存分析工具。它是一个功能丰富的 JAVA 堆转储文件分析工具,可以帮助你发现内存漏洞和...
  • truelove12358
  • truelove12358
  • 2015年10月29日 16:42
  • 1513

内存泄漏的八种可能

 Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同时防止未释放的内存把堆栈(heap)挤爆的可能,所以...
  • smxueer
  • smxueer
  • 2016年09月03日 15:26
  • 1070

Direct ByteBuffer可能会导致内存泄露的原因

Direct ByteBuffer可能会导致内存泄露的原因
  • LoveTea99
  • LoveTea99
  • 2016年09月17日 15:53
  • 721
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:可能你不知道的内存泄漏
举报原因:
原因补充:

(最多只允许输入30个字)