ThreadFiber instance. So, for every ThreadFiber instance that got
garbage collected another one was created.
This behavior can lead to the finalizer queue filling up as quickly as
it's emptied under heavy memory pressure and the JVM grinding to a
There are several fixes here to ensure that fibers finalize in
a consistent and clean way.
* Direct delivery of the kill event to the target thread.
Without this change, the delivery process caused another
ThreadFiber object to come to life, keeping objects alive and
endlessly finalizing. Delivering the event via ThreadService
caused the finalizer thread to participate in Ruby thread events,
which we do not want. The new version delivers the event directly
via a special "dieFromFinalizer" method added to RubyThread.
* Make a better effort to kill the thread.
The original code just shut down the fiber's queue and delivered
a kill event. The new logic does both of those as well as
interrupting both the RubyThread and the java.lang.Thread in case
the fiber is waiting on some other blocking call. We could go the
next step and try to forcibly Thread.stop, but that has not been
* Ensure that the fiber thread never has on-stack references to
the fiber object
In order for the fiber object to be GCable and finalizable, there
must be no live references to it. This includes references on
the fiber thread's execution stack, such as those surrounding the
yield method's call to queue.pop. In the new logic, no hard
references to the ThreadFiber object appear anywhere in code,
and the fiber object is eventually GCed and finalized as expected.
* Test to ensure that fibers are getting cleaned up
This commit also adds a test that attempts to spin up 10000 fibers
and checks that they are cleaned up by comparing pre-test and
post-test native thread counts. This should help ensure we do not
regress on fiber lifecycle in the future.