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=>#<Klass:0x00000615fa257108>}=>4}
found h1: {:a=>1}
# second run
found h1: {:a=>1}
found h4: {{:a=>#<Klass:0x0000032a2e2b2c20>}=>4}
found h3: {}
found h2: {:full_mark=>true, :immediate_sweep=>true}
# third run
found h4: {{:a=>#<Klass:0x0000067d0e3a2578>}=>4}
found h1: {:a=>1}
# fourth run (this when compiled with optimizations)
found h4: {{:a=>#<Klass:0x0000042b0e833c30>}=>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: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>