【问题标题】:Ruby Memory leak (MRI)Ruby 内存泄漏 (MRI)
【发布时间】:2017-04-17 22:11:23
【问题描述】:

我一定遗漏了一些东西,但我用 Ruby 编写的每个应用程序似乎都在泄漏一些内存。我使用 Ruby MRI 2.3,但我发现其他版本的行为相同。

每当我编写一个在循环中执行某些操作的测试应用程序时,它都会慢慢泄漏内存。

while true
   #do something
   sleep 0.1
end

例如,我可以写入数组,然后循环清理它,或者只是发送 http post 请求。

这只是一个例子,但我有很多这样的例子:

require 'net/http'
require 'json'
require 'openssl'

class Tester

    def send_http some_json
        begin
            @uri = URI('SERVER_URL')
            @http = Net::HTTP.new(@uri.host, @uri.port)
            @http.use_ssl = true
            @http.keep_alive_timeout = 10
            @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
            @http.read_timeout = 30
            @req = Net::HTTP::Post.new(@uri.path, 'Content-Type' => 'application/json')
            @req.body = some_json.to_json
            res = @http.request(@req)
        rescue Exception => e  
                puts e.message  
                puts e.backtrace.inspect  
        end
    end

    def run
        while true
            some_json = {"name": "My name"}
            send_http(some_json)
            sleep 0.1
        end
    end
end


Tester.new.run

我看到的泄漏非常小,每小时可以有 0.5 mb。

我使用 MemoryProfiler 和 GC::Profiler.enable 运行了代码,它表明我没有泄漏。所以它必须是2个选项:

  1. C 代码中存在内存泄漏。这可能是可能的,但我没有使用任何外部 gem,所以我很难相信 Ruby 正在泄漏。

  2. 没有内存泄漏,这是某种 Ruby 内存管理机制。问题是我可以目不转睛地看到记忆在增长。能长到什么时候?我需要等待多久才能知道是泄漏还是现在?

同样的代码在 JRuby 上运行得非常好,没有任何泄漏。

我很惊讶地看到一个帖子:

stack overlflow 来自乔·埃德加:

Ruby 的历史主要是作为文本处理的命令行工具 因此它重视快速启动和较小的内存占用。它 不是为长时间运行的守护进程/服务器进程设计的

如果那里写的是真的,并且 Ruby 没有将内存释放回操作系统,那么......我们总会有泄漏,对吧?

例如:

  1. Ruby 向操作系统请求内存。
  2. 操作系统为 Ruby 提供内存。
  3. Ruby 释放了内存,但 GC 仍然没有运行。
  4. Ruby 向操作系统请求更多内存。
  5. 操作系统为 Ruby 提供更多内存。
  6. Ruby 运行 GC,但为时已晚,因为 Ruby 已经询问了两次。
  7. 等等等等。

我在这里错过了什么?

【问题讨论】:

  • 您确定旧变量会超出范围吗?当 GC 收集垃圾时,旧变量似乎仍会被标记为“正在使用”。确保旧变量在测试时超出范围。考虑将循环操作嵌套在函数中 (while true; sleep 0.1; run_action; end)。
  • 在您的示例中,JSON 哈希中的临时字符串可能会保留在内存中,直到循环结束......但是您每次迭代都会创建新字符串。尝试将some_json = {"name".freeze: "My name".freeze} 放在循环之前。
  • 你是对的,我每次都创建新字符串,但为什么会出现问题?我希望 GC 收集它们。它应该不会导致内存泄漏,对吧?
  • 字符串是在循环范围内创建的。有可能(只是我的 2 美分)GC 只会在程序退出循环范围后才收集这些字符串......这是永远不会的。
  • 你有什么运气吗?

标签: ruby memory-leaks


【解决方案1】:

查看 GC 压缩和(未)冻结的字符串文字

“相同”的字符串不一定相同

在 Ruby 2.7.0 之前,主线 Ruby 没有压缩垃圾收集。虽然我不完全了解所有内部结构,但要点是某些对象couldn't be garbage collected。由于您使用的是 Ruby 2.3,因此在处理内存分配问题时要牢记这一点。其他非 YARV 虚拟机可能会以不同的方式处理其内部结构,这就是为什么在使用 JRuby 等替代引擎时可能会看到差异。

即使使用 Ruby 3.0.0-preview2,String literals aren't frozen by default,您当前的实现也是每十分之一秒创建一个具有唯一对象 ID 的新 String 对象。考虑以下几点:

3.times.map { 'foo'.__id__ }
#=> [240, 260, 280]

尽管 String 对象看起来相同,但 Ruby 实际上将每个对象分配为内存中的唯一对象。因为循环迭代不是范围门,这些 String 对象不能被 YARV 收集或压缩。

默认启用冻结字符串文字

您可能还有其他问题,但您最大的问题似乎是在无限循环中无限期地保留所有这些字符串文字。您也许可以通过使用frozen String literals 来解决您的垃圾收集问题(它不是内存泄漏)。考虑以下几点:

# run irb with universally-frozen string literals
RUBYOPT="--enable-frozen-string-literal" irb
3.times.map { 'foo'.__id__ }
#=> [240, 240, 240]

您也可以在代码中通过其他方式解决此问题,但减少保留在范围内的字符串文字的数量似乎是一个非常明智的起点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    • 1970-01-01
    • 1970-01-01
    • 2016-01-09
    • 2011-03-17
    • 2011-03-13
    • 1970-01-01
    相关资源
    最近更新 更多