

Rubyのスクリプトが一度でも巨大なメモリを確保してしまうと、ずっとメモリを保持し続けてしまう現象に悩んでいます。

[code]

    require 'objspace'
    
    array = []
    (1..20).each_with_index do |i|
      array << 'a'*1000000
      puts "memsize: #{ObjectSpace.memsize_of_all / 1024} KB, rss: #{`ps -o rss= #{Process.pid}`.chomp} KB"
    
      if i % 5 == 0
        array = []
        GC.start
        puts "memsize: #{ObjectSpace.memsize_of_all / 1024} KB, rss: #{`ps -o rss= #{Process.pid}`.chomp} KB"
      end
    end
    
[/code]

こんな感じのコードを書くと、実行結果は以下のようになります。

[code]

    memsize: 4125 KB, rss:   8016 KB
    memsize: 5113 KB, rss:   9000 KB
    memsize: 6092 KB, rss:  10000 KB
    memsize: 7071 KB, rss:  11000 KB
    memsize: 8050 KB, rss:  11984 KB
    memsize: 2473 KB, rss:  11988 KB
    memsize: 3452 KB, rss:  11988 KB
    memsize: 4431 KB, rss:  11988 KB
    memsize: 5410 KB, rss:  11988 KB
    memsize: 6389 KB, rss:  11988 KB
    memsize: 7368 KB, rss:  11988 KB
    memsize: 2473 KB, rss:  11988 KB
    memsize: 3452 KB, rss:  11988 KB
    memsize: 4431 KB, rss:  11988 KB
    memsize: 5410 KB, rss:  11988 KB
    memsize: 6389 KB, rss:  11988 KB
    memsize: 7368 KB, rss:  11988 KB
    memsize: 2473 KB, rss:  11988 KB
    memsize: 3452 KB, rss:  11988 KB
    memsize: 4431 KB, rss:  11988 KB
    memsize: 5410 KB, rss:  11988 KB
    memsize: 6389 KB, rss:  11988 KB
    memsize: 7368 KB, rss:  11988 KB
    memsize: 2473 KB, rss:  11988 KB
    
[/code]

時間を伸ばしたり、外からpsコマンドを実行したりしましたが、どうやってもRSSが減りません。  
Rubyオブジェクトに割り当てられたメモリは解放されているのに、せっかく開放したメモリをOSには返却してくれないようです。  
このプロセスに限れば確保したメモリを再利用できるからいいのですが、サーバー上には他のプロセスも動いてるわけでやはり使わなくなったメモリはOSに戻したいです。  
Rubyの仕様上、難しいことでしょうか？


CC BY-SA 3.0
2016-03-26T07:36:19.417
23478
2019-01-09T10:23:48.830
2016-03-26T08:17:42.780
14950
14950
question
ruby linux c memory-leaks
Rubyのプロセスが掴んでいる実メモリを解放する手段はありますか？


Ruby ではオブジェクトを破棄しても free されにくい実装と記憶しています。(結構前の話なので今は変わっているかも?)
またCレベルの話をすると、free を呼んでもOSにメモリを返さず、次に malloc した際に再割当するような実装も多く存在します。

ということで、一般的にはメモリの解放によっても占有メモリが減らない前提でプログラミングすることになろうかと思います。

どうしてもということであれば、メモリが解放されるようなRubyのバージョン (もし存在すれば)
とmalloc/freeの実装の組み合わせを固定して開発する必要があるでしょう。


CC BY-SA 3.0
2016-03-26T14:26:46.867
23492
2016-03-27T13:08:37.780
2016-03-27T13:08:37.780
76
76
23478
answer


### TL; DR

ruby では最大利用メモリ量まで rss が増えたあとなかなか減らないので、メモリ増設するなり、スワップを積むなりしましょう。

### 本文

MRI の ruby についての話をします。

ruby はその他プロセスと同じように、静的領域、プログラム領域(ruby本体とか native
ビルドのライブラリとか)に加えて、ヒープとスタックを持ちます。 ruby のプロセスの rss
が増大する場合、だいたいはこのうちヒープが肥大化することによるものです。

ruby プログラムがヒープ上に保持するデータは、大きく分けて次の二つがあります。

  1. オブジェクト(RVALUE)を保持するためのページ。 GC モジュールとかで取得できるデータの大体は、このページについてのお話
  2. そこそこ大きいオブジェクトの中身(例: そこそこ大きい配列の実体)

それぞれのヒープ上のデータのメモリ確保の方法は、大体以下のようになっています。

  1. オブジェクト(RVALUE)のページ: ページの中の空きが足りないときに、新しいページをヒープ上で確保(malloc)する
  2. そこそこ大きいオブジェクトの中身: 直接メモリをヒープ上で確保(malloc)する。(オブジェクトの外側は 1 のページ上にある)

また、それぞれのメモリ解放のロジックは以下です。

  1. オブジェクト(RVALUE)のページ: すべてのオブジェクトが GC で解放処理がなされたされたページに対して、 free が呼ばれうる(おそらく、そのようなページが増えてくると)
  2. オブジェクトの中身: GC でオブジェクトの解放処理が実行される際に、 free される

そして、つまり、最終的に重要になるのは malloc と free がどのように振る舞うか、になります。そして、 ruby がデフォルトで採用する
glibc の malloc/free では、 free されたメモリを OS に返却するには、次の条件が必要になります。

  * (メイン)ヒープは、その先頭のメモリが解放済みである場合のみに、先頭を切り詰める形でヒープを縮める処理が実行されうる。

この挙動があるので、 ruby (というより、大体の動的に malloc/free する系の長期実行プログラム)
では、長く実行していると、最大利用メモリ量ぐらいまではメモリが確保されて、そのままなかなか小さくならない、といったような挙動になります。

たまにしかメモリをたくさん使わない、長期実行のプログラムがある場合、個人的には swap を積んで、うまく swap in/out
してくれるのを期待する、が現実解なのではなかろうか、と思っています。


CC BY-SA 4.0
2019-01-08T00:11:51.513
51809
2019-01-09T10:23:48.830
2019-01-09T10:23:48.830
754
754
23478
answer


Ubuntu 18.04 LTS on Windows Subsystem for Linux on Windows 10 Pro 1809 (64bit)
環境で1.9～2.6のRubyについて傾向を調べました。Rubyはrbenv-installでインストールしています。

結論から言うと各バージョンともrssの減少は確認できました。しかし、増減パターンやメモリ量はbバージョンによってバラバラで傾向は掴めませんでした。

  * 1.9.3-p551  
1回目は増えて減る山を描くが、2回目以降は増えたままで、最後だけ少し減る。  
最終値は初期値よりは多い。

  * 2.0.0-p648  
山が4つになるパターンを描き、最終値は初期値とほぼ同じ。  
初期値は1.9.3-p551よりも多いが、最大値や最終値は1.9.3-p551よりも少ない。

  * 2.1.10  
2.0.0-p648とほぼ同じ。

  * 2.2.10  
2.1.10とほぼ同じだが、消費量が約1.1倍ぐらいに増加。

  * 2.3.8  
1.9.3-p551とほぼ同じパターンで増減する。  
全体として、消費量は2.2.10の約1.7倍ぐらいに増加。

  * 2.4.5  
2.3.8とほぼ同じだが、2.3.8の0.7倍ぐらいに減少。2.2.10よりはちょっと多い。

  * 2.5.3  
2.4.5とほぼ同じだが、2.4.5の1.3倍ぐらいに増加。2.3.8よりはちょっと少ない。

  * 2.6.0  
山が4つになるパターンを描き、最終値は初期値とほぼ同じ。  
ただし、初期値は2.5.3の最終値と同じぐらいと一番大きい。  
最大値は2.5.3とほぼ同じ。  
`--jit`付きの場合も同じ波形になる。

各バージョンのリリースノートを見るとGCやメモリ周りのパフォーマンス改善は度々行われており、mallocの取り方、freeの取り方なども変わっていると思われます。なお、jemallocを有効にすると、また違った結果になるバージョンがありましたので、断片化のされやすさなども影響があると思います。

ということで、今回は単純に質問者さんの環境だとOS側がメモリを回収しなかっただけと思われます。より大きな容量(数百MB単位)でも本当に全くメモリが解放されないのかを確認してみてください。


CC BY-SA 4.0
2019-01-08T12:38:34.830
51824
2019-01-08T12:38:34.830
7347
23478
answer