Skip to content

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

Open
He-Pin wants to merge 1 commit into
mainfrom
refactor/replace-reflection-with-methodhandles
Open

Replace java.lang.reflect with MethodHandle/VarHandle across core modules#3300
He-Pin wants to merge 1 commit 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, constructor, and field access adds repeated lookup/adaptation overhead in actor creation, serialization, and JVM integration paths.

Modification

  • Cache pre-adapted constructor handles in ReflectiveDynamicAccess, Reflect, and IndirectActorProducer.
  • Cache and use MethodHandles/VarHandles in VirtualThreadSupport, ProtobufSerializer, LineNumbers, DnsSettings, ByteBufferCleaner, and typed ExtensionsImpl.
  • Preserve JPMS lookup fallback, class initialization, fixed-arity varargs, and reflection-compatible exception behavior with focused regression tests.
  • Keep test-only JFR and file-descriptor access on cached MethodHandles.
  • Move the independent HashCode array-hashing optimization to Optimize JVM array hashing without reflection #3312.

Result

Hot paths avoid repeated reflective lookup and per-call adaptation while preserving behavior on modular JDKs. Actor creation JMH showed no regression and a small non-conclusive signal from 9.640 +/- 0.314 to 9.341 +/- 0.279 us/op.

Tests

  • sbt "actor-tests / Test / testOnly org.apache.pekko.util.ReflectSpec org.apache.pekko.actor.DynamicAccessSpec org.apache.pekko.PekkoExceptionSpec" - passed, 26 tests after the split
  • sbt "remote / Test / testOnly org.apache.pekko.remote.serialization.ProtobufSerializerSpec" - passed, 9 tests after the split
  • sbt "testkit / Test / testOnly org.apache.pekko.testkit.metrics.FileDescriptorMetricSetSpec" - passed, 1 test after the split
  • Earlier focused Scala 3.3.8, JDK 21, and JDK 25 reflection/virtual-thread validation passed on the same MethodHandle changes
  • sbt +headerCheckAll - passed
  • scalafmt --list --mode diff-ref=origin/main - passed
  • git diff --check - passed
  • qodercli formal stdout review - no must-fix findings

References

Refs #3300; independent HashCode optimization split to #3312

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
He-Pin added a commit that referenced this pull request Jul 6, 2026
…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
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
He-Pin added a commit that referenced this pull request Jul 7, 2026
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
He-Pin added a commit that referenced this pull request 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
He-Pin added a commit that referenced this pull request Jul 8, 2026
…re modules

Motivation:
java.lang.reflect (Method.invoke, Constructor.newInstance, Field.get/set)
prevents JIT inlining. MethodHandle/VarHandle provide equivalent
functionality with better performance characteristics.

Modification:
- Replace reflection with MethodHandle/VarHandle in ReflectiveDynamicAccess,
  Reflect, VirtualThreadSupport, ProtobufSerializer, LineNumbers,
  ByteBufferCleaner, and ExtensionsImpl
- Cache MethodHandles.lookup() values to avoid repeated lookups
- Remove unnecessary setAccessible(true) calls
- Use invoke() for zero-arg method handles

Result:
Better JIT inlining opportunities across core modules.

Tests:
- sbt "actor / Test / compile"
- sbt "actor-typed / Test / compile"

References:
Refs #3300
@He-Pin He-Pin force-pushed the refactor/replace-reflection-with-methodhandles branch from ebdd3fb to 062f859 Compare July 8, 2026 18:31
@He-Pin He-Pin added this to the 2.0.0-M4 milestone Jul 10, 2026
He-Pin added a commit that referenced this pull request Jul 10, 2026
Motivation:
Reduce repeated reflective lookup and per-call adaptation in actor creation, serialization, and JVM integration points while preserving behavior on modular JDKs.

Modification:
Cache pre-adapted MethodHandles and VarHandles at component or class lifetime, use exact invocation on hot paths, replace reflective array access with typed loops, and retain safe test migrations. Preserve JPMS caller lookup, class initialization, varargs, and exception behavior with focused regression tests.

Result:
Hot paths avoid repeated lookup, adaptation, and reflective array element access. Actor creation JMH showed no regression and a small non-conclusive improvement signal from 9.640 +/- 0.314 to 9.341 +/- 0.279 us/op.

Tests:
- JDK 17 focused actor reflection and dynamic access, protobuf serializer, file descriptor metrics, and typed extensions tests passed
- Scala 3.3.8 core reflection and dynamic access, protobuf serializer, file descriptor metrics, and typed extensions tests passed
- JDK 21 and JDK 25 focused reflection, protobuf, file descriptor metrics, and virtual-thread tests passed
- sbt javafmtCheckAll +headerCheckAll checkCodeStyle passed
- sbt +mimaReportBinaryIssues passed
- git diff --check passed
- scalafmt --list --mode diff-ref=origin/main passed
- qodercli formal stdout review found no must-fix findings
- sbt sortImports attempted; failed in unrelated modules with a scala-meta/scalafix NoSuchMethodError
- sbt validatePullRequest started on JDK 17 and was stopped at explicit user request after compilation progressed without errors

References:
Refs #3300
@He-Pin He-Pin force-pushed the refactor/replace-reflection-with-methodhandles branch from 062f859 to 0b2d049 Compare July 10, 2026 09:20
He-Pin added a commit that referenced this pull request Jul 10, 2026
Motivation:
Reduce repeated reflective lookup and per-call adaptation in actor creation, serialization, and JVM integration points while preserving reflection-compatible behavior on modular JDKs.

Modification:
Cache pre-adapted MethodHandles and VarHandles at component or class lifetime, use exact invocation on hot paths, and retain safe test migrations. Preserve JPMS caller lookup, class initialization, varargs, and exception behavior with focused regression tests.

Result:
Hot paths avoid repeated lookup and adaptation. Actor creation JMH showed no regression and a small non-conclusive improvement signal from 9.640 +/- 0.314 to 9.341 +/- 0.279 us/op.

Tests:
- JDK 17 focused actor reflection, dynamic access, PekkoException, protobuf serializer, and file descriptor metrics tests passed after the HashCode split: 36 tests
- Earlier focused Scala 3.3.8, JDK 21, and JDK 25 reflection/virtual-thread validation passed on the same MethodHandle changes
- sbt +headerCheckAll passed
- scalafmt --list --mode diff-ref=origin/main passed
- git diff --check passed
- qodercli formal stdout review found no must-fix findings
- GitHub Binary Compatibility, headers, code style, scalafmt, Scala 2.13/3/next tests, classic remoting, and Java 21 checks passed before the scope-only split

References:
Refs #3300
@He-Pin He-Pin force-pushed the refactor/replace-reflection-with-methodhandles branch from 0b2d049 to f3969ff Compare July 10, 2026 12:51
He-Pin added a commit that referenced this pull request Jul 10, 2026
Motivation:
HashCode.hash iterates JVM arrays through java.lang.reflect.Array, adding reflective per-element access and boxing in a shared utility.

Modification:
Dispatch all nine JVM array kinds to typed while loops. Add a directional test that uses the original reflective algorithm as the parity oracle across primitive, reference, empty, null-element, and nested arrays. Add a JMH benchmark for primitive and reference arrays.

Result:
Hash values remain compatible while JDK 17 JMH measurements improve by roughly 39x to 135x for the measured 16- and 256-element arrays.

Tests:
- JDK 17 Scala 2.13 HashCodeSpec: 3 tests passed
- JDK 17 Scala 3.3.8 HashCodeSpec: 3 tests passed
- JDK 17 JMH: Int[16] 824.277 to 6.106 ns/op; Int[256] 12749.433 to 183.033 ns/op; AnyRef[16] 635.492 to 13.945 ns/op; AnyRef[256] 9491.604 to 240.387 ns/op
- sbt +headerCheckAll and checkCodeStyle passed
- scalafmt --list --mode diff-ref=origin/main passed
- git diff --check passed
- qodercli formal stdout review found no must-fix findings

References:
Refs #3300
Motivation:
Reduce repeated reflective lookup and per-call adaptation in actor creation, serialization, and JVM integration points while preserving reflection-compatible behavior on modular JDKs.

Modification:
Cache pre-adapted MethodHandles and VarHandles at component or class lifetime, use exact invocation on hot paths, and retain safe test migrations. Preserve JPMS caller lookup, class initialization, varargs, and exception behavior with focused regression tests.

Result:
Hot paths avoid repeated lookup and adaptation. Actor creation JMH showed no regression and a small non-conclusive improvement signal from 9.640 +/- 0.314 to 9.341 +/- 0.279 us/op.

Tests:
- JDK 17 focused actor reflection, dynamic access, PekkoException, protobuf serializer, and file descriptor metrics tests passed after the HashCode split: 36 tests
- Earlier focused Scala 3.3.8, JDK 21, and JDK 25 reflection/virtual-thread validation passed on the same MethodHandle changes
- sbt +headerCheckAll passed
- scalafmt --list --mode diff-ref=origin/main passed
- git diff --check passed
- qodercli formal stdout review found no must-fix findings
- GitHub Binary Compatibility, headers, code style, scalafmt, Scala 2.13/3/next tests, classic remoting, and Java 21 checks passed before the scope-only split

References:
Refs #3300
@He-Pin He-Pin force-pushed the refactor/replace-reflection-with-methodhandles branch from f3969ff to a3d8cf4 Compare July 10, 2026 23:01
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