Skip to content

Replace java.lang.reflect with MethodHandle/VarHandle across core modules#3300

Open
He-Pin wants to merge 4 commits into
mainfrom
refactor/replace-reflection-with-methodhandles
Open

Replace java.lang.reflect with MethodHandle/VarHandle across core modules#3300
He-Pin wants to merge 4 commits into
mainfrom
refactor/replace-reflection-with-methodhandles

Conversation

@He-Pin

@He-Pin He-Pin commented Jul 6, 2026

Copy link
Copy Markdown
Member

Motivation

java.lang.reflect Method.invoke, Constructor.newInstance, and Field.get/set bypass JIT inlining, adding overhead in critical paths like actor instantiation, protobuf serialization, and virtual thread management.

Modification

  • ReflectiveDynamicAccess: Replace Constructor.newInstance with MethodHandle.invokeWithArguments; replace Field.get(null) for MODULE$ with VarHandle.get
  • Reflect: Replace Constructor.newInstance with MethodHandle.invoke for both no-arg and parameterized constructors
  • VirtualThreadSupport: Replace Field.set/get with VarHandle.set/get for scheduler and DEFAULT_SCHEDULER fields; replace Constructor.newInstance with cached MethodHandle for CarrierThread
  • ProtobufSerializer: Replace Method-based parseFrom/toByteArray caches with MethodHandle caches for serialization hot path
  • LineNumbers: Replace Method.invoke with MethodHandle.invoke for writeReplace lambda introspection
  • ByteBufferCleaner: Replace Field.get(null) with VarHandle.get for theUnsafe static field access
  • ExtensionsImpl: Replace Method.invoke(null) with MethodHandle.invoke for Java singleton getInstance accessor

Result

VarHandle and MethodHandle enable JIT inlining of field access, method invocation, and constructor calls across all core reflection utilities, improving performance in actor creation, serialization, and virtual thread operations.

Tests

  • sbt "actor / compile" - success
  • sbt "actor-typed / compile" - success
  • sbt "remote / compile" - success

References

None - internal refactoring

Comment thread actor/src/main/java/org/apache/pekko/io/ByteBufferCleaner.java Outdated
Comment thread actor/src/main/scala/org/apache/pekko/actor/ReflectiveDynamicAccess.scala Outdated
Comment thread actor/src/main/scala/org/apache/pekko/actor/ReflectiveDynamicAccess.scala Outdated
Comment thread actor/src/main/scala/org/apache/pekko/dispatch/VirtualThreadSupport.scala Outdated
Comment thread actor/src/main/scala/org/apache/pekko/dispatch/VirtualThreadSupport.scala Outdated
Comment thread actor/src/main/scala/org/apache/pekko/util/Reflect.scala Outdated
@He-Pin He-Pin marked this pull request as draft July 6, 2026 08:46
@He-Pin

He-Pin commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

This pR is not ready, let's mark it to draft for now

He-Pin added a commit that referenced this pull request Jul 6, 2026
Motivation:
PR #3300 replaced reflective constructor access with MethodHandles, but public JDK constructors in non-open modules were forced through privateLookupIn and failed under Scala 3 CI during throwable deserialization.

Modification:
Prefer MethodHandles.publicLookup for public constructor handles and fall back to privateLookupIn only when needed. Use constructor metadata for no-arg constructor checks and add a regression test for public JDK constructors.

Result:
TimeoutException deserializes back to its original type and constructor lookup avoids unnecessary deep access on public constructors.

Tests:
- rtk bash -lc export JAVA_HOME=$(/usr/libexec/java_home -v 17); export PATH=$JAVA_HOME/bin:$PATH; sbt ++3.3.8 actor-tests/Test/testOnly org.apache.pekko.util.ReflectSpec - passed, 11 tests
- rtk bash -lc export JAVA_HOME=$(/usr/libexec/java_home -v 17); export PATH=$JAVA_HOME/bin:$PATH; sbt ++3.3.8 remote/Test/testOnly org.apache.pekko.remote.serialization.MiscMessageSerializerSpec - passed, 103 tests
- rtk bash -lc export JAVA_HOME=$(/usr/libexec/java_home -v 17); export PATH=$JAVA_HOME/bin:$PATH; sbt ++3.3.8 actor-tests/Test/testOnly org.apache.pekko.actor.ActorCreationTest - passed, 7 tests
- rtk scalafmt --mode diff-ref=origin/main - passed
- rtk scalafmt --list --mode diff-ref=origin/main - passed
- rtk git diff --check - passed

References:
Refs #3300
@He-Pin He-Pin marked this pull request as ready for review July 6, 2026 15:22
…s core modules

Motivation:
java.lang.reflect Method.invoke, Constructor.newInstance, and Field.get/set
bypass JIT inlining, adding overhead in critical paths like actor
instantiation, protobuf serialization, and virtual thread management.

Modification:
- ReflectiveDynamicAccess: replace Constructor.newInstance with
  MethodHandle.invokeWithArguments; replace Field.get(null) for MODULE$
  with VarHandle.get
- Reflect: replace Constructor.newInstance with MethodHandle.invoke;
  use Class.forName universally; prefer publicLookup for public constructors
- VirtualThreadSupport: replace Field.set/get with VarHandle.set/get
- ProtobufSerializer: replace Method-based caches with MethodHandle caches
- LineNumbers: replace Method.invoke with MethodHandle.invoke
- ByteBufferCleaner: replace Field.get(null) with VarHandle.get
- ExtensionsImpl: replace Method.invoke(null) with MethodHandle.invoke
- Replace java.lang.reflect with ClassGraph in test suites

Result:
JIT-inlinable field access and method invocation across all core reflection
utilities, improving performance in actor creation, serialization, and
virtual thread operations.

Tests:
- sbt ++3.3.8 actor-tests/Test/testOnly org.apache.pekko.util.ReflectSpec
- sbt ++3.3.8 remote/Test/testOnly org.apache.pekko.remote.serialization.MiscMessageSerializerSpec
- sbt ++3.3.8 actor-tests/Test/testOnly org.apache.pekko.actor.ActorCreationTest

Refs #3300
@He-Pin He-Pin force-pushed the refactor/replace-reflection-with-methodhandles branch from f3c07c6 to aa26fbe Compare July 6, 2026 16:48
@He-Pin He-Pin requested a review from pjfanning July 6, 2026 16:54
Motivation:\nAvoid invokeWithArguments() overhead on no-arg method-handle calls in core runtime reflection helpers.\n\nModification:\nUse MethodHandle.invoke() for static extension lookup, DNS resolver open(), and zero-arg constructor instantiation.\n\nResult:\nThe MethodHandle migration keeps its compatibility gains while trimming avoidable dispatch and allocation overhead.
Comment thread stream-tests/src/test/scala/org/apache/pekko/stream/DslConsistencySpec.scala Outdated
Comment thread actor/src/main/scala/org/apache/pekko/util/LineNumbers.scala Outdated
@He-Pin He-Pin marked this pull request as draft July 7, 2026 11:27
Motivation:
Address review feedback for PR #3300 and avoid replacing safe class loading and metadata reflection.

Modification:
Restore ordinary Class.forName and method metadata reflection where no deep reflective access is involved. Remove the ClassGraph test dependency and related package scanning. Cache MethodHandles lookups and fixed MethodType instances on the remaining MethodHandle paths.

Result:
Deep reflection remains handled by MethodHandle or VarHandle APIs while ordinary class loading keeps the original behavior and lower overhead.

Tests:
- rtk scalafmt --mode diff-ref=origin/main
- rtk scalafmt --list --mode diff-ref=origin/main
- sbt javafmtAll with JDK 17
- sbt javafmtCheckAll with JDK 17
- rtk git diff --check
- focused actor, stream, remote, DNS, persistence, persistence-tck, cluster-sharding-typed, and cluster-sharding MultiJvm sbt validation
- JDK21 virtual thread focused sbt validation

References:
Refs #3300
@He-Pin He-Pin marked this pull request as ready for review July 7, 2026 13:08
@He-Pin He-Pin requested a review from pjfanning July 7, 2026 13:13
@He-Pin He-Pin added this to the 2.0.0-M4 milestone Jul 7, 2026
Motivation:
Code review of PR #3300 identified several MethodHandle/VarHandle
instances that were re-created on every invocation, negating the
performance benefit of migrating from reflection. Additionally,
DnsSettings used publicLookup without fallback for --add-opens users.

Modification:
- VirtualThreadSupport: cache all MethodHandles/VarHandles as lazy vals
- LineNumbers: cache writeReplace MethodHandle per lambda class via
  ConcurrentHashMap (matching Reflect.scala pattern)
- DnsSettings: add privateLookupIn fallback when publicLookup fails on
  non-exported JDK-internal sun.net.dns package
- Reflect: replace non-idiomatic return with if/else expression
- BoxedType: remove unused primitiveForBoxed and toPrimitive

Result:
MethodHandle lookups execute once and are reused on subsequent calls.
DnsSettings works with both --add-exports and --add-opens configurations.
Dead code removed. Code style improved.

Tests:
Not run - targeted micro-optimizations to existing MethodHandle paths

References:
Refs #3300
@He-Pin He-Pin removed this from the 2.0.0-M4 milestone Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants