From: "tenderlovemaking (Aaron Patterson)" Date: 2022-10-13T16:38:56+00:00 Subject: [ruby-core:110279] [Ruby master Bug#19041] Weakref is still alive after major garbage collection Issue #19041 has been updated by tenderlovemaking (Aaron Patterson). parker (Parker Finch) wrote in #note-3: > Thanks @byroot! I think this could be considered a bug in the documentation, since the [docs for WeakRef](https://ruby-doc.org/stdlib-3.1.2/libdoc/weakref/rdoc/WeakRef.html) imply that a `WeakRef` should be collected after a garbage collection. Perhaps we could call this corner-case out? > > I'm also curious to learn more about this case. (I'm unfamiliar with Ruby's use of registers and how that interacts with live objects and garbage collection. Ruby's garbage collector is conservative. Ruby objects that are allocated inside of C code must be kept alive. Lets look at a simple example: ```c void neat_function(void) { VALUE list = rb_ary_new(); rb_gc_start(); rb_ary_push(list, Qnil); } ``` The above C code is compiled in to machine code, but the array's life span is managed by the garbage collector. How can the garbage collector ensure that the array stays alive even after the call to `rb_gc_start()`? We humans can clearly see that the array is used in the C code, but the GC cannot read the C code. In fact there is no C code for the GC because it's all machine code now! So how can the GC keep the reference alive? It will scan the _machine registers_ as well as the _stack memory_ looking for addresses that _might_ be Ruby objects. The C compiler will probably have generated machine code that puts a reference to the local variable `list` in either a register or stack memory (there are cases where this doesn't happen, and we have to deal with it manually. See `RB_GC_GUARD`). The GC will look at the values stored in the machine registers, as well as any values in stack memory, then check if those values are within the bounds of Ruby's GC heap memory. If the address is inside the bounds, then the GC will consider the object to be alive. The GC cannot know if a pointer stored in a machine register will ever be used again, so it takes a _conservative_ approach and keeps the reference alive. This conservative approach can lead to the behavior that you are seeing with the weak reference: a value that nobody is actually using or referencing is kept alive because the GC can't know that fact for sure. The reference may or may not stay alive, but it depends on what machine code has executed, if the value is in the stack, if any registers have been overwritten, etc. I hope this helps. ---------------------------------------- Bug #19041: Weakref is still alive after major garbage collection https://bugs.ruby-lang.org/issues/19041#change-99566 * Author: parker (Parker Finch) * Status: Closed * Priority: Normal * ruby -v: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21] * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN ---------------------------------------- I am able to get into an infinite loop waiting for garbage collection to take a WeakRef. ### Reproduction Process The following script prints a "0", then a "1", and then hangs forever. I expect it to keep printing numbers. ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) GC.start while obj.weakref_alive? iterations += 1 end ``` ### Ruby Version I have tested this on Ruby 3.1.2, 3.1.0, 3.0.4, 3.0.0, 2.7.6, and 2.7.0 on macOS. All exhibit this behavior. ### Further Investigation #### Sleeping Sleeping before the garbage collection allows the loop to continue. The below exhibits the expected behavior: ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) (sleep(0.5); GC.start) while obj.weakref_alive? iterations += 1 end ``` However, sleeping _after_ the garbage collection still shows the buggy behavior (loop hangs): ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) (GC.start; sleep(0.5)) while obj.weakref_alive? iterations += 1 end ``` #### Running Garbage Collection Multiple Times Explicitly running garbage collection multiple times allows the loop to continue. This has the expected behavior, more numbers continue to be printed: ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) while obj.weakref_alive? GC.start GC.start GC.start end iterations += 1 end ``` However, with certain rubies, running those garbage collection calls in a `times` block prevents even a single iteration from completing. The following prints only "0" with ruby 3.0.4 on macOS, ruby 2.7.6 on macOS, and ruby 3.1.2 on linux (`ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]` on a virtual machine). It shows the expected behavior on ruby 3.1.2 on macOS. ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) 3.times { GC.start } while obj.weakref_alive? iterations += 1 end ``` -- https://bugs.ruby-lang.org/ Unsubscribe: