PowerShell 管道执行时不进行垃圾回收

10

更新:以下错误似乎已经在PowerShell 5中得到解决。该错误仍然存在于3和4中。因此,除非您使用PowerShell 2或5,否则请勿使用管道处理任何巨大的文件。


考虑以下代码片段:

function Get-DummyData() {
    for ($i = 0; $i -lt 10000000; $i++) {
        "This is freaking huge!! I'm a ninja! More words, yay!"
    }
}

Get-DummyData | Out-Null

执行Get-DummyData | Out-Null几次后,这将导致PowerShell的内存使用量不受控制地增长。我曾经看到过PowerShell的内存使用量一直增加到4 GB。

根据ANTS Memory Profiler,我们有许多东西坐在垃圾收集器的终结队列中。当我调用[GC]::Collect()时,内存从4 GB降至仅70 MB。所以严格来说我们没有内存泄漏。

现在,当我完成一个长时间运行的管道操作后,我不能只能调用[GC]::Collect()。我需要在管道操作期间进行垃圾回收。但是,如果我尝试在执行管道时调用[GC]::Collect()...

function Get-DummyData() {
    for ($i = 0; $i -lt 10000000; $i++) {
        "This is freaking huge!! I'm a ninja! More words, yay!"

        if ($i % 1000000 -eq 0) {
            Write-Host "Prompting a garbage collection..."
            [GC]::Collect()
        }
    }
}

Get-DummyData | Out-Null

...问题仍然存在。内存使用量再次不受控制地增长。我尝试了几种方法,例如添加[GC] :: WaitForPendingFinalizers()Start-Sleep -Seconds 10等。我尝试更改垃圾回收器延迟模式并强制PowerShell使用服务器垃圾收集,但无济于事。我只是不能让垃圾回收器在管道执行时发挥作用。

在PowerShell 2.0中根本没有这个问题。有趣的是,$null = Get-DummyData似乎也可以正常工作而没有内存问题。因此看起来与管道有关,而不是我们正在生成大量字符串的事实。

如何防止在长时间管道期间内存不受控制地增长?

旁注:

我的Get-DummyData函数仅供演示目的。我的真实问题是无法使用Get-ContentImport-Csv在PowerShell中读取大型文件。不,我没有将这些文件的内容存储在变量中。我严格使用管道,就像应该做的那样。 Get-Content .\super-huge-file.txt | Out-Null也会产生相同的问题。


听起来有点像 http://stackoverflow.com/q/30918020/258523。 - Etan Reisner
内存耗尽部分听起来像一个错误。您可以通过避免使用管道/枚举 1000 万个对象,而是使用赋值、转换或属性枚举来显著减少 CPU 时间。 - Mathias R. Jessen
我无法使用提供的代码片段重现问题。 - Roman Kuzmin
@RomanKuzmin 你在使用PowerShell 2.0吗? - Phil
在V4和V5版本的5.0.10240.16384构建中,在执行第二个示例期间,我看到了超过2 GB的内存使用。 - Keith Hill
J House Consulting的《解决PowerShell垃圾回收漏洞》指向了这个问题,并建议在您的循环/任何操作中包含[System.GC] :: GetTotalMemory($true) | out-null - Ross Patterson
2个回答

8
这里有几件事情需要指出。首先,垃圾回收(GC)调用确实在管道中起作用。以下是一个只调用GC的管道脚本示例:
1..10 | Foreach {[System.GC]::Collect()}

这是脚本运行期间GC的perfmon图: enter image description here 然而,仅仅因为调用了GC并不意味着私有内存使用量会返回到脚本开始前的值。GC collect只会收集不再使用的内存。如果对象有一个根引用,它就不能被收集(释放)。因此,尽管GC系统通常不会像C/C++那样泄漏,但它们可能会拥有比它们应该更长时间地保留对象的内存储备。
通过内存分析器查看,似乎大部分多余的内存被参数绑定信息的字符串副本占据: enter image description here 这些字符串的根看起来像这样: enter image description here 我想知道是否有某种日志记录功能导致PowerShell保留了字符串化形式的管道绑定对象?
顺便说一句,在这种特殊情况下,将$null赋值给忽略输出要比其他方法更节省内存:
$null = GetDummyData

另外,如果您只需要编辑文件,请查看PowerShell Community Extensions 3.2.0中的Edit-File命令。只要不使用SingleString开关参数,它就应该具有高效的内存使用率。


1
我在Connect上报告了这个问题。如果您想要的话,请在那里为它投票 - https://connect.microsoft.com/PowerShell/feedback/details/1599091/event-logging-memory-hoard-when-processing-a-large-number-of-pipeline-objects - Keith Hill
虽然它并没有完全解决我的问题,但我认为这说明了这是一个只有微软才能修复的错误。感谢您对此进行了深入调查。 - Phil
没问题。我已经阐述了通过管道流式传输数据的好处,而不是将所有内容存储在变量中 - 没有意识到PowerShell本质上也在某种程度上这样做。 - Keith Hill

1

当你处理大型文本文件等不寻常任务时,通常会发现原生的 cmdlets 无法完全满足需求。个人而言,在 PowerShell 中使用 System.IO.StreamReader 编写脚本来处理大型文件会更加高效。

$SR = New-Object -TypeName System.IO.StreamReader -ArgumentList 'C:\super-huge-file.txt';
while ($line = $SR.ReadLine()) {
    Do-Stuff $line;
}
$SR.Close() | Out-Null;

请注意在ArgumentList中应使用绝对路径。对我来说,它似乎总是假定您在相对路径中的主目录中。

Get-Content只是简单地将整个对象作为数组读入内存,然后将其输出。我想它只是调用了System.IO.File.ReadAllLines()。

我不知道有什么办法告诉Powershell在完成后立即丢弃管道中的项,或者一个函数可能异步地返回项,因此它保留顺序。这可能是因为它没有自然的方法来告诉对象不会在以后被使用,或者稍后的对象不需要引用早期对象。

Powershell的另一个好处是,您经常可以采用C#答案。我从未尝试过File.ReadLines,但那看起来也很容易使用。


1
即使使用StreamReader方法,通过管道推送字符串仍会导致问题。此外,我认为Get-Content不会返回一个简单的字符串数组。我曾在PowerShell 2.0中使用它来处理数百兆字节的数据,而内存使用几乎可以忽略不计。 - Phil
1
@Phil StreamReader方法的关键在于你根本没有使用管道。你是逐行读取文件,而不是读取整个文件并将内容传输到管道中。你正在使用我所提供的“Do-Stuff $line;”这段代码完成你需要做的所有操作。问题是你无法同时访问两行,性能可能会更差,因为IO可能会成为瓶颈,但是你几乎不使用内存。谷歌搜索会揭示许多人在使用Get-Content时遇到内存问题。然而,“Get-Content | [...]”与“$x = Get-Content”的内存使用方式不同,这一点并不清楚。 - Bacon Bits
另一种方法是使用 foreach ($line in [system.io.file]::readlines('文件路径')) {do-stuff} - Robert Cotterman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接