为了提高 string 的读写性能 Delphi 采用了 Copy-on-Write 机制进行内存管理。

简单来说,在复制一个 string 时并不是真的在内存中把原来 string 的内容复制一份到另外一个地址,而是把新的 string 在内存映射表中指向同原 string 相同的位置,并且把那块内存的引用计数加一。这样就省去了复制字符串的时间。只有当 string 的内容发生变化的时候,才真正将改动的内容完整复制一份到新的地址,然后对原地址的引用计数减一,将新地址的引用计数设为一,最后将新 string 在内存映射表中指向这个新的位置。当某个字符串内存块的引用计数为零了,这块内存就可以被其它程序使用了。注意:所有常量 string 会在编译时率先分配内存,其引用计数不会在程序中变化,始终为-1。

更详细的介绍,可以参考『Pascal 精要』和『标准C++类std::string的内存共享和Copy-On-Write技术』。

内存泄漏的发现:

在检查内存泄漏时,无意发现了使用记录过程中产生的内存泄漏。请看如下代码:

typeTMyRec = recordS: string;I: Integer;end;procedure Test;
varARec: TMyRec;
beginFillChar(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;
beginResult := Format('Addr: %p, RefCount: %d, Value: %s', [Pointer(S), PInteger(Integer(S) - 8)^, S]);
end;procedure BadExample1;
varS1: string;ARec: TMyRec;
beginS1 := Copy('string', 1, 6); // Force allocates memory for the stringWriteLn(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;
varS1: string;ARec: TMyRec;
beginS1 := 'string'; // Assigns S1 to a const (compiler time allocated) stringWriteLn(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);
beginFillChar(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 的记录类型的引用计数问题。

[转] FillChar 引起的内存泄漏相关推荐

  1. Fillchar 引起的内存泄漏[转]

    为了提高 string 的读写性能 Delphi 采用了 Copy-on-Write 机制进行内存管理. 简单来说,在复制一个 string 时并不是真的在内存中把原来 string 的内容复制一份到 ...

  2. [JS] 闭包与内存泄漏

    一句话总结闭包:函数里套函数,函数返回函数. 内存泄漏:每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址.但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删 ...

  3. android释放acitity内存,Android 内存泄漏分析与解决方法

    在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...

  4. C语言中的指针和内存泄漏

    对于任何使用 C 语言的人,如果问他们 C 语言的最大烦恼是什么,其中许多人可能会回答说是指针和内存泄漏.这些的确是消耗了开发人员大多数调试时间的事项.指针和内存泄漏对某些开发人员来说似乎令人畏惧,但 ...

  5. 初步判断内存泄漏方法

    有时候,内存泄漏不明显,或者怀疑系统有内存泄漏,我们可以通过下面介绍的方法初步确认系统是否存在内存泄漏. 首先在Java命令行中增加-verbose:gc参数, 然后重新启动java进程. 当系统运行 ...

  6. 野指针与内存泄漏那些事

    野指针:不是NULL指针,是指向垃圾内存的指针 野指针成因: 1.指针变量没有被初始化:指针变量在创建时同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存. 2.指针p被free或者d ...

  7. 介绍两个非常好用的Javascript内存泄漏检测工具

    内存泄漏对开发者来说一般很难检测因为它们是由一些大量代码中的意外的错误引起的,但它在系统内存不足前并不影响程序的功能.这就是为什么会有人在很长时间的测试期中收集应用程序性能指标来测试性能. 最简单的检 ...

  8. 内存溢出和内存泄漏的定义,产生原因以及解决方法(面试经验总结)

    一.定义(概念与区别) 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory:比如申请 了一个integer,但给它存了long才能存 ...

  9. Unix下C程序内存泄漏检测工具Valgrind安装与使用

    Valgrind是一款用于内存调试.内存泄漏检测以及性能分析的软件开发工具. Valgrind的最初作者是Julian Seward,他于2006年由于在开发Valgrind上的工作获得了第二届Goo ...

最新文章

  1. xcode 消除警告
  2. QIIME 2教程. 32如何写方法和引用Citing(2021.2)
  3. Git reset , revert, checkout的区别和联系
  4. sql distinct 去重复 (mysql)
  5. (4)HTML标签补充和HTML转义字符
  6. php lwm2m,理解COAP/LWM2M/MQTT协议和TCP/UDP协议的关系
  7. Qt 语言家实现中英文切换(解决纯代码添加部件的中英文转换问题)
  8. java file exists用法_Java File exists()方法
  9. 基于安卓WebServicw天气预报demo
  10. linux下cuda cudnn安装 没有权限的安装
  11. python正确的赋值语句是_python中赋值的方法
  12. idea下载安装破解详解
  13. “智能+场景”,喜临门或造就深睡时代新风口
  14. 微信小程序中短信验证码登录全流程及代码
  15. CSS三种样式表 内部样式表、行内部样式表、外部引用
  16. C语言编程鉴赏,吴坚鸿单片机程序风格赏析(一)
  17. 实验8-2-10 IP地址转换 (20 分)
  18. Lua脚本的基本使用
  19. 托业单词表Part3
  20. 如何入门自动控制原理

热门文章

  1. iOS 11 : CORE ML—浅析
  2. mysql删除账号,如何删除MySQL用户帐户
  3. 用java实现web中闹钟小功能_Java多线程小练习,闹钟
  4. 从青萍之末到微澜之间,问题就是这么简单
  5. OpenGL ES for Android 绘制立方体
  6. layui loading
  7. 006_similarsites
  8. 不提拔你,就因为你只想把工作做好
  9. 在地图上绘制实时轨迹线和方向箭头
  10. 使用jgit第三方库拉取代码