-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add an API to clear the valid-object bit (a.k.a. alloc-bit) #648
Comments
I don't think this is a good idea. MMTk is a garbage collector, and it is MMTk's responsibility to determine which objects are alive, and which are dead in a GC. It makes little sense that a runtime can tell MMTk which object is dead through an API. For your example, from MMTk's point of view, the object is kept alive in a GC, and the object will be considered as live until the next GC finds the object is unreachable. But the binding gets the object, wipes the object content, and asks MMTk to trace/scan the broken object. To me, this is completely the binding's wrong behavior. However, we probably want to make it clear that |
We had some discussion about this:
|
There is one more problem. If multiple inter-connected objects die at the same time, they will all be enqueued for finalization. However, before the next GC, the mutator may only have time to execute the finalizer for some of the object but not all. Even if we clear the VO bit of the objects that we finalized, other read-to-finalize objects are still added to the root set, and they can still reach the "invalid" objects. Ruby's approach doesn't have this problem, because it changes the type information of the object so that even if such objects are scanned, the scanning function can do a no-op. I am OK with doing both (1) clearing the VO bit, and (2) clearing the object header. |
If you have to do (2), does that mean (1) is not necessary? |
The issue with (2) is that it is not sound w.r.t. the "valid object" semantics. Some random memory may return a false positive. |
Separate point: The API should include symmetric functions to set and clear the vob: |
@qinsoon (1) is not necessary. Vanilla Ruby does only (2), and will refuse to report an object as root if the header is cleared. But having (1) allows a stack word to be filtered faster when calling |
If the "valid object" semantics means invalid objects cannot be reachable, then it may be impossible to maintain this semantics if some finalizable objects form cycles. If object A and B form a cycle, and they are both finalizable, and they die at the same time, then both of them are subject to finalization. If we call |
The fact that (2) is still necessary could be an indication that our old semantics about 'alloc bit' is sufficient enough for Ruby, and letting MMTk know about invalid objects will not solve the problem for Ruby. |
@qinsoon You are right. The existing 'alloc bit' API ( This proposed API change is an optimization so that we can rule out "invalid" objects from root set without looking at the header. This doesn't prevent "invalid" objects from being transitively reached, and it is why it is still necessary to clear the header, too. @steveblackburn may have a stronger opinion on this. |
From our discussion in the evening, the "cyclic dead objects" problem can be solved by requiring the GC to inspect the VO bit before attempting to scan an object. Alternatively, the VM can skip an object in Vanilla Ruby never sees invalid objects during tracing, because it calls finalizers during GC (in the sweeping phase), and does not postpone it to the mutator time. But that's too invasive for GC. |
Proposed API:
UPDATED: changed from "declare object dead" to "clear VO bit".
This API is only available when the
is_mmtk_object
feature is enabled.TODO list
Rationale: Object finalization interfering with object scanning when using conservative stack scanning
In Ruby, a
T_DATA
object is a heap object that contains a pointer to off-heap C struct, and has customizable marking and freeing functions (dmark
anddfree
). What complicates things is, the off-heap C struct may contain references to other heap objects. Therefore, itsdmark
function usually contain code that accesses the off-heap C struct, and thedmark
is called by GC to scan that object. Similarly, itsdfree
function may free the off-heap C struct and its sub-structures.The following is simplified pseudocode in Rust:
In vanilla Ruby, when GC decide that an object is unreachable, it calls
obj_free
on the object. ForT_DATA
objects, it calls thedfree
in the object. And then it just returns the space of the on-heap object back to the free-list.With MMTk, when a
T_DATA
is unreachable, it is resurrected and enqueued in the read-for-finalization queue. The mutator can poll from the queue, and callobj_free
on that object after the GC. Ideally, the object dies now, and it should never be reached again.However, since the stack scanning is conservative, the next GC may still find references to such finalized in-heap objects on the stack, for two reasons:
obj_free
left a reference to the object on the stack, and got picked up during the next conservative stack scanning.So during the next GC, the GC will try to scan it. Since it is a
T_DATA
, the GC will call itsdmark
function with itsptr
as argument. However,ptr
points to an object that is alread freed bydfree
.dmark
never expects it would be called on an object that is already finalized, and it accesses freed memory and crashes.(UPDATED) The root cause of the problem
The problem manifests when both of the following conditions hold.
In the case of Ruby,
obj_free
makes the object un-scannable.T_DATA
) have attached off-heap malloc-ed memory that contains object references, and must be scanned.obj_free
, the off-heap part is freed. When such an object is scanned again, the GC will load from freed memory.T_DATA
has customizabledmark
anddfree
makes the situation worse. The VM developers have no control over third-party types.This problem never happens in Java, because no Java function can interfere with object scanning.
How does vanilla Ruby avoid this problem?
Vanilla Ruby also uses conservative stack scanning. Vanilla Ruby uses free-list allocator. It decides whether a slot is free or is allocated by looking at the location of the object header. The header for all Ruby objects is:
If
flags
is zero, the GC will consider the slot occupied by this object is a free slot; otherwise, some bits offlags
will contain the type of the object (such asT_DATA
).When vanilla Ruby returns an object back to the free-list, it will clear its
flags
so subsequent GC will not consider it alive.So the criteria for Ruby to decide, during conservative stack scanning, whether a word is a valid Ruby object are:
flags
of the object in the slot is not zero.If both of them are true, that word is an object pointer.
How do I do it in mmtk-ruby?
My criteria for a word on the stack to be an object pointer are:
is_mmtk_object(obj)
returns true, andflags
of the object in the slot is not zero.I replaced the first criteria with an MMTk call. When an object is resurrected and finalized, MMTk still considers that object as a valid MMTk object.
I also make sure the
flags
is cleared after calling the finalizerobj_free
. By doing this, even ifis_mmtk_object(obj)
returns true, the conservative stack scanner will still refuse to report it to MMTk as a root if theflags
is zero.While that functions correctly just like vanilla Ruby, we can see the two criteria are redundant. We are looking at two places instead of one to decide if a word is a valid object reference.
Add an API to clear the alloc bit
If we add an API function to clear the alloc bit for
obj
, thenis_mmtk_object(obj)
will return false, and we don't need to look at its Ruby header any more.We add a function
clear_vo_bit(mmtk, obj)
. It clears the valid-object bit ("VO bit" for short. currently known as alloc-bit in mmtk-core). By clearing the VO bit, it has the following effects:is_mmtk_object(obj)
will return false. Conservative stack scanners use this API, and they will not put invalid objects into root set.Note that an invalid object can be transitively reachable from valid objects if multiple inter-connected objects die at the same time. If object A and object B contain references to each other, and both of them die at the same time, they are both added to the ready-to-finalize queue. If the mutator only finalized one of them, the other is still a valid object, and is alive (because finalizer is yet to be called). So it is not always possible to avoid "valid object pointing to invalid object".
To counter the cyclic dead objects problem, the GC should look at the VO bit before attempting to scan the object. By doing this, the VM doesn't even have a chance to look at the header because the GC never calls
Scanning::scan_object
if the VO bit is cleared.What if the object can be resurrected?
In short, if an object can be resurrected, the VO bit should not be cleared, and the VM must ensure the object remain valid after finalization (because it is resurrected).
Ruby never resurrects objects, at least not on the Ruby semantics level.
Currently I implement Ruby-style
obj_free
with Java-style object resurrection. In fact, thedfree
function provided by the C extension writer is written in C, and has access to the entire Ruby VM state. So in theory, thedfree
function can store the object somewhere in the VM which is considered part of the root, and the object would become accessible again. However, in the Ruby community, it is considered a mistake if the programmer makes an object accessible afterobj_free
. So it is at least sound to use this API for Ruby.Java may resurrect objects, but in Java, object scanning is not customisable by the programmer. If a Java object has an associated C structure, the common practice is to use a
long nativePointer;
field to hold the native pointer. It may call into JNI to free the object pointed bynativePointer
, but this happens duringfinalize()
, not during GC. Therefore, even a resurrected object is reached by GC again, thefinalize()
method is never called twice. Therefore, JVM never needs thisclear_vm_bit
API. p.s. Sincefinalize()
is deprecated for removal since Java 18, the modern approach is to useCleaner
which is based onPhantomReference
, which never resurrects objects in the first place.Alternative solution in Ruby
Ruby VM could, in theory, clear the
RData::ptr
field soobj_free
can refuse to calldmark
ifptr==NULL
. But this is not what the vanilla Ruby does, and it is slower than clearing theflags
header.The text was updated successfully, but these errors were encountered: