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

6983726: Reimplement MethodHandleProxies.asInterfaceInstance #13197

Closed
wants to merge 54 commits into from

Conversation

liach
Copy link
Member

@liach liach commented Mar 27, 2023

As John Rose has pointed out in this issue, the current j.l.r.Proxy based implementation of MethodHandleProxies.asInterface has a few issues:

  1. Exposes too much information via Proxy supertype (and WrapperInstance interface)
  2. Does not allow future expansion to support SAM1 abstract classes
  3. Slow (in fact, very slow)

This patch addresses all 3 problems:

  1. It updates the WrapperInstance methods to take an Empty to avoid method clashes
  2. This patch obtains already generated classes from a ClassValue by the requested interface type; the ClassValue can later be updated to compute implementation generation for abstract classes as well.
  3. This patch's faster than old implementation in general.

Benchmark for revision 17:

Benchmark                                                          Mode  Cnt      Score       Error  Units
MethodHandleProxiesAsIFInstance.baselineAllocCompute               avgt   15      1.503 ±     0.021  ns/op
MethodHandleProxiesAsIFInstance.baselineCompute                    avgt   15      0.269 ±     0.005  ns/op
MethodHandleProxiesAsIFInstance.testCall                           avgt   15      1.806 ±     0.018  ns/op
MethodHandleProxiesAsIFInstance.testCreate                         avgt   15     17.332 ±     0.210  ns/op
MethodHandleProxiesAsIFInstance.testCreateCall                     avgt   15     19.296 ±     1.371  ns/op
MethodHandleProxiesAsIFInstanceCall.callDoable                     avgt    5      0.419 ±     0.004  ns/op
MethodHandleProxiesAsIFInstanceCall.callHandle                     avgt    5      0.421 ±     0.004  ns/op
MethodHandleProxiesAsIFInstanceCall.callInterfaceInstance          avgt    5      1.731 ±     0.018  ns/op
MethodHandleProxiesAsIFInstanceCall.callLambda                     avgt    5      0.418 ±     0.003  ns/op
MethodHandleProxiesAsIFInstanceCall.constantDoable                 avgt    5      0.263 ±     0.003  ns/op
MethodHandleProxiesAsIFInstanceCall.constantHandle                 avgt    5      0.262 ±     0.002  ns/op
MethodHandleProxiesAsIFInstanceCall.constantInterfaceInstance      avgt    5      0.262 ±     0.002  ns/op
MethodHandleProxiesAsIFInstanceCall.constantLambda                 avgt    5      0.267 ±     0.019  ns/op
MethodHandleProxiesAsIFInstanceCall.direct                         avgt    5      0.266 ±     0.013  ns/op
MethodHandleProxiesAsIFInstanceCreate.createCallInterfaceInstance  avgt    5     18.057 ±     0.182  ns/op
MethodHandleProxiesAsIFInstanceCreate.createCallLambda             avgt    5  19646.500 ± 18797.196  ns/op
MethodHandleProxiesAsIFInstanceCreate.createInterfaceInstance      avgt    5     16.956 ±     0.064  ns/op
MethodHandleProxiesAsIFInstanceCreate.createLambda                 avgt    5  19379.897 ± 18064.751  ns/op
MethodHandleProxiesSuppl.testInstanceTarget                        avgt   15      8.606 ±     0.010  ns/op
MethodHandleProxiesSuppl.testInstanceType                          avgt   15      3.168 ±     0.006  ns/op
MethodHandleProxiesSuppl.testIsWrapperInstance                     avgt   15      1.848 ±     0.002  ns/op

Additionally, an obsolete ProxyForMethodHandle test was removed, for it's no longer applicable.


State of this patch:

  • Now using a shared-class approach, since often times proxies are created for multiple handles where each is rarely called, like property getters.
  • Whether the classes generated should have stable names
  • Dedicated-class approach is valuable, but we might implement that in another API and seek to be leyden friendly
  • The held MH may be wrapped for SecurityManager checks but the original non-secured MH can be accessed via wrapperInstanceTarget(); does calling the held MH directly impose any security risk?

Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change requires CSR request JDK-8305691 to be approved

Issues

  • JDK-6983726: Reimplement MethodHandleProxies.asInterfaceInstance (Enhancement - P4)
  • JDK-8305691: Reimplement MethodHandleProxies.asInterfaceInstance (CSR)

Reviewers

Contributors

  • Mandy Chung <mchung@openjdk.org>

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/13197/head:pull/13197
$ git checkout pull/13197

Update a local copy of the PR:
$ git checkout pull/13197
$ git pull https://git.openjdk.org/jdk.git pull/13197/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 13197

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

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/13197.diff

Webrev

Link to Webrev Comment

Footnotes

  1. single abstract method

@bridgekeeper
Copy link

bridgekeeper bot commented Mar 27, 2023

👋 Welcome back liach! 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.

@openjdk openjdk bot added the rfr Pull request is ready for review label Mar 27, 2023
@openjdk
Copy link

openjdk bot commented Mar 27, 2023

@liach 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.

@openjdk openjdk bot added the core-libs core-libs-dev@openjdk.org label Mar 27, 2023
@forax
Copy link
Member

forax commented Mar 28, 2023

I believe you can have better performance if you pass the method handle as the class data of the hidden class and you load it with a constant dynamic
https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#defineHiddenClassWithClassData(byte[],java.lang.Object,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...)
and
https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/invoke/MethodHandles.html#classData(java.lang.invoke.MethodHandles.Lookup,java.lang.String,java.lang.Class)

EDIT: you may have to use invokedynamic instead of a ldc constant dynamic + invokeExact() because the interface can have several methods to implement due to bridge methods (inheritance + generics)

@liach
Copy link
Member Author

liach commented Mar 28, 2023

I believe you can have better performance if you pass the method handle as the class data of the hidden class and you load it with a constant dynamic https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#defineHiddenClassWithClassData(byte[],java.lang.Object,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...) and https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/invoke/MethodHandles.html#classData(java.lang.invoke.MethodHandles.Lookup,java.lang.String,java.lang.Class)

With one class per method handle + classdata, you do have better performance for invocations, but the penalties on creation is insurmountable: JMH results comparison between OracleJDK 20 and my initial patch, implemented based on this idea

EDIT: you may have to use invokedynamic instead of a ldc constant dynamic + invokeExact() because the interface can have several methods to implement due to bridge methods (inheritance + generics)

Since this API needs to call asType on the input method handle to perform input validation, I just decided to pass the validated method handles wholesale to the generated class, either as condy (initial patch) or constructor arguments.

@forax
Copy link
Member

forax commented Mar 28, 2023

Let suppose you have an interface like:

interface StringConsumer extends Consumer<String> {}

The implementation needs to override both accept(Object) and accept(String).

@JornVernee
Copy link
Member

JornVernee commented Mar 28, 2023

I believe you can have better performance if you pass the method handle as the class data of the hidden class and you load it with a constant dynamic https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#defineHiddenClassWithClassData(byte[],java.lang.Object,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...) and https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/invoke/MethodHandles.html#classData(java.lang.invoke.MethodHandles.Lookup,java.lang.String,java.lang.Class)

With one class per method handle + classdata, you do have better performance for invocations, but the penalties on creation is insurmountable: JMH results comparison between OracleJDK 20 and my initial patch, implemented based on this idea

But this doesn't implement the same ClassValue cache? And spinning byte code can indeed be slow (I've seen this in other profiles).

I think it should be possible to spin the class bytes once, stick the result in a ClassValue cache, but then use the bytes to define multiple classes with different class data (MethodHandles).

Also, it would be nice if you could include the benchmarks you used in the patch as well.

@JornVernee
Copy link
Member

Also, some history.

  • I've worked on something similar before: https://bugs.openjdk.org/browse/JDK-8257605 The idea then was to add a new API in MethodHandles but since then I've been thinking that enhancing the performance of MethodHandleProxies might be better. The only issue I saw with that is that the current API doesn't accept a MethodHandles.Lookup object which would be needed to define an interface instance if the interface is in a non-exported package. Your solution just creates the lookup directly. Typically we have to be careful with injecting classes into other packages/modules since it can lead to access escalation, but maybe in this case it is fine. I don't think there's a way that this could be used to escalate access, though it seems strange that anyone could now define a class in a non-exported package/some other module.

  • We've also recently discussed LambdaMetafactory 8302154: Hidden classes created by LambdaMetaFactory can't be unloaded #12493 which will produce classes strongly tied to the class loader. If we can bring MethodHandleProxies up to the same performance level as the classes produced by LambdaMetaFactory, it could serve as an alternative and people could move away from using the LambdaMetafactory runtime API (which is not really meant to be used directly). WRT that, I think the fact that the implementation proposed by this patch defines classes that are not strongly tied to the defining loader is a good thing.

@liach
Copy link
Member Author

liach commented Mar 28, 2023

I think it should be possible to spin the class bytes once, stick the result in a ClassValue cache, but then use the bytes to define multiple classes with different class data (MethodHandles).

Great point, I was dumbfounded there.

Let suppose you have an interface like:

interface StringConsumer extends Consumer<String> {}

The implementation needs to override both accept(Object) and accept(String).

It already does, and there are multiple test cases covering that: mine and another in MethodHandlesGeneralTest

Also, it would be nice if you could include the benchmarks you used in the patch as well.

They are from the JDK itself: https://github.com/openjdk/jdk/blob/master/test/micro/org/openjdk/bench/java/lang/invoke/MethodHandleProxiesSuppl.java https://github.com/openjdk/jdk/blob/master/test/micro/org/openjdk/bench/java/lang/invoke/MethodHandleProxiesAsIFInstance.java

Due to make issues (it passed / for module exports as \ on my windows somehow), I ran the test by copying them to a separate Gradle project with Gradle JMH plugin.

I've worked on something similar before: https://bugs.openjdk.org/browse/JDK-8257605 The idea then was to add a new API in MethodHandles but since then I've been thinking that enhancing the performance of MethodHandleProxies might be better. The only issue I saw with that is that the current API doesn't accept a MethodHandles.Lookup object which would be needed to define an interface instance if the interface is in a non-exported package. Your solution just creates the lookup directly. Typically we have to be careful with injecting classes into other packages/modules since it can lead to access escalation, but maybe in this case it is fine. I don't think there's a way that this could be used to escalate access, though it seems strange that anyone could now define a class in a non-exported package/some other module.

About access:

  1. The method handle itself is already access-checked upon creation.
  2. The interface access may be problematic: A non-exported interface Class object can be obtained via Reflection inspection on exported types, such as java packages and jdk.internal packages.
    • In that case, it might not be of best interest to create an interface, but I don't think the current asInterfaceInstance API rejects such creations either.
  3. The class definition under the interface and its classloader IMO is safe, as the class will not refer to any type the interface itself does not refer to; the annotation is not involved in class loading either.

We've also recently discussed LambdaMetafactory #12493 which will produce classes strongly tied to the class loader. If we can bring MethodHandleProxies up to the same performance level as the classes produced by LambdaMetaFactory, it could serve as an alternative and people could move away from using the LambdaMetafactory runtime API (which is not really meant to be used directly). WRT that, I think the fact that the implementation proposed by this patch defines classes that are not strongly tied to the defining loader is a good thing.

Sounds good; it's not hard to make a benchmark that compares asInterfaceInstance and metafactory side-by-side either. The non-strong feature was intended indeed when one class is defined for each class + methodhandle combination. I agree asInterfaceInstance would be extremely useful to users, like a plugin loader converting json-based static method references into SAM implementations, which definitely should not become defined as a part of the plugin loader; or an event bus firing events.

@liach
Copy link
Member Author

liach commented Mar 28, 2023

But this doesn't implement the same ClassValue cache? And spinning byte code can indeed be slow (I've seen this in other profiles).

liachmodded@821d6b3

I attempted to spin one class for each mh+interface combination again. The creation time is halved compared to the no-template version, but the creation time is still very long (factor of 1000) compared to the passing to instance approach.

I think we need to determine our approach based on the call patterns of this API:

  1. how often asInterfaceInstance is called with the same interface
  2. how often asInterfaceInstance is called vs. how often the implementations are called

In addition, we might write a benchmark for fresh creation performance as well (as Proxy does class caching, and LambdaMetafactory also caches class in CallSite). I don't think that asInterfaceInstance class spinning is much slower than metafactory.

@liach
Copy link
Member Author

liach commented Mar 28, 2023

On a side note, one implementation class for one interface and passing handles via final fields approach (currently in this PR) is the closest to the current Proxy approach, that it almost always have better performance than the existing Proxy implementations. But this approach needs to be subject to the fresh creation performance checks as well.

@liach
Copy link
Member Author

liach commented Mar 29, 2023

I think the current MethodHandleProxies implementation is indeed on par with LambdaMetafactory:
https://jmh.morethan.io/?gist=fcb946d83ee4ac7386901795ca49b224

The creation performance is slightly slower than that of Lmf (maybe because of heads-up asType conversions, which is required by the API specification), but the execution performance is already on par with it.

Copy link
Member

@JornVernee JornVernee left a comment

Choose a reason for hiding this comment

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

I think the approach taken by this patch is good.

I agree with your analysis about access checking. The access checks are preformed when creating the target method handle, and if the target is a @CallerSensitive method, the caller will be the lookup class. So, it seems like it's not possible to e.g. turn a MethodHandle pointing at MethodHandles::lookup into an interface instance to get access to a Lookup created in the target interface's module.

I also think the approach of creating a new class per method handle is good. This is currently the only way to guarantee peak performance as far as I'm aware. If only a single class is desired, this can be achieved by using a lambda expression that captures the target method handle. Though, that also requires knowing the target type statically.

I think this change needs a CSR as well to document the change in behavior. If an application currently calls asInterfaceInstance many times for the same interface, the amount of classes generated will increase (potentially a lot).

Comment on lines 212 to 213
proxy = lookup.findConstructor(lookup.lookupClass(), methodType(void.class))
.asType(methodType(Object.class)).invokeExact();
Copy link
Member

Choose a reason for hiding this comment

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

This can use invoke instead of an explicit asType and invokeExact. It is more or less the same.

Suggested change
proxy = lookup.findConstructor(lookup.lookupClass(), methodType(void.class))
.asType(methodType(Object.class)).invokeExact();
proxy = lookup.findConstructor(lookup.lookupClass(), methodType(void.class)).invoke();

Copy link
Member Author

Choose a reason for hiding this comment

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

Fyi the original code was an emulation of how LambdaMetafactory initializes no-arg lambda instances.

Copy link
Member

Choose a reason for hiding this comment

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

@JornVernee Is there any performance difference with invokeExact vs invoke? I have the impression there is.

Copy link
Member

@JornVernee JornVernee Apr 6, 2023

Choose a reason for hiding this comment

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

It only matters when we get to C2 compilation for a constant method handle instance being called, which is not the case here. invoke is just a nice shorthand for doing an inexact call.

I had another look at the implementation, and it seems like C2 would not be able to inline through invoke calls, unless the type of the call site matches the type of the MethodHandle exactly. Using invokeExact would make sure that is the case. But, again, since we're just doing a one-off invocation of a non-constant method handle here, it doesn't matter in this case, so let's just use invoke for its convenience.

proxyLoader = cl != null ? cl : ClassLoader.getSystemClassLoader();

// Interface-specific setup
var info = INTERFACE_INFOS.get(intfc); // throws IllegalArgumentException
Copy link
Member

Choose a reason for hiding this comment

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

I find the use of var confusing here, since the type doesn't appear locally and is not well known either.

Suggested change
var info = INTERFACE_INFOS.get(intfc); // throws IllegalArgumentException
InterfaceInfo info = INTERFACE_INFOS.get(intfc); // throws IllegalArgumentException


private record LocalMethodInfo(MethodTypeDesc desc, List<ClassDesc> thrown) {}

private record InterfaceInfo(@Stable MethodType[] types, Lookup lookup, @Stable byte[] template) {}
Copy link
Member

Choose a reason for hiding this comment

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

Use of @Stable here is not needed. We don't constant fold through InterfaceInfo instances.

Suggested change
private record InterfaceInfo(@Stable MethodType[] types, Lookup lookup, @Stable byte[] template) {}
private record InterfaceInfo(MethodType[] types, Lookup lookup, byte[] template) {}

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I thought records are constant-folded.

if (holder->is_record())
Per #9143 (comment), array cloning sees 10% less time per operation with the annotation.

I will try with and without these annotations and report the creation benchmark results to see if they are worth it.

Copy link
Member

@JornVernee JornVernee Apr 5, 2023

Choose a reason for hiding this comment

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

The InterfaceInfo instance would have to be constant as well in order for loads of the fields & array elements to be constant folded (the same applies to records in general). However, the instances come from a call from ClassValue::get so they are not constant. (See also: https://bugs.openjdk.org/browse/JDK-8238260)

ih);
}

var template = createTemplate(desc(intfc), methods.get(0).getName(), infos);
Copy link
Member

Choose a reason for hiding this comment

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

I think using an explicit type would be preferable here as well

Suggested change
var template = createTemplate(desc(intfc), methods.get(0).getName(), infos);
byte[] template = createTemplate(desc(intfc), methods.get(0).getName(), infos);


// Spin an implementation class for an interface. A new class should be defined for each handle.
// constructor parameter: Array[target, mh1, mh2, ...]
private static byte[] createTemplate(ClassDesc ifaceDesc, String name, List<LocalMethodInfo> methods) {
Copy link
Member

Choose a reason for hiding this comment

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

'name' is quite ambiguous here, I suggest name -> methodName

Comment on lines 315 to 321
bcb.constantInstruction(condy);
int slot = 1;
for (var t : mi.desc.parameterList()) {
var kind = TypeKind.from(t);
bcb.loadInstruction(kind, slot);
slot += kind.slotSize();
}
Copy link
Member

Choose a reason for hiding this comment

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

You could also use CodeBuilder::parameterSlot instead of computing the slot manually.

.return_());

// actual implementations
int i = 1;
Copy link
Member

Choose a reason for hiding this comment

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

Can you give i a more descriptive name here? Also, can you add a comment that says why it starts at 1 (e.g. // +1 skip original target mh).

Comment on lines 104 to 123
// We both check for correctness and that it doesn't throw
@Test
public void testObjectMethods() throws Throwable {
var mh = MethodHandles.publicLookup()
.findVirtual(Integer.class, "compareTo", methodType(int.class, Integer.class));
@SuppressWarnings("unchecked")
Comparator<Integer> p1 = (Comparator<Integer>) asInterfaceInstance(Comparator.class, mh);
@SuppressWarnings("unchecked")
Comparator<Integer> p2 = (Comparator<Integer>) asInterfaceInstance(Comparator.class, mh);

assertEquals(System.identityHashCode(p1), p1.hashCode());
assertEquals(System.identityHashCode(p2), p2.hashCode());

assertEquals(p1, p1);
assertEquals(p1 == p2, p1.equals(p2));
assertEquals(p2 == p1, p2.equals(p1));

assertEquals(Objects.toIdentityString(p1), p1.toString());
assertEquals(Objects.toIdentityString(p2), p2.toString());
}
Copy link
Member

Choose a reason for hiding this comment

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

tbh I don't really see the point of this test since we don't override hashCode/equals/toString?

}

@Benchmark
public Doable lambdaCreate() throws Throwable {
Copy link
Member

Choose a reason for hiding this comment

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

I know this is pre-existing, but I don't think having both call and create benchmarks in the same class is that useful. Since the time that is taken by each is very different.

I suggest splitting these benchmarks into 2 files instead: one for the call benchmarks, and the other for create, and create + call benchmarks (and then change the output time units for the latter).

I think the call benchmark could be fleshed out a bit more as well. It would be interesting to see these cases:

  1. direct call to doWork (this would be the baseline)
  2. call through non-constant method handle
  3. call through non-constant interface instance created with lambda (existing lambdaCall)
  4. call through non-constant interface instance created with MHP::asInterfaceInstance (existing testCall)
  5. call through constant (static final) method handle
  6. call through constant (static final) interface instance created with lambda
  7. call through constant (static final) interface instance created with MHP::asInterfaceInstance

@liach
Copy link
Member Author

liach commented Jul 11, 2023

Thank you so much @JornVernee: The WeakReference should point to the impl class. The Lookup is a cheap wrapper, so I changed it to be created each time instead. The test comes back green on my device with your suggestions.

I think this patch is stable now; feel free to review again, especially that I've rearranged the tests.

@mlchung
Copy link
Member

mlchung commented Jul 11, 2023

I missed to comment on getProxyClassLookup - we agree to change this to synchronize on the holder object to avoid spinning multiple hidden classes when multiple threads are in the race of creating the wrapper class for the same interface. Can you make the change?

@mlchung
Copy link
Member

mlchung commented Jul 11, 2023

The WeakReference should point to the impl class. The Lookup is a cheap wrapper, so I changed it to be created each time instead.

I won't object to keep the impl class. Just to be clear, the test should pass even if it keeps Lookup as the referent, right?

*/
@Test
public void testNoAccess() {
Client untrusted = asInterfaceInstance(Client.class, MethodHandles.zero(void.class));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Client untrusted = asInterfaceInstance(Client.class, MethodHandles.zero(void.class));
Client obj = asInterfaceInstance(Client.class, MethodHandles.zero(void.class));

public void testNoAccess() {
Client untrusted = asInterfaceInstance(Client.class, MethodHandles.zero(void.class));
var instanceClass = untrusted.getClass();
var leakLookup = Client.leakLookup();
Copy link
Member

Choose a reason for hiding this comment

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

This is not really malicious code. It's checking the interface has no access to the proxy class. Probably better to rename them to be less alarming.

Suggested change
var leakLookup = Client.leakLookup();
var lookup = Client.lookup();

var instanceClass = untrusted.getClass();
var leakLookup = Client.leakLookup();
assertEquals(MethodHandles.Lookup.ORIGINAL, leakLookup.lookupModes() & MethodHandles.Lookup.ORIGINAL,
"Leaked lookup original flag");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"Leaked lookup original flag");
"expect lookup has original flag");

WeakReference<Class<?>> cl;

var c1 = asInterfaceInstance(ifaceClass, mh);
cl = new WeakReference<>(c1.getClass());
Copy link
Member

Choose a reason for hiding this comment

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

Nit: define a new variable not to mix with c1.

Suggested change
cl = new WeakReference<>(c1.getClass());
var wr = new WeakReference<>(c1.getClass());

Comment on lines 196 to 198
System.gc();
var c2 = asInterfaceInstance(ifaceClass, mh);
assertTrue(cl.refersTo(c2.getClass()), "MHP should reuse implementation class when available");
Copy link
Member

Choose a reason for hiding this comment

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

We can simplify the test a little bit. Just to check if the class of c1 and c2 is the same first.

Suggested change
System.gc();
var c2 = asInterfaceInstance(ifaceClass, mh);
assertTrue(cl.refersTo(c2.getClass()), "MHP should reuse implementation class when available");
var c2 = asInterfaceInstance(ifaceClass, mh);
assertTrue(c1.getClass() == c2.getClass(), "MHP should reuse implementation class when available");

Copy link
Member Author

Choose a reason for hiding this comment

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

I will keep the gc, which ensures a scenario like that incorrectly weakref'd Lookup doesn't happpen again.

* @build ProxiesImplementationTest Client
* @run junit ProxiesImplementationTest
*/
public class ProxiesImplementationTest {
Copy link
Member

Choose a reason for hiding this comment

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

how about WrapperHiddenClassTest? It's explicit that this verifies the wrapper hidden class.

assertTrue(cl.refersTo(c2.getClass()), "MHP should reuse implementation class when available");
Reference.reachabilityFence(c1);

// allow GC in interpreter
Copy link
Member

Choose a reason for hiding this comment

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

This not only affects GC in interpreter. maybe something like this:

Suggested change
// allow GC in interpreter
// clear strong reference to the wrapper instances

import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.lang.invoke.LambdaMetafactory;
Copy link
Member

Choose a reason for hiding this comment

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

this import is unused.


/**
* Benchmark evaluates the call performance of MethodHandleProxies.asInterfaceInstance
* return value, compared to
Copy link
Member

Choose a reason for hiding this comment

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

incomplete comment?

Copy link
Member Author

Choose a reason for hiding this comment

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

On a side note, these benchmarks were added for the class-per-MH implementation; I probably need to reevaluate if these 2 benchmarks are needed, or if the original MethodHandleProxiesAsIFInstance.java suffices.

Comment on lines 320 to 325
if (cl == null) {
// If the referent is cleared, create a new value and update cached weak reference.
cl = newProxy(intfc);
r.set(cl);
}
return new Lookup(cl);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (cl == null) {
// If the referent is cleared, create a new value and update cached weak reference.
cl = newProxy(intfc);
r.set(cl);
}
return new Lookup(cl);
if (cl != null)
return new Lookup(cl);
synchronized (r) {
cl = r.get();
if (cl == null) {
// If the referent is cleared, create a new value and update cached weak reference.
cl = newProxy(intfc);
r.set(cl);
}
return new Lookup(cl);
}

@liach
Copy link
Member Author

liach commented Jul 11, 2023

Just to be clear, the test should pass even if it keeps Lookup as the referent, right?

No. The test will not because there's no reference to the Lookup except in asInterfaceInstance itself; the Lookups thus are always eligible for GC, even if there's living wrapper instance, as shown in the first broken assertTrue(cl.referTo(c2.getClass())) test. The Class object for implementation, however, is always reachable as long as any wrapper instance is reachable.

The alternative to keeping Classes in weak reference is to keep a lookup in the hidden class and cache that particular lookup, which IMO only complicates both the support code (have to distinguish from Lookup returned by defineHiddenClass and edit spinned bytecode).

@mlchung
Copy link
Member

mlchung commented Jul 11, 2023

I see now. Ok. Then getProxyClassLookup can simply return Class<?> and let the caller to create the Lookup object then (should rename to getProxyClass).

Copy link
Member

@mlchung mlchung left a comment

Choose a reason for hiding this comment

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

Looks good. As the hidden interface check is added, the spec needs update to say "The interface must be public and not sealed and not hidden." as the CSR says. (sorry I might have confused you).

The CSR will need update as well as it no longer uses class data.

assertTrue(implModule.getName().startsWith("jdk.MHProxy"),
() -> "incorrect dynamic module name: " + implModule.getName());

assertSame(implClass.getClassLoader(), implModule.getClassLoader(),
Copy link
Member

Choose a reason for hiding this comment

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

I think this should check against ifaceModule.getClassLoader() instead, right? Since the dynamic module is defined in the interface' class loader.

Copy link
Member

Choose a reason for hiding this comment

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

Good catch! Yes, this should check against the interface's loader.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added a new check for ifaceClass.getClassLoader(), should be functionally equivalent. This check is originally part of ProxyForMethodHandle test, so I decided to keep it as well as it tests impl module's behavior.

@openjdk
Copy link

openjdk bot commented Jul 18, 2023

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

6983726: Reimplement MethodHandleProxies.asInterfaceInstance

Co-authored-by: Mandy Chung <mchung@openjdk.org>
Reviewed-by: jvernee, mchung

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

  • 71cac8c: 8311663: Additional refactoring of Locale tests to JUnit
  • aa23fd9: 8311879: SA ClassWriter generates invalid invokedynamic code
  • 6f66213: 8312014: [s390x] TestSigInfoInHsErrFile.java Failure
  • b5b6f4e: 8312164: Refactor Arrays.hashCode for long, boolean, double, float, and Object arrays
  • 14cf035: 8302987: Add uniform and spatially equidistributed bounded double streams to RandomGenerator
  • d1c788c: 8312392: ARM32 build broken since 8311035
  • c119037: 8311971: SA's ConstantPool.java uses incorrect computation to read long value in the constant pool
  • 028068a: 8312166: (dc) DatagramChannel's socket adaptor does not release carrier thread when blocking in receive
  • e7adbdb: 8311923: TestIRMatching.java fails on RISC-V
  • c6ab9c2: 8308103: Massive (up to ~30x) increase in C2 compilation time since JDK 17
  • ... and 30 more: https://git.openjdk.org/jdk/compare/a53345ad03e07ab2a990721a506ebc25eed0f7c9...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.

As you do not have Committer status in this project an existing Committer must agree to sponsor your change. Possible candidates are the reviewers of this PR (@JornVernee, @mlchung) but any other Committer may sponsor as well.

➡️ To flag this PR as ready for integration with the above commit message, type /integrate in a new comment. (Afterwards, your sponsor types /sponsor in a new comment to perform the integration).

@openjdk openjdk bot added ready Pull request is ready to be integrated and removed csr Pull request needs approved CSR before integration labels Jul 18, 2023
@liach
Copy link
Member Author

liach commented Jul 18, 2023

Joe Darcy asks us to consider whether a release note should be added. I think yes, since there's some behavioral differences like hidden vs non-hidden classes.

However, I think I might be able to upgrade asInterfaceInstance to accept protected abstract classes with a single public/protected abstract method(s) and a no-arg public/protected constructor, like ClassValue. If that's the case, do we still require a release note for this one, or do we defer and merge the notes?

@mlchung
Copy link
Member

mlchung commented Jul 18, 2023

I think applications unlikely depend on the old implementation of dynamic proxies. A release note may serve as a good documentation in case any application observes the performance difference because the hidden classes are GC'ed.

However, I think I might be able to upgrade asInterfaceInstance to accept protected abstract classes with a single public/protected abstract method(s) and a no-arg public/protected constructor, like ClassValue. If that's the case, do we still require a release note for this one, or do we defer and merge the notes?

This is a separate issue and should not combine with this.

@liach
Copy link
Member Author

liach commented Jul 19, 2023

I have made a release note documenting the potential performance impact: https://bugs.openjdk.org/browse/JDK-8312331
Please help review and revise. Thanks!

@liach
Copy link
Member Author

liach commented Jul 19, 2023

Looks like Mandy has cleaned up the release note. Thank you, integrating.

/integrate

@openjdk openjdk bot added the sponsor Pull request is ready to be sponsored label Jul 20, 2023
@openjdk
Copy link

openjdk bot commented Jul 20, 2023

@liach
Your change (at version a12fba0) is now ready to be sponsored by a Committer.

@mlchung
Copy link
Member

mlchung commented Jul 20, 2023

yes, I made some modification to the release note in particular the performance regression seems alarming. If this becomes a real issue, we could change it to soft reference.

This is great work!

/sponsor

@openjdk
Copy link

openjdk bot commented Jul 20, 2023

Going to push as commit 5d57b5c.
Since your change was applied there have been 40 commits pushed to the master branch:

  • 71cac8c: 8311663: Additional refactoring of Locale tests to JUnit
  • aa23fd9: 8311879: SA ClassWriter generates invalid invokedynamic code
  • 6f66213: 8312014: [s390x] TestSigInfoInHsErrFile.java Failure
  • b5b6f4e: 8312164: Refactor Arrays.hashCode for long, boolean, double, float, and Object arrays
  • 14cf035: 8302987: Add uniform and spatially equidistributed bounded double streams to RandomGenerator
  • d1c788c: 8312392: ARM32 build broken since 8311035
  • c119037: 8311971: SA's ConstantPool.java uses incorrect computation to read long value in the constant pool
  • 028068a: 8312166: (dc) DatagramChannel's socket adaptor does not release carrier thread when blocking in receive
  • e7adbdb: 8311923: TestIRMatching.java fails on RISC-V
  • c6ab9c2: 8308103: Massive (up to ~30x) increase in C2 compilation time since JDK 17
  • ... and 30 more: https://git.openjdk.org/jdk/compare/a53345ad03e07ab2a990721a506ebc25eed0f7c9...master

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Jul 20, 2023
@openjdk openjdk bot closed this Jul 20, 2023
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review sponsor Pull request is ready to be sponsored labels Jul 20, 2023
@openjdk
Copy link

openjdk bot commented Jul 20, 2023

@mlchung @liach Pushed as commit 5d57b5c.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core-libs core-libs-dev@openjdk.org integrated Pull request has been integrated
8 participants