实际上,严格来说,您的测试在第一次运行时会泄漏内存。
这不是FastMM、DUnit 或 Delphi 中的错误,错误在您的测试中。
让我们从澄清误解开始,并解释一些内部工作原理:
误解:FastMM 证明我的应用程序没有泄漏
这里的问题是,如果 FastMM 没有检测到泄漏,它会给你一种错误的安全感。原因是任何类型的泄漏检测都必须从检查点寻找泄漏。如果在开始检查点之后完成的所有分配都由结束检查点恢复 - 一切都很酷。
因此,如果您创建一个全局对象 Bin,并将所有对象发送到 Bin 而不破坏它们,那么您确实存在内存泄漏。继续运行,您的应用程序将耗尽内存。但是,如果 Bin 在 FastMM End 检查点之前销毁其所有对象,FastMM 将不会注意到任何不良情况。
您的测试中发生的情况是,FastMM 的检查点范围比 DUnit 泄漏检测范围更广。您的测试会泄漏内存,但该内存稍后会在 FastMM 进行检查时恢复。
每个 DUnit 测试都有自己的实例进行多次运行
DUnit 为每个测试用例创建一个单独的测试类实例。但是,每次运行测试都会重复使用这些实例。事件的简化顺序如下:
- 启动检查点
- 呼叫设置
- 调用测试方法
- 调用拆解
- 结束检查点
因此,如果您在这 3 种方法之间存在泄漏 - 即使泄漏仅针对实例,并且会在对象被销毁后立即恢复 - 也会报告泄漏。在您的情况下,当对象被销毁时,泄漏被恢复。因此,如果 DUnit 为每次运行创建并销毁了测试类,则不会报告泄漏。
注意这是设计使然,因此您不能真正称其为错误。
基本上,DUnit 对您的测试必须 100% 自包含的原则非常严格。从 SetUp 到 TearDown,您分配(直接/间接)的任何内存都必须恢复。
常量字符串在分配给变量时被复制
每当您编码StringVar := 'SomeLiteralString'
或复制常量StringVar := SomeConstString
的StringVar := SomeResourceString
值时(是的,已复制- 不计入引用)
同样,这是设计使然。目的是,如果从库中检索到字符串,则在卸载库时不会丢弃该字符串。所以这不是一个真正的错误,只是一个“不方便”的设计。
因此,您的测试代码在第一次运行时泄漏内存的原因A := 'test'
是为“测试”的副本分配内存。在随后的运行中,“测试”的另一个副本被制作,前一个副本被销毁 - 但净内存分配是相同的。
解决方案
在这种特殊情况下的解决方案是微不足道的。
procedure TTest.TearDown;
begin
A := ''; //Remove the last reference to the copy of "test" and presto leak is gone :)
end;
一般来说,你不应该做更多的事情。如果您的测试创建了引用常量字符串副本的子对象,则这些副本将在子对象被销毁时被销毁。
但是,如果您的任何测试将对字符串的引用传递给任何全局对象/单例(顽皮,顽皮,您知道您不应该这样做),那么您将泄漏引用并因此泄漏一些内存 - 即使它是后来恢复了。
一些进一步的观察
回到关于 DUnit 如何运行测试的讨论。同一测试的单独运行可能会相互干扰。例如
procedure TTestLeaks.SetUp;
begin
FSwitch := not FSwitch;
if FSwitch then Fail('This test fails every second run.');
end;
扩展这个想法,您可以让您的测试在第一次和每秒(甚至)运行时“泄漏”内存。
procedure TTestLeaks.SetUp;
begin
FSwitch := not FSwitch;
case FSwitch of
True : FString := 'Short';
False : FString := 'This is a long string';
end;
end;
procedure TTestLeaks.TearDown;
begin
// nothing here :( <-- note the **correct** form for the smiley
end;
这并不会真正导致内存的总体消耗增加,因为每次交替运行恢复的内存量与每次运行时泄漏的内存量相同。
字符串复制会导致一些有趣的(也许是意想不到的)行为。
var
S1, S2: string;
begin
S1 := 'Some very very long string literal';
S2 := S1; { A pointer copy and increased ref count }
if (S1 = S2) then { Very quick comparison because both vars point to the same address, therefore they're obviously equal. }
end;
然而....
const
CLongStr = 'Some very very long string literal';
var
S1, S2: string;
begin
S1 := CLongStr;
S2 := CLongStr; { A second **copy** of the same constant is allocated }
if (S1 = S2) then { A full comparison has to be done because there is no shortcut to guarantee they're the same. }
end;
这确实提出了一个有趣但极端且可能不明智的解决方法,只是由于该方法的荒谬性:
const
CLongStr = 'Some very very long string literal';
var
GlobalLongStr: string;
initialization
GlobalLongStr := CLongStr; { Creates a copy that is safely on the heap so it will be allowed to be reference counted }
//Elsewhere in a test
procedure TTest.SetUp;
begin
FString1 := GlobalLongStr; { A pointer copy and increased ref count }
FString2 := GlobalLongStr; { A pointer copy and increased ref count }
if (FString1 = FString2) then { Very efficient compare }
end;
procedure TTest.TearDown;
begin
{... and no memory leak even though we aren't clearing the strings. }
end;
最后/结论
是的,显然这篇冗长的帖子即将结束。
非常感谢您提出这个问题。
它给了我一个关于我记得不久前遇到的相关问题的线索。在我有机会证实我的理论之后,我会发布一个问答;因为其他人也可能会发现它很有用。