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
8277072: ObjectStreamClass caches keep ClassLoaders alive #6375
Conversation
👋 Welcome back rkennke! A progress list of the required criteria for merging this PR into |
Webrevs
|
If the intent of this change is to alter the lifetimes of the objects in question in a meaningful way, I recommend a CSR for the behavioral compatibility impact. |
It would be hard for application code to observe this change: before the change, a ClassLoader and its classes could be lingering in the cache longer than necessary, even if otherwise not reachable. With the change, they would be reclaimed as soon as they become unreachable. This could only be observed, if application code holds onto ClassLoader or Class instances via Weak or PhantomReference, and even then I am not sure if that qualifies as 'meaningful'. |
Ping? Can I please get another review? Thanks! |
Hi Roman, What your patch changes is the following:
into:
While it is true that when the Class object used in a weak key is not reachable any more by the app, it is not sensible to hold on to the value any longer so in that respect SoftReference is to "storng" of a weakness. But while the Class object is still reachable by the app, the app expects to obtain the ObjectStreamClass (the value) from the cache at least most of the time. If you change the SoftReference into WeakReference, the ObjectStreamClass might get GC-ed even while in the middle of stream deserialization. ObjectStream class pre-dates java.lang.invoke (MethodHandles), so it uses its own implementation of weak caching. But since MethodHandles, there is a class called ClassValue that would solve these problem with more elegance, because it ties the lifetime of the value (ObjectStreamClass) to the lifetime of the Class key (Class object has a strong reference to the associated value) while the Class key is only Weakly referenced. |
I don't quite understand this: If the Class object is still reachable by the app, 1. a weak reference would not get cleared and 2. the Class's ClassLoader would not get unloaded. Conversely, if it's not reachable by the app anymore, then the key in the cache would get cleared, and we would not find the ObjectStreamClass anyway. Except that the OSC holds onto the Class object by a SoftReference, so it would effectively prevent getting cleared (and get unloaded).
Hmm, sounds nice. Do you think that would work in the context of OSC? |
A patch is worth a thousand words. Here's what I meant when I said this could be elegantly solved with ClassValue: Note this is not tested. Just an idea. |
...but the ObjectStreamClass instance could still get GC-ed, because it is held in the map using WeakReference. The fact that associated Class is still reachable does not mean that the ObjectStreamClass instance is! |
|
Very nice! |
I think most "hard work" (the tests) is still yours. I just removed a chunk of legacy code and replaced it with one-liners :-). I'm glad that this actually works! Please, continue... |
...I think that you could remove now obsolete java.io.ObjectStreamClass.EntryFuture nested class. It's not used any more. It would be nice to follow-up this patch with patches that make use of ClassValue also for:
...this way the common static machinery like:
|
Done.
I filed: https://bugs.openjdk.java.net/browse/JDK-8278065 Thanks! |
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.
Without the use of SoftReference, memory pressure won't release any of the cached info.
That seems to swing the other way from overly aggressively freeing memory with the WeakReference (and needing to recompute) as the change originally proposed.
Its hard to tell in what environments it might be observed.
reflector = new FieldReflector(matchFields(fields, localDesc)); | ||
var oldReflector = clReflectors.putIfAbsent(key, reflector); | ||
if (oldReflector != null) { | ||
reflector = oldReflector; | ||
} |
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.
Map.computeIfAbsent(key, () -> new FieldReflector(matchFields, localDesc));
might be more compact.
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.
That would be nicer, indeed. Problem is that matchFields throws an InvalidClassException, and that would have to get passed through the lambda.
Also, that problem is pre-existing and not related to the change.
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.
Yes, I did computeIfAbsent() originally just to find out handling check exception/wrapping/unwrapping would make the code much more complex.
TestClassLoader myOwnClassLoader = new TestClassLoader(); | ||
Class<?> loadClass = myOwnClassLoader.loadClass("ObjectStreamClass_MemoryLeakExample"); | ||
Constructor con = loadClass.getConstructor(); | ||
con.setAccessible(true); |
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.
Isn't the constructor already public?
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.
Yes, but:
test TestOSCClassLoaderLeak.run(): failure
java.lang.IllegalAccessException: class TestOSCClassLoaderLeak cannot access a member of class ObjectStreamClass_MemoryLeakExample with modifiers "public"
Right. The problem with the original code was that the softreference would keep the class from getting unloaded, except when under pressure. Now that the cached value is tied to the object lifetime using ClassValue, we can relatively easily use SoftReference to also make it sensitive to memory pressure. I factored this code out into its own class to avoid making a mess, and to be able to reuse it in subclassAudits (see #6637). |
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.
ObjectStreamClass may have an unnecesary import of SoftReference
.
Otherwise, looks good to me.
@rkennke This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be:
You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 180 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
Thanks, @RogerRiggs! |
reflector = new FieldReflector(matchFields(fields, localDesc)); | ||
var oldReflector = clReflectors.putIfAbsent(key, reflector); | ||
if (oldReflector != null) { | ||
reflector = oldReflector; | ||
} |
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.
Yes, I did computeIfAbsent() originally just to find out handling check exception/wrapping/unwrapping would make the code much more complex.
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.
Thanks for the updates. LGTM
} | ||
|
||
assertNotNull(ObjectStreamClass.lookup(TestClass.class).getFields()); | ||
} |
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 don't quite get this test. It loads ObjectStreamClass_MemoryLeakExample class from child class loader, constructs an instance from it and calls .toString() on an instance. This is just to indicate that the class initializer of that class did lookup an ObjectStreamClass instance for Test class loaded by the same child loader. OK so far...
Then there is this loop that tries to exhibit some memory pressure while constantly looking up OSC for another Test class (this time loaded by parent class loader) presumably to trigger clearing the SoftReference(s) of both classes loaded by child ClassLoader.... Is this what the loop was supposed to do?
And finally there is an assertNotNull that does another lookup for OSC of Test class loaded by parent class loader, retrive its fields and check that the returned OSC instance as well as the field array are not null. This will always succeed regardless of what you do before the assertion.
I don't think you need any custom class loading to verify the correctness of caching. The following two tests pass on old implementation of OSC. Do they pass on the new one too?
public class ObjectStreamClassCaching {
@Test
public void testCachingEffectiveness() throws Exception {
var ref = lookupObjectStreamClass(TestClass.class);
System.gc();
Thread.sleep(100L);
// to trigger any ReferenceQueue processing...
lookupObjectStreamClass(AnotherTestClass.class);
Assertions.assertFalse(ref.refersTo(null),
"Cache lost entry although memory was not under pressure");
}
@Test
public void testCacheReleaseUnderMemoryPressure() throws Exception {
var ref = lookupObjectStreamClass(TestClass.class);
pressMemoryHard(ref);
System.gc();
Thread.sleep(100L);
Assertions.assertTrue(ref.refersTo(null),
"Cache still has entry although memory was pressed hard");
}
// separate method so that the looked-up ObjectStreamClass is not kept on stack
private static WeakReference<?> lookupObjectStreamClass(Class<?> cl) {
return new WeakReference<>(ObjectStreamClass.lookup(cl));
}
private static void pressMemoryHard(Reference<?> ref) {
try {
var list = new ArrayList<>();
while (!ref.refersTo(null)) {
list.add(new byte[1024 * 1024 * 64]); // 64 MiB chunks
}
} catch (OutOfMemoryError e) {
// release
}
}
}
class TestClass implements Serializable {
}
class AnotherTestClass implements Serializable {
}
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.
The test was a rather crude (but successful) attempt to demonstrate the ClassCastException. Thanks for providing the better testcase. I verified that it succeeds with this PR, and also demonstrates the ClassCastException if I revert my previous change in ClassCache. I pushed this new test, and removed my old one.
I think this looks good now. Thanks for following through all the changes... |
This should go to openjdk/jdk18 now, right? Can I simply push it there, or do I need to re-open a PR against jdk18? |
This is a P4 bug. If the priority is correct, it does not meet the criteria to get it into JDK 18 during RDP1, as indicated in JEP 3. If this is objectively a P3 bug, and really does need to go into JDK 18, then you will need to close this PR and open a new pull request in the jdk18 repo. |
Hmm, good question. It is kind-of leaking: current implementation prevents unloading of classes that are referenced from the OSC caches, unless memory pressure is high enough to trigger soft-ref-cleaning. Does it qualify for P3 ("Major loss of function."), or even P2 ("Crashes, loss of data, severe memory leak.")? We have users hitting this problem under different circumstances, I'd say it qualifies for P3. Opinions? See: |
This fix hasn't had any bake-time and might have some effects that aren't immediately noticeable. |
/integrate |
Going to push as commit 8eb453b.
Your commit was automatically rebased without conflicts. |
The caches in ObjectStreamClass basically map WeakReference to SoftReference, where the ObjectStreamClass also references the same Class. That means that the cache entry, and thus the class and its class-loader, will not get reclaimed, unless the GC determines that memory pressure is very high.
However, this seems bogus, because that unnecessarily keeps ClassLoaders and all its classes alive much longer than necessary: as soon as a ClassLoader (and all its classes) become unreachable, there is no point in retaining the stuff in OSC's caches.
The proposed change is to use WeakReference instead of SoftReference for the values in caches.
Testing:
Progress
Issue
Reviewers
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.java.net/jdk pull/6375/head:pull/6375
$ git checkout pull/6375
Update a local copy of the PR:
$ git checkout pull/6375
$ git pull https://git.openjdk.java.net/jdk pull/6375/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 6375
View PR using the GUI difftool:
$ git pr show -t 6375
Using diff file
Download this PR as a diff file:
https://git.openjdk.java.net/jdk/pull/6375.diff