-
Notifications
You must be signed in to change notification settings - Fork 738
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
Mark receiver objects of an instance method #20384
Conversation
Marking the receiver object of an instance method ensures that it is not collected by the garbage collector (GC) while the instance method is executing. If the receiver object is not marked, there is a risk that the GC might collect it. In such cases, finalization mechanisms, such as PhantomReferences and Cleaners, could indicate that the object has been collected while it is still in use. This issue is resolved if the receiver object is marked during the execution of its instance method. Fixes: ibmruntimes/Semeru-Runtimes#93 Signed-off-by: Babneet Singh <[email protected]>
I don't believe this is correct - it's legal for bytecodes to overwrite the receiver slot with an int value (which is handled correctly by the local mapper). I suggest you create a test which stores -1 into local slot 0 and then calls System.gc() a couple of times. I strongly suspect this will crash with this change. I believe the code you replaced is also incorrect - the scenario just doesn't come up very often. |
The test should be run -Xint - I believe the JIT detects the receiver overwrite and simply abandons compilation of the method. |
The "early" (actually on-time) collection of the receiver is completely valid. If there are any references to the object in any stack or reachable object it will not be collected. The issue we've seen in the past is NIO fetching a long memory address from the object as the last use of that object, then the object gets collected and the finalizer runs freeing the native memory before returning control to the method which fetched the address. Use of that address then crashes. This is why the keepAlive/reachabilityFence was added to such methods. |
The issue asserts that the RI works (though this problem is likely so intermittent I'm not sure that's true). Try running the suggested test on the RI - unless they're explicitly copying the receiver into a hidden live slot, they should crash too. This would be another potential solution for the issue, though adding another hidden slot to stack frames will slow us down a bit and we already have 2 or 3, so may be complex to implement. |
Another option would be to update the local mapper to detect the overwrite. If one does not occur, keep the receiver alive. If an int value is written over it, proceed as we do today. |
After a bit more thought, I think the best solution is to move your code to the top of the function - declare the receiver as "read as object" from the start, and let the mapper override that along the appropriate paths if an int overwrite occurs. We could do this for all of the object arguments if desired (this is what the debug mapper does, which is why debug mode works). Assuming that the new test fails as I expect with the current change, I think it would be worth adding the test into the builds. |
Modified the provided test, and it doesn't crash.
It's run with
The RI passes consistently with the modified test. On the other hand, OpenJ9 fails consistently.
Doesn't work, it seems like the mapper overwrites it with an int.
Might have applied my fix or nogc option previously, which would have made the debug mode work. Is there a simple way to verify if our behavior is correct w.r.t the RI i.e. the object is being collected correctly? |
I'd like to see a standalone test for this, which cannot be done solely with the java compiler. Probably the easiest way to do it would be to have a java method like:
and hexedit the class file so the second store is into slot 0 instead of slot 1. |
Did you do it after the initial zeriong of the resultArray? |
Yes, and before
|
Is that not the correct behaviour? Or are you saying it's declared int without the int write to the receiver? |
re #20384 (comment): Using
Yes, it is the correct behavior. Moving the fix before |
If you have actually written -1 to the receiver and marked it as object, then a crash is the only "correct" behaviour - anything else means the test is not operating correctly. |
You ran the test with this change in place? |
I had tried the change where the fix was moved before
|
OK that's what I expected (and proves that the current change is not correct). I suggest you look at the debug mapper to see when/how it marks the object arguments. I maintain that the correct fix is to mark the receiver (and I recommend the other object arguments) as object before starting the mapper. I don't know why it didn't just work, so some debugging may be necessary. |
babsingh@c85a8ea implements the above suggestion. The object still gets collected early.
babsingh@687a002 enables the debug mapper. With the debug mapper, the object stays alive and is no longer collected early.
Overall, both implementations work correctly. Even though
|
If the code at the start doesn't fix the issue, then there's no point pushing that. Given that we don't want to modify the local mapper too much, I think the best solution might be to check for any stores back to slot 0 during the bytecode walk, and if there are none, do your existing code at the end. We should still (in the future) consider doing this for all of the incoming arguments, as |
We may also want to consider simply always using the debug mapper. I doubt we get any real benefit from the aggressive collection of locals during a method's run. @tajila What do you think? |
I think we can give this a try, if there is no noticeable difference in key benchmarks then we should make the switch. |
@babsingh Please give this a try and get some measurements, |
I implemented the above suggestion in babsingh@68ae493. @gacholio Can you confirm if it was correctly implemented? This re-introduces the earlier crash from #20384 (comment).
Yes, I can give it a try. Which benchmarks are we targeting (e.g. Liberty startup, footprint, throughput)? |
Yup these ones. |
Let's wait for the debug mapper results. I see no reason why any crash would be introduced by your change. |
Opened #20569 to make OpenJ9's default mapper. I have run the following benchmarks: Liberty DT8/DT10 Startup & Footprint, JRuby and Nashorn. No performance difference is seen between |
Marking the receiver object of an instance method ensures that it
is not collected by the garbage collector (GC) while the instance
method is executing.
If the receiver object is not marked, there is a risk that the GC
might collect it. In such cases, finalization mechanisms, such as
PhantomReferences and Cleaners, could indicate that the object has
been collected while it is still in use. This issue is resolved if
the receiver object is marked during the execution of its instance
method.
Fixes: ibmruntimes/Semeru-Runtimes#93