From: merch-redmine@... Date: 2019-10-26T02:21:15+00:00 Subject: [ruby-core:95557] [Ruby master Bug#16278] Potential memory leak when an hash is used as a key for another hash Issue #16278 has been updated by jeremyevans0 (Jeremy Evans). Here's a modified version of your script, fixing the issue where `$id_h4 = h3.object_id`, and showing the actual contents of the objects found with those ids: ```ruby require 'objspace' class Klass; end def create h1 = { :a => 1 } h2 = { Klass.new => 2 } h3 = { [Klass.new] => 3 } h4 = { { :a => Klass.new } => 4 } $id_h1 = h1.object_id $id_h2 = h2.object_id $id_h3 = h3.object_id $id_h4 = h4.object_id nil end 10.times do GC.start(full_mark: true, immediate_sweep: true) end create 10.times do GC.start(full_mark: true, immediate_sweep: true) end ObjectSpace.each_object(Hash) do |h| puts "found h1: #{ObjectSpace._id2ref($id_h1)}" if h.object_id == $id_h1 puts "found h2: #{ObjectSpace._id2ref($id_h2)}" if h.object_id == $id_h2 puts "found h3: #{ObjectSpace._id2ref($id_h3)}" if h.object_id == $id_h3 puts "found h4: #{ObjectSpace._id2ref($id_h4)}" if h.object_id == $id_h4 end ``` Here's the output from running it a few times ``` # first run found h4: {{:a=>#}=>4} found h1: {:a=>1} # second run found h1: {:a=>1} found h4: {{:a=>#}=>4} found h3: {} found h2: {:full_mark=>true, :immediate_sweep=>true} # third run found h4: {{:a=>#}=>4} found h1: {:a=>1} # fourth run (this when compiled with optimizations) found h4: {{:a=>#}=>4} ``` It does seem to always retain `h4`, but that could be due to the conservative GC. When compiled without optimizations (`-O0`), `h1` seems to always be retained as well. On my system, `h4` seems to always be retained in ruby 2.1 and 2.3-2.7, not always retained in 2.2 and the branch I have to fully separate keyword arguments from positional arguments (https://github.com/jeremyevans/ruby/tree/r3). As those changes seem unrelated, I'm going to assume those versions just change the memory layout such that the conservative GC no longer retains `h4`. The fact that `h1` is always retained when compiled without optimizations lends weight to this theory. I agree with @mame that unless you are seeing unbounded memory growth, this is not a bug, just a result of the conservative GC. ---------------------------------------- Bug #16278: Potential memory leak when an hash is used as a key for another hash https://bugs.ruby-lang.org/issues/16278#change-82335 * Author: cristiangreco (Cristian Greco) * Status: Open * Priority: Normal * Assignee: * Target version: * ruby -v: ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin18] * Backport: 2.5: UNKNOWN, 2.6: UNKNOWN ---------------------------------------- Hi, I've been hitting what seems to be a memory leak. When an hash is used as key for another hash, the former object will be retained even after multiple GC runs. The following code snippet demonstrates how the hash `{:a => 1}` (which is never used outside the scope of `create`) is retained even after 10 GC runs (`find` will look for an object with a given `object_id` on heap). ```ruby # frozen_string_literal: true def create h = {{:a => 1} => 2} h.keys.first.object_id end def find(object_id) ObjectSpace.each_object(Hash).any?{|h| h.object_id == object_id} ? 1 : 0 end leaked = create 10.times do GC.start(full_mark: true, immediate_sweep: true) end exit find(leaked) ``` This code snippet is expected to exit with `0` while it exits with `1` in my tests. I've tested this on multiple recent ruby versions and OSs, either locally (OSX with homebrew) or in different CIs (e.g. [here](https://github.com/cristiangreco/ruby-hash-leak/commit/285e586b7193104989f59b92579fe8f25770141e/checks?check_suite_id=278711566)). Can you please help understand what's going on here? Thanks! -- https://bugs.ruby-lang.org/ Unsubscribe: