-
Notifications
You must be signed in to change notification settings - Fork 76
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 MemberName finalizer to mark clazz for MemberName list pruning #759
Conversation
This prevents a memory leak in the maintenance of per-class lists used to find affected MemberName objects in case of class redefinition.
22, 21, and 17 to follow |
@Override | ||
protected void finalize() { | ||
if (null != clazz) { | ||
MethodHandleNatives.markClassForMemberNamePruning(clazz); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finalize()
methods are deprecated; this should use a "cleaner" - see java.lang.ref.Cleaner
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See also https://openjdk.org/jeps/421.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll create a Cleaner
and register with it on paths that call native methods of MethodHandleNatives
that could add the MemberName
to the per-class list. That's init
, resolve
, and (in Java 17) getMembers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found that Cleaner
was noticeably harder to use than a finalizer for this use case:
- There's no easy way to make it apply to every instance.
MemberName
has many constructors, one of which is empty and commented to say that the VM will initialize the object, so it's not clear whether in that case that constructor even runs. - The cleaning action can't even be created until the class is known anyway (without moving the
clazz
field into the cleaning action entirely). This will makegetMembers
in Java 17 especially annoying: the cleaning actions can't be created before the call becauseclazz
hasn't been set yet, but it looks like it's possible for the native code to initialize one or moreMemberName
instances and then throw while trying to initialize another. - It was easy to accidentally prevent some
MemberName
instances from being reclaimed and thereby also prevent anonymous classes from being unloaded.
On top of that, it takes more code to accomplish the same thing.
For these reasons, if possible I'd like to stick with using a finalizer at least in all of the JDK versions where we can be sure that finalization is not being removed.
That said, I worked through those problems in an attempt to future-proof this code for jdknext (at least), and I came up with this commit.
The reason I haven't updated this PR is that the mere presence of the following line is causing some test failures for me:
private static final Cleaner CLEANER = Cleaner.create();
In particular, thrstatetest_0 and thrstatetest_1 say "ALL TESTS COMPLETED AND PASSED" but then fail with "Failed to destroy JVM". And VmArgumentTests_0 fails testMemoryLeaks
.
Please note that the logic in this PR is an important component of eclipse-openj9/openj9#19187, which I'm trying to get into 0.44.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should mention that I doubt that I would be able to debug these test failures in time, and I also doubt that I'm the right one to debug them anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it did and the class got unloaded before the cleaning action ran, then we'd have a dangling pointer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are only three places that write to the clazz
field and only via init()
or MethodHandleNatives.init()
might it be non-null
. Those would seem to be the only places where registering a cleaner is necessary.
It feels risky to embark on the path to a solution involving a cleaner at this late stage approaching the 0.44.0 release. I'm ready to accept the "finalizer" approach if we can agree that it should be a priority to switch to using a cleaner as soon as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are only three places that write to the
clazz
field and only viainit()
orMethodHandleNatives.init()
might it be non-null
. Those would seem to be the only places where registering a cleaner is necessary.
Yes, I believe I did work out the places where it's necessary to create the cleaning action. Rather than look for everywhere clazz
is set, I looked for everywhere the MemberName
might get added to the per-class list (from eclipse-openj9/openj9#19187) that would need to be pruned. So that's just MethodHandleNatives.init()
and MethodHandleNatives.resolve()
(in Java 21+).
My main points here are:
- The code using a
Cleaner
was harder to write and is harder to understand than the finalizer. - Merely creating a
Cleaner
causes some test failures that I'm ill-equipped to deal with myself. - Yes, finalization is deprecated for removal as of Java 18, but that means that it won't be removed in Java 17, and it's quite possible that we could be confident that it won't be removed in 21 (or maybe even 22) either.
None of that is to say that I love the finalizer though 😅
It feels risky to embark on the path to a solution involving a cleaner at this late stage approaching the 0.44.0 release. I'm ready to accept the "finalizer" approach if we can agree that it should be a priority to switch to using a cleaner as soon as possible.
I agree that it should be a priority to get rid of the finalizer soon. From discussion with @tajila, @babsingh, @amicic, and @dmitripivkine, there should be a better fix that modifies the GC to eliminate this finalization step and the separate pruning pass over the list. There are different potential design points for how that should work, but none of them include finalization or a cleaner. Instead, the GC will do the necessary cleanup directly. I'll leave it up to the GC and VM teams to decide whether it's worth switching to a cleaner before that point, only for the cleaner to itself be replaced. (However, I wouldn't personally recommend switching to a cleaner in versions where finalization will remain enabled, since IMO for this particular code it's not an improvement.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A solution that needs neither finalization nor a cleaner sounds good (as long as it doesn't suffer from excessive delay recovering resources). If we think it will arrive relatively soon (say before the next release) then I would agree we don't need to pursue the cleaner approach.
Cleaners can yield more timely recovery of resources than an approach that depends on running finalization code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are in the process of designing a solution that introduces a new mechanism to enable the creation of a weak object list managed by the GC. This will allow us to create a per class member name list that is automatically pruned by the GC. The complexity of this solution means we have no chance of getting it into 0.44 (which is important as stakeholders are waiting for known issues to be addressed). So we have gone with the solution in this PR for the time being.
As Devin stated, the long term solution will not rely on finalization or cleaners, both of which have issues.
@keithc-ca Do you have further comments? |
As I reviewed this again today (I had been waiting for eclipse-openj9/openj9#19187 to be merged), I am a little puzzled that the finalizer provides the |
Jenkins test sanity alinux jdknext |
I'm passing only the class because that's all that's needed. I would tend (or at least try) not to pass more information than necessary unless there's some particular benefit to doing so That decision has already been helpful in this PR: it helped me to try |
My point is that by passing the class, the native code has to consider all The native method is new so I don't understand what "change" your referring to. |
The native code just flags the class so that we know (later) to look through its list for defunct entries. As it stands, it wouldn't be much help to know the specific
I went through the exercise to change what I had in this PR to use |
In fact, without additional state to correlate them, we wouldn't even be able to recognize the corresponding list node, since by the time the finalizer is running, the weak reference will have already been cleared |
I don't think use of a weak reference in the cleaner approach was appropriate, but as all this is going away (soon!), I wasn't going to worry about that. |
This prevents a memory leak in the maintenance of per-class lists used to find affected
MemberName
objects in case of class redefinition.This depends on eclipse-openj9/openj9#19187