Skip to content
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

8078641: MethodHandle.asTypeCache can retain classes from unloading #5246

Closed
wants to merge 6 commits into from

Conversation

@iwanowww
Copy link

@iwanowww iwanowww commented Aug 25, 2021

MethodHandle.asTypeCache keeps a strong reference to adapted MethodHandle and it can introduce a class loader leak through its MethodType.

Proposed fix introduces a 2-level cache (1 element each) where 1st level can only contain MethodHandles which are guaranteed to not introduce any dependencies on new class loaders compared to the original MethodHandle. 2nd level is backed by a SoftReference and is used as a backup when the result of MethodHandle.asType() conversion can't populate the higher level cache.

The fix is based on the work made by Peter Levart @plevart back in 2015.

Testing: tier1 - tier6


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed

Issue

  • JDK-8078641: MethodHandle.asTypeCache can retain classes from unloading

Reviewers

Contributors

  • Peter Levart <plevart@openjdk.org>
  • Vladimir Ivanov <vlivanov@openjdk.org>

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.java.net/jdk pull/5246/head:pull/5246
$ git checkout pull/5246

Update a local copy of the PR:
$ git checkout pull/5246
$ git pull https://git.openjdk.java.net/jdk pull/5246/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 5246

View PR using the GUI difftool:
$ git pr show -t 5246

Using diff file

Download this PR as a diff file:
https://git.openjdk.java.net/jdk/pull/5246.diff

@bridgekeeper
Copy link

@bridgekeeper bridgekeeper bot commented Aug 25, 2021

👋 Welcome back vlivanov! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

Loading

@openjdk
Copy link

@openjdk openjdk bot commented Aug 25, 2021

@iwanowww The following label will be automatically applied to this pull request:

  • core-libs

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

Loading

@openjdk openjdk bot added the core-libs label Aug 25, 2021
@iwanowww
Copy link
Author

@iwanowww iwanowww commented Aug 25, 2021

/contributor add plevart
/contributor add vlivanov

Loading

@openjdk
Copy link

@openjdk openjdk bot commented Aug 25, 2021

@iwanowww
Contributor Peter Levart <plevart@openjdk.org> successfully added.

Loading

@openjdk
Copy link

@openjdk openjdk bot commented Aug 25, 2021

@iwanowww
Contributor Vladimir Ivanov <vlivanov@openjdk.org> successfully added.

Loading

@iwanowww iwanowww marked this pull request as ready for review Aug 25, 2021
@openjdk openjdk bot added the rfr label Aug 25, 2021
@mlbridge
Copy link

@mlbridge mlbridge bot commented Aug 25, 2021

Loading

Copy link
Member

@PaulSandoz PaulSandoz left a comment

Looks good.

I guess it is not that common for the soft ref to get instantiated i.e. for the case of the ~common class loader of type, MTC say, and the ~common classoader of newType, NMTC say, then NMTC is not an ancestor of MTC.

It's possible that asTypeCache and asTypeSoftCache could both be non-null i.e. we don't null out one of them, buy does not seem a problem.

Loading

Loading
Loading
@openjdk
Copy link

@openjdk openjdk bot commented Aug 26, 2021

@iwanowww 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:

8078641: MethodHandle.asTypeCache can retain classes from unloading

Co-authored-by: Peter Levart <plevart@openjdk.org>
Co-authored-by: Vladimir Ivanov <vlivanov@openjdk.org>
Reviewed-by: psandoz, mchung, plevart

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 112 new commits pushed to the master branch:

  • ff4018b: 8268148: unchecked warnings handle ? and ? extends Object differently
  • 8c37909: 8273234: extended 'for' with expression of type tvar causes the compiler to crash
  • 28ba78e: 8244675: assert(IncrementalInline || (_late_inlines.length() == 0 && !has_mh_late_inlines()))
  • d05494f: 8266239: Some duplicated javac command-line options have repeated effect
  • 93eec9a: 8272776: NullPointerException not reported
  • 7b023a3: 8273257: jshell doesn't compile a sealed hierarchy with a sealed interface and a non-sealed leaf
  • f17ee0c: 8273263: Incorrect recovery attribution of record component type when j.l.Record is unavailable
  • fa9c865: 8273112: -Xloggc: should override -verbose:gc
  • dd87181: 8214761: Bug in parallel Kahan summation implementation
  • 7fff22a: 8272805: Avoid looking up standard charsets
  • ... and 102 more: https://git.openjdk.java.net/jdk/compare/7f80683cfeee3c069f48d5bce45fa92b2381b518...master

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 master branch, type /integrate in a new comment.

Loading

@openjdk openjdk bot added the ready label Aug 26, 2021
Copy link
Member

@mlchung mlchung left a comment

Thanks for fixing this. JEP 416 depends on this.

Loading

Loading
Loading
@iwanowww
Copy link
Author

@iwanowww iwanowww commented Sep 1, 2021

Thanks for the reviews, Paul and Mandy.

What do you think about the latest version?

Loading


/* Returns true when {@code loader} keeps {@code cls} either directly or indirectly through the loader delegation chain. */
private static boolean keepsAlive(Class<?> cls, ClassLoader loader) {
return keepsAlive(cls.getClassLoader(), loader);
Copy link
Member

@mlchung mlchung Sep 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return keepsAlive(cls.getClassLoader(), loader);
ClassLoader defLoader = cls.getClassLoader();
if (isBuiltinLoader(defLoader)) {
return true; // built-in loaders are always reachable
}
return keepsAlive(defLoader, loader);

I think it's clearer to check if cls is not defined by any builtin loader here and then check if loader keeps cls alive.

So keepsAlive(ClassLoader loader1, ClassLoader loader2) is not needed and replace line 935 and 940 to keepsAlive(Class, ClassLoader) instead.

Loading

Copy link
Author

@iwanowww iwanowww Sep 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Incorporated your suggestions along with some minor refactorings.

Loading

@mlchung
Copy link
Member

@mlchung mlchung commented Sep 3, 2021

This looks good to me.

For the change of MethodHandle::asType to a final method, this needs a CSR. Is this spec change intentional? I wonder if MethodHandle should be a sealed class instead. In any case, maybe you can consider the spec change as a separate issue.

Loading

@iwanowww
Copy link
Author

@iwanowww iwanowww commented Sep 3, 2021

For the change of MethodHandle::asType to a final method, this needs a CSR.

It is not allowed to extend/subclass MethodHandle outside java.lang.invoke package.
So, the aforementioned change doesn't have any compatibility risks. Do I miss something important?

    /**
     * Package-private constructor for the method handle implementation hierarchy.
     * Method handle inheritance will be contained completely within
     * the {@code java.lang.invoke} package.
     */
    // @param type type (permanently assigned) of the new method handle
    /*non-public*/
    MethodHandle(MethodType type, LambdaForm form) {

Loading

}
if (asTypeSoftCache != null) {
atc = asTypeSoftCache.get();
if (newType == atc.type) {
Copy link
Contributor

@plevart plevart Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NPE is possible here! act can be null as it is a result of SoftReference::get

Loading

Copy link
Author

@iwanowww iwanowww Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Fixed.

Loading

return atc; // cache hit
}
if (asTypeSoftCache != null) {
atc = asTypeSoftCache.get();
Copy link
Contributor

@plevart plevart Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NPE is possible here too! asTypeSoftCache is a non-volatile field which is read twice. First time in the if (...) condition, 2nd time in the line that de-references it to call .get(). This is a data-race since concurrent thread may be setting this field from null to non-null. Those two reads may get reordered. 1st read may return non-null while 2nd may return null. This can be avoided if the field is read just once by introducing a local variable to store its value.

Loading

Copy link
Author

@iwanowww iwanowww Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Loading

return loader;
}

/* Returns true when {@code loader} keeps {@code mt} either directly or indirectly through the loader delegation chain. */
Copy link
Contributor

@plevart plevart Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, to be precise, loader can't keep mt alive. It would be better to say "keeps mt components alive" ...

Loading

Copy link
Author

@iwanowww iwanowww Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Loading

ClassLoader defLoader = cls.getClassLoader();
if (isBuiltinLoader(defLoader)) {
return true; // built-in loaders are always reachable
}
Copy link
Contributor

@plevart plevart Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for special case here. isAncestorLoaderOf(defLoader, loader) already handles this case.

Loading

Copy link
Author

@iwanowww iwanowww Sep 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though the check is redundant, I find the current version clearer.

Loading

@plevart
Copy link
Contributor

@plevart plevart commented Sep 3, 2021

This looks good to me now.

Loading

plevart
plevart approved these changes Sep 3, 2021
@stuart-marks
Copy link
Member

@stuart-marks stuart-marks commented Sep 3, 2021

Did we want this to use a soft reference or a weak reference? The original bug report mentions weak references, and the typical approach for preventing caches from holding onto things unnecessarily is to use weak references. I haven't thought through the implications of using soft instead of weak references. (Sorry if I missed any discussion on this issue.)

Loading

@iwanowww
Copy link
Author

@iwanowww iwanowww commented Sep 3, 2021

Other MethodHandle/LambdaForm caches in java.lang.invoke deliberately rely on SoftReferences. ​(Only MethodType table uses WeakReference, but it is there for interning purposes.) MH adaptations are quite expensive (and may involve class loading), so it's beneficial to keep the result around for an extended period of time when circumstances allow.

On the other hand, MH.asType() case is special because it can hold user classes around which makes effective memory footprint of cached value much higher. So, WeakReference would enable more prompt recycling of heap memory at the expense of higher cache miss rate.

Personally, I'm in favor SoftReference here since it allows to get most of performance benefits out of the cache while preserving the correctness w.r.t. heap exhaustion.

Loading

@stuart-marks
Copy link
Member

@stuart-marks stuart-marks commented Sep 8, 2021

I don't have a strong opinion on whether this should use a SoftReference or a WeakReference. (In fact, one might say that I have a phantom opinion.) The main thing was that the bug report mentioned WeakReference but the commit here uses SoftReferences, and I didn't see any discussion about this change. You gave some reasons above for why SoftReference is preferable, and that's ok with me.

Loading

@iwanowww
Copy link
Author

@iwanowww iwanowww commented Sep 8, 2021

Thanks for the reviews, Mandy, Paul, Peter, and Stuart.

/integrate

Loading

@openjdk
Copy link

@openjdk openjdk bot commented Sep 8, 2021

Going to push as commit 21012f2.
Since your change was applied there have been 150 commits pushed to the master branch:

  • 1855574: 8273038: ChannelInputStream.transferTo() uses FileChannel.transferTo(FileChannel)
  • 6750c34: 8270533: AArch64: size_fits_all_mem_uses should return false if its output is a CAS
  • a66629a: 8254167: G1: Record regions where evacuation failed to provide targeted iteration
  • 286a1f6: 8273440: Zero: Disable runtime/Unsafe/InternalErrorTest.java
  • 7d24a33: 8273318: Some containers/docker/TestJFREvents.java configs are running out of memory
  • 1513dc7: 8271603: Unnecessary Vector usage in java.desktop
  • ea4907a: 8273047: test jfr/api/consumer/TestRecordedFrame.java timing out
  • 4eacdb3: 8273104: Refactoring option parser for UL
  • 8884d2f: 8273462: ProblemList vmTestbase/vm/mlvm/anonloader/stress/oome/heap/Test.java in -Xcomp mode
  • d6d6c06: 8273246: Amend the test java/nio/channels/DatagramChannel/ManySourcesAndTargets.java to execute in othervm mode
  • ... and 140 more: https://git.openjdk.java.net/jdk/compare/7f80683cfeee3c069f48d5bce45fa92b2381b518...master

Your commit was automatically rebased without conflicts.

Loading

@openjdk openjdk bot closed this Sep 8, 2021
@openjdk openjdk bot added integrated and removed ready rfr labels Sep 8, 2021
@openjdk
Copy link

@openjdk openjdk bot commented Sep 8, 2021

@iwanowww Pushed as commit 21012f2.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

Loading

@mlchung
Copy link
Member

@mlchung mlchung commented Sep 9, 2021

For the change of MethodHandle::asType to a final method, this needs a CSR.

It is not allowed to extend/subclass MethodHandle outside java.lang.invoke package.
So, the aforementioned change doesn't have any compatibility risks. Do I miss something important?

    /**
     * Package-private constructor for the method handle implementation hierarchy.
     * Method handle inheritance will be contained completely within
     * the {@code java.lang.invoke} package.
     */
    // @param type type (permanently assigned) of the new method handle
    /*non-public*/
    MethodHandle(MethodType type, LambdaForm form) {

Sorry for the late reply as I was on vacation and just return today.

Thanks for the clarification. I missed the fact that MethodHandle has no public constructor and cannot be extended. This is fine.

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
5 participants