-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
The method handle implementation will be a huge improvement over fast classes and reflection, but it is not without risks.
- Spinning too many method handles consumes 'class meta space' and may create some surprising jit/garbage collection overheads
As we construct handles, the JVM will under the hood produce LambdaForms. These are aggresively 'interned' and optimized, but fundamentally are used to produce tiny .class files that host the generated methodhandle code. This whole process leverages a bunch of very well optimized and special cased codepaths in the JVM but still, dynamically creating lots of tiny classes has a cost. Possibly this will slow down 'startup', but it might also trigger some as yet unknown pathological behavior.
Mitigation idea: Investigate a more structured 'intermediate represenation' than just MethodHandle. This would allow us to generate fewer intermediate handles.
- a simple example is injecting a Collection<Provider<T>> (from a multibinder) into an @Provides method. We end up adding a bunch of silly wrapper handles. such that the resulting thing looks like
// from the delegate factory
var handle = MethodHandles.dropArguments(MethodHandles.constant(Object.class, <collection-constant>), 0, InternalContext.class, Dependency.class);
// from SingleParameterInjector
handle = MethodHandles.insertArguments(handle, 1, dependency);
handle = catchInternalProvisionExceptionAndRethrowWithSource(handle, dependency);
/// plus logic to attach it to the provider method using `filterArguments`
If we knew it was a constant, this could boil down to a insertArguments call for the provider method instead of adding parameters to ignore and then passing them. At the very least we could avoid the try catch.
The JIT can do these things already through the magic of inlining, but generating 6 tiny methods adds overhead. This is exactly why compilers use 'intermediate representations', so that they can leverage higher level abstractions to eliminate redundant work. In the above case we know the value will never fail and is never null and doesn't depend on context or dependency.
- startup time regression
I had hoped to measure this on some Google applications but never did.
In theory with this approach we never allocate a FastClass (ignoring AOP and certain silly edge cases), so Guice.createInjector will be a bit faster, however those initial Provider.get calls will be more expensive as we produce and link MethodHandles for the first time. Due to caching and the way handles work internally, this will be amortized over time but still it is ambiguous what the impact will be overall.
If we do see startup time regressions (which from our perspective would be time from Guice.createinjector() to when the first Provider.get() call completes), then we could pursue solution #1 which should help, but beyond that there isn't a ton of ideas.
In the openjdk project they fixed a startup problem with string-concatenation via more hand-coded class generation: openjdk/jdk#20273
This could work for us as well but would be a large undertaking. Still with some profiling information about where time is spent it could make sense to take this approach for some specific cases (multibinders? perhaps the logic around tryStart/finish should be hand coded?), which would greatly decrease the effort. Still for that to be attractive it might make sense to wait for jdk24 so we can drop our asm dependency and leverage the ClassBuilding api.
- Security! Modules! Unsafe!
Currently guice is using Unsafe in a few places to define classes and access package private methods in foreign modules/classloaders. The MethodHandle implementation grealy reduces our reliance on these things but increases our reliance on the setAccessible API.
So what to do!
Well we could continue to rely on setAccessible, and just tell users to add --add-opens` calls to address any issues, but that is lame, it would be better to create a fine grained capability API for users to use.
Binder.provideAccess(MethodHandles.Lookup lookup)
Then guice can use these lookups to unreflect Methods Constructors and Fields in other packages.
If we had that we could simply report errors when unreflect fails and advise users to pass us a Lookup object to address. Similarly we could use this to avoid the use of Unsafe in our back-compat fast-class API, since defineHiddenClass can target package private classes given an appropriate Lookup object.
So this API will probably be required even if the overall MethodHandle approach crashes and dies for some reason.
- Eliminate the BytecodeGen option?
MethodHandles are certainly bytecode generation... but then again java.lang.reflect is implemented in terms of methodhandles already so any user setting this option is already using methodhandles. So i believe the only real usecase here is android? and possibly restricted runtime environments like app-engine?
A downside of the MethodHandle implementation is that a number of core classes got split into 3 paths (method-handles, fast-class, reflection), so it would be very nice to get that down to exactly 1 implementation to reduce testing and maintenance overhead.
I might poke at idea 1 above, but then again maybe not 👍 Just wanted to write this down so it isn't lost