Bug #19460
closedClass not able to be garbage collected
Description
I'm working on something where I need to remove a class and release all memory related to that class.
I've stumbled upon a limitation where in some instances I cannot get the class to be GC'd. The problem
(I think) is that inline method caches cache the class, and even if the class would otherwise be gone, it stays.
For example:
class A
def do_something
end
end
a = A.send(:new)
a_id = A.object_id
a = nil
Object.send(:remove_const, :A)
# A should be able to be released now.
10.times { GC.start }
a_ref = ObjectSpace._id2ref(a_id) rescue nil
puts "a_ref: #{a_ref.class}"
# we get NilClass, it is released
However: using A.new
above, it can't be GC'd.
If there is an initialize method (even empty) it can't be GC'd.
If there's a method call inside the class to another instance method on self ex: self.do_something
, it can't be GC'd.
I'm not sure exactly what is going on but it looks like an inline cache marking issue. Maybe caches are getting marked, even inside methods where the method itself doesn't get marked.
Updated by luke-gru (Luke Gruber) over 1 year ago
- Subject changed from inline method cache won't let class be garbage collected to Class not able to be garbage collected
- Description updated (diff)
Updated by luke-gru (Luke Gruber) over 1 year ago
I should also note I don't know if this is a bug or if this is simply not possible to do in Ruby. I know using load
and remove_const
(auto-reload feature of Rails, for example) is
possible but that's different.
Updated by byroot (Jean Boussier) over 1 year ago
I just had a look at this.
Modifying your script a bit to dump the heap:
require 'objspace'
class A
def do_something
end
end
a = A.send(:new)
a_id = A.object_id
a = nil
Object.send(:remove_const, :A)
# A should be able to be released now.
10.times { GC.start }
a_ref = ObjectSpace._id2ref(a_id) rescue nil
puts "a_ref: #{a_ref.class}"
# we get NilClass, it is released
ObjectSpace.dump_all(output: File.open("/tmp/foo.json", "w+"))
$ rg -F '"name":"A"' /tmp/foo.json
17466:{"address":"0x1024f0dc8", "type":"CLASS", "shape_id":13, "slot_size":160, "class":"0x1024f0d28", "variation_count":0, "superclass":"0x1024dfe60", "name":"A", "references":["0x1024dfe60", "0x1022d38b0", "0x10249e988", "0x1022d3798", "0x1022d8dd8"], "memsize":392, "flags":{"wb_protected":true, "old":true, "uncollectible":true, "marked":true}}
So the class has slot 0x1024f0dc8
.
Then using harb
$ ./harb /tmp/foo.json
parsing (100%)
updating references (100%)
generating dominator tree (100%)
harb> rootpath 0x1024f0dc8
root path to 0x1024f0dc8:
ROOT (machine_context)
0x1022d7d98 (IMEMO: env)
0x1024f0dc8 (CLASS: A)
It's directly held in the machine_context
, so I think it's just bad luck because the reference stay in an unused register saved on the stack.
I also modified the script a tiny bit more to see something, and fixed it by accident:
require 'objspace'
class A
def do_something
end
end
a = A.send(:new)
a_id = A.object_id
puts ObjectSpace.dump(A)
a = nil
Object.send(:remove_const, :A)
# A should be able to be released now.
10.times { GC.start }
a_ref = ObjectSpace._id2ref(a_id) rescue nil
puts "a_ref: #{a_ref.class}"
# we get NilClass, it is released
So yeah, I'm 99% certain you simply hit the issue that the Ruby GC isn't 100% precise. It has to scan the registers and just because a pointer is in a register doesn't mean it's in use.
Updated by byroot (Jean Boussier) over 1 year ago
- Status changed from Open to Closed
Closing as I don't think it's a bug per say, but happy to re-open if anyone thinks otherwise.
Updated by byroot (Jean Boussier) over 1 year ago
- Related to Bug #19041: Weakref is still alive after major garbage collection added
Updated by luke-gru (Luke Gruber) over 1 year ago
Wow, interesting! Thanks for looking into it, I hadn't heard of harb before, pretty neat.