From 062f859503fb1c8a4d65778d60de92e88014c7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=99=8E=E9=B8=A3?= Date: Thu, 9 Jul 2026 02:31:19 +0800 Subject: [PATCH] perf: replace java.lang.reflect with MethodHandle/VarHandle across core 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 --- .../org/apache/pekko/PekkoExceptionSpec.scala | 4 +- .../apache/pekko/util/ByteIteratorSpec.scala | 10 +- .../util/ByteStringInitializationSpec.scala | 9 +- .../org/apache/pekko/util/ReflectSpec.scala | 9 +- .../actor/typed/internal/ExtensionsImpl.scala | 5 +- .../apache/pekko/io/ByteBufferCleaner.java | 11 +- .../apache/pekko/actor/AbstractProps.scala | 40 ++----- .../pekko/actor/IndirectActorProducer.scala | 2 +- .../pekko/actor/ReflectiveDynamicAccess.scala | 56 ++++++++-- .../org/apache/pekko/dispatch/Mailboxes.scala | 4 +- .../pekko/dispatch/VirtualThreadSupport.scala | 84 ++++++++------ .../org/apache/pekko/io/dns/DnsSettings.scala | 33 ++++-- .../org/apache/pekko/util/HashCode.scala | 20 +++- .../org/apache/pekko/util/LineNumbers.scala | 21 +++- .../scala/org/apache/pekko/util/Reflect.scala | 105 ++++++++++++------ .../cluster/sharding/FlightRecording.scala | 18 ++- .../DurableStateExceptionsSpec.scala | 4 +- .../serialization/ProtobufSerializer.scala | 49 ++++---- .../metrics/FileDescriptorMetricSet.scala | 28 ++++- 19 files changed, 330 insertions(+), 182 deletions(-) diff --git a/actor-tests/src/test/scala/org/apache/pekko/PekkoExceptionSpec.scala b/actor-tests/src/test/scala/org/apache/pekko/PekkoExceptionSpec.scala index 1c178ceb94a..384f8ce1b4f 100644 --- a/actor-tests/src/test/scala/org/apache/pekko/PekkoExceptionSpec.scala +++ b/actor-tests/src/test/scala/org/apache/pekko/PekkoExceptionSpec.scala @@ -13,6 +13,8 @@ package org.apache.pekko +import java.lang.invoke.{ MethodHandles, MethodType } + import org.apache.pekko.actor._ import org.scalatest.matchers.should.Matchers @@ -37,6 +39,6 @@ class PekkoExceptionSpec extends AnyWordSpec with Matchers { } def verify(clazz: java.lang.Class[?]): Unit = { - clazz.getConstructor(Array(classOf[String]): _*) + MethodHandles.publicLookup().findConstructor(clazz, MethodType.methodType(Void.TYPE, classOf[String])) } } diff --git a/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala b/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala index 4539117a238..2f3a4815061 100644 --- a/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala +++ b/actor-tests/src/test/scala/org/apache/pekko/util/ByteIteratorSpec.scala @@ -35,11 +35,13 @@ class ByteIteratorSpec extends AnyWordSpec with Matchers { freshIterator().indexOf(0x10, 3) should be(5) // There is also an indexOf with another signature, which is hard to invoke :D + val otherIndexOfHandle = + java.lang.invoke.MethodHandles.publicLookup().findVirtual( + classOf[ByteIterator], + "indexOf", + java.lang.invoke.MethodType.methodType(classOf[Int], classOf[Byte], classOf[Int])) def otherIndexOf(iterator: ByteIterator, byte: Byte, from: Int): Int = - classOf[ByteIterator] - .getMethod("indexOf", classOf[Byte], classOf[Int]) - .invoke(iterator, byte.asInstanceOf[Object], from.asInstanceOf[Object]) - .asInstanceOf[Int] + otherIndexOfHandle.invoke(iterator, byte, from).asInstanceOf[Int] otherIndexOf(freshIterator(), 0x20, 1) should be(1) otherIndexOf(freshIterator(), 0x10, 1) should be(2) diff --git a/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringInitializationSpec.scala b/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringInitializationSpec.scala index 071fbea89c4..d68aef41285 100644 --- a/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringInitializationSpec.scala +++ b/actor-tests/src/test/scala/org/apache/pekko/util/ByteStringInitializationSpec.scala @@ -46,10 +46,11 @@ class ByteStringInitializationSpec extends AnyWordSpec with Matchers { } } - cleanCl - .loadClass("org.apache.pekko.util.ByteStringInitTest") - .getDeclaredConstructor() - .newInstance() + val clazz = cleanCl.loadClass("org.apache.pekko.util.ByteStringInitTest") + java.lang.invoke.MethodHandles + .privateLookupIn(clazz, java.lang.invoke.MethodHandles.lookup()) + .findConstructor(clazz, java.lang.invoke.MethodType.methodType(Void.TYPE)) + .invokeWithArguments() .asInstanceOf[Runnable] .run() } diff --git a/actor-tests/src/test/scala/org/apache/pekko/util/ReflectSpec.scala b/actor-tests/src/test/scala/org/apache/pekko/util/ReflectSpec.scala index d786578cef3..3cb6ad6f90b 100644 --- a/actor-tests/src/test/scala/org/apache/pekko/util/ReflectSpec.scala +++ b/actor-tests/src/test/scala/org/apache/pekko/util/ReflectSpec.scala @@ -13,6 +13,8 @@ package org.apache.pekko.util +import java.util.concurrent.TimeoutException + import scala.annotation.nowarn import scala.collection.immutable @@ -50,7 +52,7 @@ class ReflectSpec extends AnyWordSpec with Matchers { } "deal with `null` in 1 matching case" in { val constructor = Reflect.findConstructor(classOf[One], immutable.Seq(null)) - constructor.newInstance(null) + constructor.invoke(null.asInstanceOf[AnyRef]) } "deal with multiple constructors" in { Reflect.findConstructor(classOf[MultipleOne], immutable.Seq(new A)) @@ -62,6 +64,11 @@ class ReflectSpec extends AnyWordSpec with Matchers { Reflect.findConstructor(classOf[MultipleOne], immutable.Seq(null)) } } + "use public lookup for public JDK constructors in non-open modules" in { + val constructor = Reflect.findConstructor(classOf[TimeoutException], immutable.Seq("err")) + val instance = Reflect.instantiate[TimeoutException](constructor, immutable.Seq("err")) + instance.getMessage should ===("err") + } } "Reflect#getCallerClass" must { diff --git a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/ExtensionsImpl.scala b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/ExtensionsImpl.scala index 53395451256..09e93f3518e 100644 --- a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/ExtensionsImpl.scala +++ b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/ExtensionsImpl.scala @@ -23,6 +23,7 @@ import org.apache.pekko import pekko.actor.typed.{ ActorSystem, Extension, ExtensionId, Extensions } import pekko.actor.typed.ExtensionSetup import pekko.annotation.InternalApi +import pekko.util.Reflect /** * INTERNAL API @@ -70,8 +71,8 @@ private[pekko] trait ExtensionsImpl extends Extensions { self: ActorSystem[?] wi dynamicAccess.getClassFor[ExtensionId[Extension]](extensionIdFQCN).flatMap[ExtensionId[Extension]] { (clazz: Class[?]) => Try { - val singletonAccessor = clazz.getDeclaredMethod("getInstance") - singletonAccessor.invoke(null).asInstanceOf[ExtensionId[Extension]] + val handle = Reflect.findStaticNoArgMethod(clazz, "getInstance") + handle.invoke().asInstanceOf[ExtensionId[Extension]] } } diff --git a/actor/src/main/java/org/apache/pekko/io/ByteBufferCleaner.java b/actor/src/main/java/org/apache/pekko/io/ByteBufferCleaner.java index aab02e45ab7..47b722166c2 100644 --- a/actor/src/main/java/org/apache/pekko/io/ByteBufferCleaner.java +++ b/actor/src/main/java/org/apache/pekko/io/ByteBufferCleaner.java @@ -21,7 +21,7 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; -import java.lang.reflect.Field; +import java.lang.invoke.VarHandle; import java.nio.ByteBuffer; /** @@ -50,11 +50,12 @@ private static final class Java9Cleaner implements Cleaner { private final MethodHandle invokeCleaner; private Java9Cleaner() throws ReflectiveOperationException { - final Class unsafeClass = Class.forName("sun.misc.Unsafe"); - final Field field = unsafeClass.getDeclaredField("theUnsafe"); - field.setAccessible(true); - final Object theUnsafe = field.get(null); MethodHandles.Lookup lookup = MethodHandles.lookup(); + final Class unsafeClass = lookup.findClass("sun.misc.Unsafe"); + final VarHandle unsafeHandle = + MethodHandles.privateLookupIn(unsafeClass, lookup) + .findStaticVarHandle(unsafeClass, "theUnsafe", unsafeClass); + final Object theUnsafe = unsafeHandle.get(); MethodHandle invokeCleaner = lookup.findVirtual( unsafeClass, "invokeCleaner", methodType(void.class, ByteBuffer.class)); diff --git a/actor/src/main/scala/org/apache/pekko/actor/AbstractProps.scala b/actor/src/main/scala/org/apache/pekko/actor/AbstractProps.scala index bff3f4f22fa..9ce98b13d4e 100644 --- a/actor/src/main/scala/org/apache/pekko/actor/AbstractProps.scala +++ b/actor/src/main/scala/org/apache/pekko/actor/AbstractProps.scala @@ -13,10 +13,8 @@ package org.apache.pekko.actor -import java.lang.reflect.Constructor import java.lang.reflect.Modifier -import scala.annotation.tailrec import scala.annotation.varargs import org.apache.pekko @@ -58,42 +56,18 @@ private[pekko] trait AbstractProps { private def checkCreatorClosingOver(clazz: Class[?]): Unit = { val enclosingClass = clazz.getEnclosingClass - def hasDeclaredConstructorWithEmptyParams(declaredConstructors: Array[Constructor[?]]): Boolean = { - @tailrec def loop(i: Int): Boolean = { - if (i == declaredConstructors.length) false - else { - if (declaredConstructors(i).getParameterCount == 0) - true - else - loop(i + 1) // recur - } - } - loop(0) - } - - def hasDeclaredConstructorWithEnclosingClassParam(declaredConstructors: Array[Constructor[?]]): Boolean = { - @tailrec def loop(i: Int): Boolean = { - if (i == declaredConstructors.length) false - else { - val c = declaredConstructors(i) - if (c.getParameterCount >= 1 && c.getParameterTypes()(0) == enclosingClass) - true - else - loop(i + 1) // recur - } - } - loop(0) - } - def hasValidConstructor: Boolean = { - val constructorsLength = clazz.getConstructors.length - if (constructorsLength > 0) + if (clazz.getConstructors.nonEmpty) true else { - val decl = clazz.getDeclaredConstructors + val declaredConstructors = clazz.getDeclaredConstructors // the hasDeclaredConstructorWithEnclosingClassParam check is for supporting `new Creator {` // which was supported in versions before 2.4.5 - hasDeclaredConstructorWithEmptyParams(decl) || !hasDeclaredConstructorWithEnclosingClassParam(decl) + declaredConstructors.exists(_.getParameterCount == 0) || + !((enclosingClass ne null) && declaredConstructors.exists { constructor => + val parameterTypes = constructor.getParameterTypes + parameterTypes.nonEmpty && parameterTypes(0) == enclosingClass + }) } } diff --git a/actor/src/main/scala/org/apache/pekko/actor/IndirectActorProducer.scala b/actor/src/main/scala/org/apache/pekko/actor/IndirectActorProducer.scala index 39dec1dae9d..e6f142c8435 100644 --- a/actor/src/main/scala/org/apache/pekko/actor/IndirectActorProducer.scala +++ b/actor/src/main/scala/org/apache/pekko/actor/IndirectActorProducer.scala @@ -107,7 +107,7 @@ private[pekko] class ArgsReflectConstructor(clz: Class[? <: Actor], args: immuta extends IndirectActorProducer { private val constructor = Reflect.findConstructor(clz, args) override def actorClass = clz - override def produce() = Reflect.instantiate(constructor, args) + override def produce() = Reflect.instantiate[Actor](constructor, args) } /** diff --git a/actor/src/main/scala/org/apache/pekko/actor/ReflectiveDynamicAccess.scala b/actor/src/main/scala/org/apache/pekko/actor/ReflectiveDynamicAccess.scala index a35feafce5c..7427b854c4b 100644 --- a/actor/src/main/scala/org/apache/pekko/actor/ReflectiveDynamicAccess.scala +++ b/actor/src/main/scala/org/apache/pekko/actor/ReflectiveDynamicAccess.scala @@ -13,7 +13,8 @@ package org.apache.pekko.actor -import java.lang.reflect.InvocationTargetException +import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType, VarHandle } +import java.util.concurrent.ConcurrentHashMap import scala.collection.immutable import scala.reflect.ClassTag @@ -26,13 +27,14 @@ import pekko.annotation.DoNotInherit /** * This is the default [[pekko.actor.DynamicAccess]] implementation used by [[pekko.actor.ExtendedActorSystem]] * unless overridden. It uses reflection to turn fully-qualified class names into `Class[_]` objects - * and creates instances from there using `getDeclaredConstructor()` and invoking that. The class loader + * and creates instances from there using constructor method handles. The class loader * to be used for all this is determined by the actor system’s class loader by default. * * Not for user extension or construction */ @DoNotInherit class ReflectiveDynamicAccess(val classLoader: ClassLoader) extends DynamicAccess { + import ReflectiveDynamicAccess._ override def getClassFor[T: ClassTag](fqcn: String): Try[Class[? <: T]] = Try[Class[? <: T]] { @@ -45,13 +47,12 @@ class ReflectiveDynamicAccess(val classLoader: ClassLoader) extends DynamicAcces Try { val types = args.map(_._1).toArray val values = args.map(_._2).toArray - val constructor = clazz.getDeclaredConstructor(types: _*) - constructor.setAccessible(true) - val obj = constructor.newInstance(values: _*) + val handle = constructorHandle(clazz, types) + val obj = handle.invokeWithArguments(values: _*) val t = implicitly[ClassTag[T]].runtimeClass if (t.isInstance(obj)) obj.asInstanceOf[T] else throw new ClassCastException(clazz.getName + " is not a subtype of " + t) - }.recover { case i: InvocationTargetException if i.getTargetException ne null => throw i.getTargetException } + } override def createInstanceFor[T: ClassTag](fqcn: String, args: immutable.Seq[(Class[?], AnyRef)]): Try[T] = getClassFor(fqcn).flatMap { c => @@ -60,7 +61,7 @@ class ReflectiveDynamicAccess(val classLoader: ClassLoader) extends DynamicAcces override def classIsOnClasspath(fqcn: String): Boolean = getClassFor[Any](fqcn) match { - case Failure(_: ClassNotFoundException | _: NoClassDefFoundError) => + case Failure(_: ClassNotFoundException | _: NoClassDefFoundError | _: LinkageError) => false case _ => true @@ -72,17 +73,50 @@ class ReflectiveDynamicAccess(val classLoader: ClassLoader) extends DynamicAcces else getClassFor(fqcn + "$").recoverWith { case _ => getClassFor(fqcn) } classTry.flatMap { c => Try { - val module = c.getDeclaredField("MODULE$") - module.setAccessible(true) + val moduleHandle = moduleHandleFor(c) val t = implicitly[ClassTag[T]].runtimeClass - module.get(null) match { + moduleHandle.get() match { case null => throw new NullPointerException case x if !t.isInstance(x) => throw new ClassCastException(fqcn + " is not a subtype of " + t) case x: T => x case unexpected => throw new IllegalArgumentException(s"Unexpected module field: $unexpected") // will not happen, for exhaustiveness check } - }.recover { case i: InvocationTargetException if i.getTargetException ne null => throw i.getTargetException } + } + } + } +} + +private object ReflectiveDynamicAccess { + private val lookup = MethodHandles.lookup() + private val publicLookup = MethodHandles.publicLookup() + private val constructorHandles = new ConcurrentHashMap[ConstructorKey, MethodHandle] + private val moduleHandles = new ConcurrentHashMap[Class[?], VarHandle] + private final case class ConstructorKey(clazz: Class[?], parameterTypes: Vector[Class[?]]) + + private def constructorHandle(clazz: Class[?], parameterTypes: Array[Class[?]]): MethodHandle = { + val key = ConstructorKey(clazz, parameterTypes.toVector) + val cached = constructorHandles.get(key) + if (cached ne null) cached + else { + val methodType = MethodType.methodType(Void.TYPE, parameterTypes.asInstanceOf[Array[Class[?]]]) + val handle = try publicLookup.findConstructor(clazz, methodType) + catch { + case _: IllegalAccessException | _: NoSuchMethodException => + MethodHandles.privateLookupIn(clazz, lookup).findConstructor(clazz, methodType) + } + val existing = constructorHandles.putIfAbsent(key, handle) + if (existing eq null) handle else existing + } + } + + private def moduleHandleFor(clazz: Class[?]): VarHandle = { + val cached = moduleHandles.get(clazz) + if (cached ne null) cached + else { + val handle = MethodHandles.privateLookupIn(clazz, lookup).findStaticVarHandle(clazz, "MODULE$", clazz) + val existing = moduleHandles.putIfAbsent(clazz, handle) + if (existing eq null) handle else existing } } } diff --git a/actor/src/main/scala/org/apache/pekko/dispatch/Mailboxes.scala b/actor/src/main/scala/org/apache/pekko/dispatch/Mailboxes.scala index f255eb832d4..586acc75fb2 100644 --- a/actor/src/main/scala/org/apache/pekko/dispatch/Mailboxes.scala +++ b/actor/src/main/scala/org/apache/pekko/dispatch/Mailboxes.scala @@ -123,7 +123,7 @@ private[pekko] class Mailboxes( throw new IllegalArgumentException(s"no wildcard type allowed in RequireMessageQueue argument (was [$x])") } case unexpected => - throw new IllegalArgumentException(s"Unexpected actor class marker: $unexpected") // will not happen, for exhaustiveness check + throw new IllegalArgumentException(s"Unexpected actor class marker: $unexpected") } // don’t care if this happens twice @@ -148,7 +148,7 @@ private[pekko] class Mailboxes( s"no wildcard type allowed in ProducesMessageQueue argument (was [$x])") } case unexpected => - throw new IllegalArgumentException(s"Unexpected message queue type marker: $unexpected") // will not happen, for exhaustiveness check + throw new IllegalArgumentException(s"Unexpected message queue type marker: $unexpected") } } diff --git a/actor/src/main/scala/org/apache/pekko/dispatch/VirtualThreadSupport.scala b/actor/src/main/scala/org/apache/pekko/dispatch/VirtualThreadSupport.scala index 4d2595bce6d..c7ea51466c7 100644 --- a/actor/src/main/scala/org/apache/pekko/dispatch/VirtualThreadSupport.scala +++ b/actor/src/main/scala/org/apache/pekko/dispatch/VirtualThreadSupport.scala @@ -18,7 +18,7 @@ package org.apache.pekko.dispatch import java.lang.invoke.{ MethodHandles, MethodType } -import java.util.concurrent.{ ExecutorService, ForkJoinPool, ForkJoinWorkerThread, ThreadFactory } +import java.util.concurrent.{ Executor, ExecutorService, ForkJoinPool, ForkJoinWorkerThread, ThreadFactory } import scala.util.control.NonFatal @@ -30,6 +30,43 @@ import pekko.util.JavaVersion private[dispatch] object VirtualThreadSupport { private val zero = java.lang.Long.valueOf(0L) private val lookup = MethodHandles.publicLookup() + private val privateLookup = MethodHandles.lookup() + private val newThreadPerTaskExecutorMethodType = + MethodType.methodType(classOf[ExecutorService], classOf[ThreadFactory]) + private val threadFactoryMethodType = MethodType.methodType(classOf[ThreadFactory]) + private val carrierThreadConstructorMethodType = MethodType.methodType(Void.TYPE, classOf[ForkJoinPool]) + + // Cached method/var handles to avoid repeated lookups on every invocation + private lazy val newThreadPerTaskExecutorHandle = { + val executorsClazz = ClassLoader.getSystemClassLoader.loadClass("java.util.concurrent.Executors") + lookup.findStatic(executorsClazz, "newThreadPerTaskExecutor", newThreadPerTaskExecutorMethodType) + } + + private lazy val threadBuilderClass = ClassLoader.getSystemClassLoader.loadClass("java.lang.Thread$Builder") + private lazy val threadBuilderOfVirtualClass = + ClassLoader.getSystemClassLoader.loadClass("java.lang.Thread$Builder$OfVirtual") + + private lazy val ofVirtualHandle = + lookup.findStatic(classOf[Thread], "ofVirtual", MethodType.methodType(threadBuilderOfVirtualClass)) + + private lazy val ofVirtualNameHandle = + lookup.findVirtual(threadBuilderOfVirtualClass, "name", + MethodType.methodType(threadBuilderOfVirtualClass, classOf[String])) + + private lazy val ofVirtualNameWithStartHandle = + lookup.findVirtual(threadBuilderOfVirtualClass, "name", + MethodType.methodType(threadBuilderOfVirtualClass, classOf[String], java.lang.Long.TYPE)) + + private lazy val builderFactoryHandle = + lookup.findVirtual(threadBuilderClass, "factory", threadFactoryMethodType) + + private lazy val virtualThreadClass = + ClassLoader.getSystemClassLoader.loadClass("java.lang.VirtualThread") + + private lazy val defaultSchedulerHandle = + MethodHandles + .privateLookupIn(virtualThreadClass, privateLookup) + .findStaticVarHandle(virtualThreadClass, "DEFAULT_SCHEDULER", classOf[ForkJoinPool]) /** * Is virtual thread supported @@ -42,12 +79,7 @@ private[dispatch] object VirtualThreadSupport { def newThreadPerTaskExecutor(threadFactory: ThreadFactory): ExecutorService = { require(threadFactory != null, "threadFactory should not be null.") try { - val executorsClazz = ClassLoader.getSystemClassLoader.loadClass("java.util.concurrent.Executors") - val newThreadPerTaskExecutorMethod = lookup.findStatic( - executorsClazz, - "newThreadPerTaskExecutor", - MethodType.methodType(classOf[ExecutorService], classOf[ThreadFactory])) - newThreadPerTaskExecutorMethod.invoke(threadFactory).asInstanceOf[ExecutorService] + newThreadPerTaskExecutorHandle.invoke(threadFactory).asInstanceOf[ExecutorService] } catch { case NonFatal(e) => // --add-opens java.base/java.lang=ALL-UNNAMED @@ -84,30 +116,22 @@ private[dispatch] object VirtualThreadSupport { require(isSupported, "Virtual thread is not supported.") require(prefix != null && prefix.nonEmpty, "prefix should not be null or empty.") try { - val builderClass = ClassLoader.getSystemClassLoader.loadClass("java.lang.Thread$Builder") - val ofVirtualClass = ClassLoader.getSystemClassLoader.loadClass("java.lang.Thread$Builder$OfVirtual") - val ofVirtualMethod = lookup.findStatic(classOf[Thread], "ofVirtual", MethodType.methodType(ofVirtualClass)) - var builder = ofVirtualMethod.invoke() + var builder = ofVirtualHandle.invoke() // set the name if (start <= -1) { - val nameMethod = lookup.findVirtual(ofVirtualClass, "name", - MethodType.methodType(ofVirtualClass, classOf[String])) - builder = nameMethod.invoke(builder, prefix + "-virtual-thread") + builder = ofVirtualNameHandle.invoke(builder, prefix + "-virtual-thread") } else { - val nameMethod = lookup.findVirtual(ofVirtualClass, "name", - MethodType.methodType(ofVirtualClass, classOf[String], classOf[Long])) - builder = nameMethod.invoke(builder, prefix + "-virtual-thread-", zero) + builder = ofVirtualNameWithStartHandle.invoke(builder, prefix + "-virtual-thread-", zero) } // set the scheduler if (executor ne null) { - // Use reflection here, method handle is stricter on access control val clazz = builder.getClass - val field = clazz.getDeclaredField("scheduler") - field.setAccessible(true) - field.set(builder, executor) + val schedulerHandle = + MethodHandles.privateLookupIn(clazz, privateLookup).findVarHandle(clazz, "scheduler", + classOf[Executor]) + schedulerHandle.set(builder, executor) } - val factoryMethod = lookup.findVirtual(builderClass, "factory", MethodType.methodType(classOf[ThreadFactory])) - factoryMethod.invoke(builder).asInstanceOf[ThreadFactory] + builderFactoryHandle.invoke(builder).asInstanceOf[ThreadFactory] } catch { case NonFatal(e) => // --add-opens java.base/java.lang=ALL-UNNAMED @@ -119,10 +143,12 @@ private[dispatch] object VirtualThreadSupport { // --add-opens java.base/java.lang=ALL-UNNAMED // --add-opens java.base/jdk.internal.misc=ALL-UNNAMED private val clazz = ClassLoader.getSystemClassLoader.loadClass("jdk.internal.misc.CarrierThread") - // TODO lookup.findClass is only available in Java 9 - private val constructor = clazz.getDeclaredConstructor(classOf[ForkJoinPool]) + private val constructorHandle = + MethodHandles + .privateLookupIn(clazz, privateLookup) + .findConstructor(clazz, carrierThreadConstructorMethodType) override def newThread(pool: ForkJoinPool): ForkJoinWorkerThread = { - constructor.newInstance(pool).asInstanceOf[ForkJoinWorkerThread] + constructorHandle.invoke(pool).asInstanceOf[ForkJoinWorkerThread] } } @@ -132,11 +158,7 @@ private[dispatch] object VirtualThreadSupport { def getVirtualThreadDefaultScheduler: ForkJoinPool = try { require(isSupported, "Virtual thread is not supported.") - val clazz = ClassLoader.getSystemClassLoader.loadClass("java.lang.VirtualThread") - val fieldName = "DEFAULT_SCHEDULER" - val field = clazz.getDeclaredField(fieldName) - field.setAccessible(true) - field.get(null).asInstanceOf[ForkJoinPool] + defaultSchedulerHandle.get().asInstanceOf[ForkJoinPool] } catch { case NonFatal(e) => // --add-opens java.base/java.lang=ALL-UNNAMED diff --git a/actor/src/main/scala/org/apache/pekko/io/dns/DnsSettings.scala b/actor/src/main/scala/org/apache/pekko/io/dns/DnsSettings.scala index 273d2f15963..2f5489797c3 100644 --- a/actor/src/main/scala/org/apache/pekko/io/dns/DnsSettings.scala +++ b/actor/src/main/scala/org/apache/pekko/io/dns/DnsSettings.scala @@ -30,6 +30,7 @@ package org.apache.pekko.io.dns import java.io.File +import java.lang.invoke.{ MethodHandles, MethodType } import java.net.{ InetSocketAddress, URI } import java.util @@ -160,7 +161,8 @@ private[dns] final class DnsSettings(system: ExtendedActorSystem, c: Config) { def failUnableToDetermineDefaultNameservers = throw new IllegalStateException( - "Unable to obtain default nameservers from JNDI or via reflection. " + + "Unable to obtain default nameservers from JNDI or via the JDK internal ResolverConfiguration fallback. " + + "On modular JDKs, that fallback requires access to java.base/sun.net.dns. " + "Please set `pekko.io.dns.async-dns.nameservers` explicitly in order to be able to resolve domain names. ") } @@ -169,6 +171,9 @@ object DnsSettings { private final val DnsFallbackPort = 53 private val inetSocketAddress = """(.*?)(?::(\d+))?""".r + private val publicLookup = MethodHandles.publicLookup() + private val lookup = MethodHandles.lookup() + private val nameserversMethodType = MethodType.methodType(classOf[util.List[?]]) /** * INTERNAL API @@ -186,7 +191,8 @@ object DnsSettings { * Find out the default search lists that Java would use normally, e.g. when using InetAddress to resolve domains. * * The default nameservers are attempted to be obtained from: jndi-dns and from `sun.net.dnsResolverConfiguration` - * as a fallback (which is expected to fail though when running on JDK9+ due to the module encapsulation of sun packages). + * as a fallback. That JDK-internal fallback is expected to fail when running on JDK9+ unless java.base/sun.net.dns + * is exported or opened to Pekko. * * Based on: https://github.com/netty/netty/blob/4.1/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java#L58-L146 */ @@ -229,24 +235,31 @@ object DnsSettings { } } - // this method is used as a fallback in case JNDI results in an empty list - // this method will not work when running modularised of course since it needs access to internal sun classes - def getNameserversUsingReflection: Try[List[InetSocketAddress]] = { + // This method is used as a best-effort fallback in case JNDI results in an empty list. It needs access to + // JDK-internal sun classes, so it will not work on modular JDKs unless java.base/sun.net.dns is exported or opened. + def getNameserversUsingMethodHandles: Try[List[InetSocketAddress]] = { system.dynamicAccess.getClassFor[Any]("sun.net.dns.ResolverConfiguration").flatMap { c => Try { - val open = c.getMethod("open") - val nameservers = c.getMethod("nameservers") - val instance = open.invoke(null) + val effectiveLookup: MethodHandles.Lookup = + try { + publicLookup.findStatic(c, "open", MethodType.methodType(c)) + publicLookup + } catch { + case _: IllegalAccessException => MethodHandles.privateLookupIn(c, lookup) + } + val open = effectiveLookup.findStatic(c, "open", MethodType.methodType(c)) + val nameservers = effectiveLookup.findVirtual(c, "nameservers", nameserversMethodType) + val instance = open.invoke() val ns = nameservers.invoke(instance).asInstanceOf[util.List[String]] val res = if (ns.isEmpty) throw new IllegalStateException( - "Empty nameservers list discovered using reflection. Consider configuring default nameservers manually!") + "Empty nameservers list discovered using method handles. Consider configuring default nameservers manually!") else ns.asScala.toList res.flatMap(s => asInetSocketAddress(s).toOption) } } } - getNameserversUsingJNDI.orElse(getNameserversUsingReflection) + getNameserversUsingJNDI.orElse(getNameserversUsingMethodHandles) } } diff --git a/actor/src/main/scala/org/apache/pekko/util/HashCode.scala b/actor/src/main/scala/org/apache/pekko/util/HashCode.scala index fa52f1ff0fb..29019f57096 100644 --- a/actor/src/main/scala/org/apache/pekko/util/HashCode.scala +++ b/actor/src/main/scala/org/apache/pekko/util/HashCode.scala @@ -14,7 +14,6 @@ package org.apache.pekko.util import java.lang.{ Double => JDouble, Float => JFloat } -import java.lang.reflect.{ Array => JArray } /** * Set of methods which allow easy implementation of hashCode. @@ -47,7 +46,7 @@ object HashCode { var result = seed if (value eq null) result = hash(result, 0) else if (!isArray(value)) result = hash(result, value.hashCode()) - else for (id <- 0 until JArray.getLength(value)) result = hash(result, JArray.get(value, id)) // is an array + else result = hashArray(result, value) result case unexpected => throw new IllegalArgumentException(s"Unexpected hash parameter: $unexpected") // will not happen, for exhaustiveness check @@ -61,5 +60,22 @@ object HashCode { private def firstTerm(seed: Int): Int = PRIME * seed private def isArray(anyRef: AnyRef): Boolean = anyRef.getClass.isArray + private def hashArray(seed: Int, value: AnyRef): Int = { + var result = seed + value match { + case array: Array[AnyRef] => array.foreach(element => result = hash(result, element)) + case array: Array[Boolean] => array.foreach(element => result = hash(result, element)) + case array: Array[Char] => array.foreach(element => result = hash(result, element)) + case array: Array[Short] => array.foreach(element => result = hash(result, element)) + case array: Array[Int] => array.foreach(element => result = hash(result, element)) + case array: Array[Long] => array.foreach(element => result = hash(result, element)) + case array: Array[Float] => array.foreach(element => result = hash(result, element)) + case array: Array[Double] => array.foreach(element => result = hash(result, element)) + case array: Array[Byte] => array.foreach(element => result = hash(result, element)) + case unexpected => + throw new IllegalArgumentException(s"Unexpected array hash parameter: $unexpected") + } + result + } private val PRIME = 37 } diff --git a/actor/src/main/scala/org/apache/pekko/util/LineNumbers.scala b/actor/src/main/scala/org/apache/pekko/util/LineNumbers.scala index 424941490c1..0022a96520f 100644 --- a/actor/src/main/scala/org/apache/pekko/util/LineNumbers.scala +++ b/actor/src/main/scala/org/apache/pekko/util/LineNumbers.scala @@ -14,7 +14,8 @@ package org.apache.pekko.util import java.io.{ DataInputStream, InputStream } -import java.lang.invoke.SerializedLambda +import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType, SerializedLambda } +import java.util.concurrent.ConcurrentHashMap import scala.annotation.switch import scala.util.control.NonFatal @@ -30,6 +31,10 @@ import scala.util.control.NonFatal */ object LineNumbers { + private val lookup = MethodHandles.lookup() + private val writeReplaceMethodType = MethodType.methodType(classOf[Object]) + private val writeReplaceHandleCache = new ConcurrentHashMap[Class[?], MethodHandle] + sealed trait Result case object NoSourceInfo extends Result final case class UnknownSourceFormat(explanation: String) extends Result @@ -207,9 +212,17 @@ object LineNumbers { private def getStreamForLambda(l: AnyRef): Option[(InputStream, Some[String])] = try { val c = l.getClass - val writeReplace = c.getDeclaredMethod("writeReplace") - writeReplace.setAccessible(true) - writeReplace.invoke(l) match { + val cached = writeReplaceHandleCache.get(c) + val writeReplaceHandle = + if (cached ne null) cached + else { + val handle = MethodHandles + .privateLookupIn(c, lookup) + .findVirtual(c, "writeReplace", writeReplaceMethodType) + val existing = writeReplaceHandleCache.putIfAbsent(c, handle) + if (existing eq null) handle else existing + } + writeReplaceHandle.invoke(l) match { case serialized: SerializedLambda => if (debug) println(s"LNB: found Lambda implemented in ${serialized.getImplClass}:${serialized.getImplMethodName}") diff --git a/actor/src/main/scala/org/apache/pekko/util/Reflect.scala b/actor/src/main/scala/org/apache/pekko/util/Reflect.scala index 45203e718bc..d0243c08468 100644 --- a/actor/src/main/scala/org/apache/pekko/util/Reflect.scala +++ b/actor/src/main/scala/org/apache/pekko/util/Reflect.scala @@ -12,14 +12,13 @@ */ package org.apache.pekko.util +import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType } import java.lang.StackWalker -import java.lang.reflect.Constructor -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type +import java.lang.reflect.{ Constructor, Modifier, ParameterizedType, Type } +import java.util.concurrent.ConcurrentHashMap import scala.annotation.tailrec import scala.collection.immutable -import scala.util.Try import scala.util.control.NonFatal import org.apache.pekko.annotation.InternalApi @@ -32,6 +31,11 @@ import org.apache.pekko.annotation.InternalApi */ @InternalApi private[pekko] object Reflect { + private val lookup = MethodHandles.lookup() + private val publicLookup = MethodHandles.publicLookup() + private val constructorHandles = new ConcurrentHashMap[ConstructorKey, MethodHandle] + + private final case class ConstructorKey(clazz: Class[?], argumentTypes: Vector[Class[?]]) /** * This optionally holds a function which looks N levels above itself @@ -59,13 +63,7 @@ private[pekko] object Reflect { * @return a new instance from the default constructor of the given class */ private[pekko] def instantiate[T](clazz: Class[T]): T = { - val ctor = clazz.getDeclaredConstructor() - try ctor.newInstance() - catch { - case _: IllegalAccessException => - ctor.setAccessible(true) - ctor.newInstance() - } + findConstructor(clazz, Nil).invoke().asInstanceOf[T] } /** @@ -80,9 +78,8 @@ private[pekko] object Reflect { * INTERNAL API * Invokes the constructor with the given arguments. */ - private[pekko] def instantiate[T](constructor: Constructor[T], args: immutable.Seq[Any]): T = { - constructor.setAccessible(true) - try constructor.newInstance(args.asInstanceOf[Seq[AnyRef]]: _*) + private[pekko] def instantiate[T](constructor: MethodHandle, args: immutable.Seq[Any]): T = { + try constructor.invokeWithArguments(args.asInstanceOf[Seq[AnyRef]]: _*).asInstanceOf[T] catch { case e: IllegalArgumentException => val argString = args.map(safeGetClass).mkString("[", ", ", "]") @@ -95,35 +92,46 @@ private[pekko] object Reflect { * Implements a primitive form of overload resolution a.k.a. finding the * right constructor. */ - private[pekko] def findConstructor[T](clazz: Class[T], args: immutable.Seq[Any]): Constructor[T] = { + private[pekko] def findConstructor[T](clazz: Class[T], args: immutable.Seq[Any]): MethodHandle = { def error(msg: String): Nothing = { val argClasses = args.map(safeGetClass).mkString(", ") throw new IllegalArgumentException(s"$msg found on $clazz for arguments [$argClasses]") } - val constructor: Constructor[T] = - if (args.isEmpty) Try { clazz.getDeclaredConstructor() }.getOrElse(null) - else { - val length = args.length - val candidates = - clazz.getDeclaredConstructors.asInstanceOf[Array[Constructor[T]]].iterator.filter { c => - val parameterTypes = c.getParameterTypes - parameterTypes.length == length && - (parameterTypes.iterator.zip(args.iterator).forall { - case (found, required) => - found.isInstance(required) || BoxedType(found).isInstance(required) || - (required == null && !found.isPrimitive) - }) - } - if (candidates.hasNext) { - val cstrtr = candidates.next() - if (candidates.hasNext) error("multiple matching constructors") - else cstrtr - } else null + val key = ConstructorKey(clazz, args.map(safeGetClass).toVector) + val cached = constructorHandles.get(key) + if (cached ne null) cached + else { + val selectedConstructorType = + findConstructorMethodTypeFromClassMetadata(clazz, args).getOrElse(error("no matching constructor")) + val handle = findConstructorHandle(clazz, selectedConstructorType) + val existing = constructorHandles.putIfAbsent(key, handle) + if (existing eq null) handle else existing + } + } + + private def findConstructorMethodTypeFromClassMetadata[T](clazz: Class[T], args: immutable.Seq[Any]) + : Option[MethodType] = { + def matches(parameterTypes: Array[Class[?]]): Boolean = + parameterTypes.length == args.length && + parameterTypes.iterator.zip(args.iterator).forall { + case (found, required) => + found.isInstance(required) || BoxedType(found).isInstance(required) || + (required == null && !found.isPrimitive) } - if (constructor eq null) error("no matching constructor") - else constructor + val candidates = clazz.getDeclaredConstructors.iterator + .map(constructorMethodType) + .filter(methodType => matches(methodType.parameterArray())) + .toList + candidates match { + case single :: Nil => Some(single) + case Nil => None + case _ => + val argClasses = args.map(safeGetClass).mkString(", ") + throw new IllegalArgumentException( + s"multiple matching constructors found on $clazz for arguments [$argClasses]") + } } private def safeGetClass(a: Any): Class[?] = @@ -153,6 +161,31 @@ private[pekko] object Reflect { rec(root) } + private def constructorMethodType(constructor: Constructor[?]): MethodType = + MethodType.methodType(Void.TYPE, constructor.getParameterTypes) + + private def findConstructorHandle[T](clazz: Class[T], methodType: MethodType): MethodHandle = + try publicLookup.findConstructor(clazz, methodType) + catch { + case _: IllegalAccessException | _: NoSuchMethodException => + MethodHandles.privateLookupIn(clazz, lookup).findConstructor(clazz, methodType) + } + + private[pekko] def findStaticNoArgMethod(clazz: Class[?], methodName: String): MethodHandle = + clazz.getDeclaredMethods.iterator + .filter(method => + method.getName == methodName && Modifier.isStatic(method.getModifiers) && method.getParameterCount == 0) + .toList match { + case method :: Nil => + MethodHandles + .privateLookupIn(clazz, lookup) + .findStatic(clazz, methodName, MethodType.methodType(method.getReturnType)) + case Nil => + throw new NoSuchMethodException(s"${clazz.getName}.$methodName()") + case _ => + throw new IllegalArgumentException(s"multiple static no-arg methods named [$methodName] found on $clazz") + } + /** * INTERNAL API */ diff --git a/cluster-sharding-typed/src/test/scala/org/apache/pekko/cluster/sharding/FlightRecording.scala b/cluster-sharding-typed/src/test/scala/org/apache/pekko/cluster/sharding/FlightRecording.scala index d1628667c03..a46ee637a9a 100644 --- a/cluster-sharding-typed/src/test/scala/org/apache/pekko/cluster/sharding/FlightRecording.scala +++ b/cluster-sharding-typed/src/test/scala/org/apache/pekko/cluster/sharding/FlightRecording.scala @@ -12,6 +12,7 @@ */ package org.apache.pekko.cluster.sharding +import java.lang.invoke.{ MethodHandles, MethodType } import java.nio.file.Files import java.nio.file.Path @@ -25,19 +26,26 @@ import org.apache.pekko.actor.{ ActorSystem, ExtendedActorSystem } */ class FlightRecording(system: ActorSystem) { + private val lookup = MethodHandles.publicLookup() + private val noArgVoidMethodType = MethodType.methodType(Void.TYPE) + private val dumpMethodType = MethodType.methodType(Void.TYPE, classOf[Path]) + private val dynamic = system.asInstanceOf[ExtendedActorSystem].dynamicAccess private val recording = dynamic.createInstanceFor[AnyRef]("jdk.jfr.Recording", Nil).toOption private val clazz = recording.map(_.getClass) - private val startMethod = clazz.map(_.getDeclaredMethod("start")) - private val stopMethod = clazz.map(_.getDeclaredMethod("stop")) - private val dumpMethod = clazz.map(_.getDeclaredMethod("dump", classOf[Path])) + private val startMethod = + clazz.map(lookup.findVirtual(_, "start", noArgVoidMethodType)) + private val stopMethod = + clazz.map(lookup.findVirtual(_, "stop", noArgVoidMethodType)) + private val dumpMethod = + clazz.map(lookup.findVirtual(_, "dump", dumpMethodType)) def start() = { for { r <- recording - m <- startMethod - } yield m.invoke(r) + handle <- startMethod + } yield handle.invoke(r) } def endAndDump(location: Path) = { diff --git a/persistence/src/test/scala/org/apache/pekko/persistence/state/exception/DurableStateExceptionsSpec.scala b/persistence/src/test/scala/org/apache/pekko/persistence/state/exception/DurableStateExceptionsSpec.scala index 1a37d0c4744..66fadb3bcfa 100644 --- a/persistence/src/test/scala/org/apache/pekko/persistence/state/exception/DurableStateExceptionsSpec.scala +++ b/persistence/src/test/scala/org/apache/pekko/persistence/state/exception/DurableStateExceptionsSpec.scala @@ -39,8 +39,8 @@ class DurableStateExceptionsSpec extends AnyWordSpecLike "DurableStateException support" must { "allow creating DeleteRevisionException using MethodHandle" in { - val exceptionClassOpt: Option[Class[?]] = Try(Class.forName( - "org.apache.pekko.persistence.state.exception.DeleteRevisionException")).toOption + val exceptionClassOpt: Option[Class[?]] = + Try(Class.forName("org.apache.pekko.persistence.state.exception.DeleteRevisionException")).toOption exceptionClassOpt should not be empty val constructorOpt = exceptionClassOpt.map { clz => val mt = MethodType.methodType(classOf[Unit], classOf[String]) diff --git a/remote/src/main/scala/org/apache/pekko/remote/serialization/ProtobufSerializer.scala b/remote/src/main/scala/org/apache/pekko/remote/serialization/ProtobufSerializer.scala index c11a4d43f01..14cfaa679b0 100644 --- a/remote/src/main/scala/org/apache/pekko/remote/serialization/ProtobufSerializer.scala +++ b/remote/src/main/scala/org/apache/pekko/remote/serialization/ProtobufSerializer.scala @@ -13,10 +13,9 @@ package org.apache.pekko.remote.serialization -import java.lang.reflect.Method +import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType } import java.util.concurrent.atomic.AtomicReference -import scala.annotation.tailrec import scala.util.control.NonFatal import org.apache.pekko @@ -28,7 +27,8 @@ import pekko.serialization.{ BaseSerializer, Serialization } import pekko.serialization.SerializationExtension object ProtobufSerializer { - private val ARRAY_OF_BYTE_ARRAY = Array[Class[?]](classOf[Array[Byte]]) + private val publicLookup = MethodHandles.publicLookup() + private val toByteArrayMethodType = MethodType.methodType(classOf[Array[Byte]]) /** * Helper to serialize a [[pekko.actor.ActorRef]] to Pekko's @@ -56,8 +56,8 @@ object ProtobufSerializer { */ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer { - private val parsingMethodBindingRef = new AtomicReference[Map[Class[?], Method]](Map.empty) - private val toByteArrayMethodBindingRef = new AtomicReference[Map[Class[?], Method]](Map.empty) + private val parsingMethodBindingRef = new AtomicReference[Map[Class[?], MethodHandle]](Map.empty) + private val toByteArrayMethodBindingRef = new AtomicReference[Map[Class[?], MethodHandle]](Map.empty) private val allowedClassNames: Set[String] = { import scala.jdk.CollectionConverters._ @@ -74,25 +74,26 @@ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer override def fromBinary(bytes: Array[Byte], manifest: Option[Class[?]]): AnyRef = { manifest match { case Some(clazz) => - @tailrec - def parsingMethod(method: Method = null): Method = { + def parsingMethod(): MethodHandle = { val parsingMethodBinding = parsingMethodBindingRef.get() parsingMethodBinding.get(clazz) match { - case Some(cachedParsingMethod) => cachedParsingMethod + case Some(cachedParsingHandle) => cachedParsingHandle case None => checkAllowedClass(clazz) - val unCachedParsingMethod = - if (method eq null) clazz.getDeclaredMethod("parseFrom", ProtobufSerializer.ARRAY_OF_BYTE_ARRAY: _*) - else method + val unCachedHandle = + ProtobufSerializer.publicLookup.findStatic( + clazz, + "parseFrom", + MethodType.methodType(clazz, classOf[Array[Byte]])) if (parsingMethodBindingRef.compareAndSet( parsingMethodBinding, - parsingMethodBinding.updated(clazz, unCachedParsingMethod))) - unCachedParsingMethod + parsingMethodBinding.updated(clazz, unCachedHandle))) + unCachedHandle else - parsingMethod(unCachedParsingMethod) + parsingMethod() } } - parsingMethod().invoke(null, bytes) + parsingMethod().invoke(bytes).asInstanceOf[AnyRef] case None => throw new IllegalArgumentException("Need a protobuf message class to be able to serialize bytes using protobuf") @@ -101,21 +102,19 @@ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer override def toBinary(obj: AnyRef): Array[Byte] = { val clazz = obj.getClass - @tailrec - def toByteArrayMethod(method: Method = null): Method = { + def toByteArrayMethod(): MethodHandle = { val toByteArrayMethodBinding = toByteArrayMethodBindingRef.get() toByteArrayMethodBinding.get(clazz) match { - case Some(cachedtoByteArrayMethod) => cachedtoByteArrayMethod - case None => - val unCachedtoByteArrayMethod = - if (method eq null) clazz.getMethod("toByteArray") - else method + case Some(cachedHandle) => cachedHandle + case None => + val unCachedHandle = + ProtobufSerializer.publicLookup.findVirtual(clazz, "toByteArray", ProtobufSerializer.toByteArrayMethodType) if (toByteArrayMethodBindingRef.compareAndSet( toByteArrayMethodBinding, - toByteArrayMethodBinding.updated(clazz, unCachedtoByteArrayMethod))) - unCachedtoByteArrayMethod + toByteArrayMethodBinding.updated(clazz, unCachedHandle))) + unCachedHandle else - toByteArrayMethod(unCachedtoByteArrayMethod) + toByteArrayMethod() } } toByteArrayMethod().invoke(obj).asInstanceOf[Array[Byte]] diff --git a/testkit/src/test/scala/org/apache/pekko/testkit/metrics/FileDescriptorMetricSet.scala b/testkit/src/test/scala/org/apache/pekko/testkit/metrics/FileDescriptorMetricSet.scala index 0cb8f9179db..9b5bf26a3a8 100644 --- a/testkit/src/test/scala/org/apache/pekko/testkit/metrics/FileDescriptorMetricSet.scala +++ b/testkit/src/test/scala/org/apache/pekko/testkit/metrics/FileDescriptorMetricSet.scala @@ -13,8 +13,10 @@ package org.apache.pekko.testkit.metrics +import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType } import java.lang.management.{ ManagementFactory, OperatingSystemMXBean } import java.util +import java.util.concurrent.ConcurrentHashMap import scala.jdk.CollectionConverters._ @@ -38,8 +40,28 @@ private[pekko] class FileDescriptorMetricSet(os: OperatingSystemMXBean = Managem } private def invoke(name: String): Long = { - val method = os.getClass.getDeclaredMethod(name) - method.setAccessible(true) - method.invoke(os).asInstanceOf[Long] + FileDescriptorMetricSet.methodHandle(os.getClass, name).invoke(os).asInstanceOf[Long] + } +} + +private object FileDescriptorMetricSet { + private final case class MethodKey(clazz: Class[?], name: String) + + private val lookup = MethodHandles.lookup() + private val longMethodType = MethodType.methodType(classOf[Long]) + private val handles = new ConcurrentHashMap[MethodKey, MethodHandle] + + private def methodHandle(clazz: Class[?], name: String): MethodHandle = { + val key = MethodKey(clazz, name) + val cached = handles.get(key) + if (cached ne null) cached + else { + val handle = + MethodHandles + .privateLookupIn(clazz, lookup) + .findVirtual(clazz, name, longMethodType) + val existing = handles.putIfAbsent(key, handle) + if (existing eq null) handle else existing + } } }