[ruby-core:112546] [Ruby master Bug#19460] inline method cache won't let class be garbage collected

Issue #19460 has been reported by luke-gru (Luke Gruber). ---------------------------------------- Bug #19460: inline method cache won't let class be garbage collected https://bugs.ruby-lang.org/issues/19460 * Author: luke-gru (Luke Gruber) * Status: Open * Priority: Normal * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN ---------------------------------------- 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: ```ruby class A def do_something self.call_other # inline cache caches class A, so now A is marked every time cache is marked end def call_other end end a = A.new a.send(:do_something) # don't make normal call, use send so no inline cache used here 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}" ```ruby One solution to this would be if inline caches are inside methods, only mark them when the method itself is marked. I'm guessing they're marked independently right now. -- https://bugs.ruby-lang.org/

Issue #19460 has been updated by luke-gru (Luke Gruber). 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. ---------------------------------------- Bug #19460: Class not able to be garbage collected https://bugs.ruby-lang.org/issues/19460#change-101994 * Author: luke-gru (Luke Gruber) * Status: Open * Priority: Normal * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN ---------------------------------------- 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: ```ruby 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. -- https://bugs.ruby-lang.org/

Issue #19460 has been updated by byroot (Jean Boussier). I just had a look at this. Modifying your script a bit to dump the heap: ```ruby 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. ---------------------------------------- Bug #19460: Class not able to be garbage collected https://bugs.ruby-lang.org/issues/19460#change-102002 * Author: luke-gru (Luke Gruber) * Status: Open * Priority: Normal * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN ---------------------------------------- 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: ```ruby 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. -- https://bugs.ruby-lang.org/

Issue #19460 has been updated by byroot (Jean Boussier). 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. ---------------------------------------- Bug #19460: Class not able to be garbage collected https://bugs.ruby-lang.org/issues/19460#change-102003 * Author: luke-gru (Luke Gruber) * Status: Closed * Priority: Normal * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN ---------------------------------------- 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: ```ruby 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. -- https://bugs.ruby-lang.org/

Issue #19460 has been updated by luke-gru (Luke Gruber). Wow, interesting! Thanks for looking into it, I hadn't heard of harb before, pretty neat. ---------------------------------------- Bug #19460: Class not able to be garbage collected https://bugs.ruby-lang.org/issues/19460#change-102009 * Author: luke-gru (Luke Gruber) * Status: Closed * Priority: Normal * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN ---------------------------------------- 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: ```ruby 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. -- https://bugs.ruby-lang.org/
participants (2)
-
byroot (Jean Boussier)
-
luke-gru (Luke Gruber)