Hello,
it seems like Method.getParameterTypes() is often misused in JDK (and beyond):
array returned from the method is used only to acquire number of method params
by retrieving array.length.
The problem here is that Method.getPatameterTypes() clones underlying array and
returns the copy.
Instead we can use Method.getParameterCount() which doesn't allocate any
additional memory but returns directly the length of underlying array.
To measure probable performance difference I've created a benchmark for the
most simple case when tested method has no parameters:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MethodToStringBenchmark {
private Method method;
@Setup
public void setup() throws Exception { method =
getClass().getMethod("toString"); }
@Benchmark
public int getParameterCount() { return method.getParameterCount(); }
@Benchmark
public int getParameterTypes() { return method.getParameterTypes().length; }
}
on my i7-7700 with JDK 11 it produces these results:
Benchmark Mode
Cnt Score Error Units
MethodToStringBenchmark.getParameterCount avgt
25 2,528 ± 0,085 ns/op
MethodToStringBenchmark.getParameterCount:·gc.alloc.rate avgt
25 ≈ 10⁻⁴ MB/sec
MethodToStringBenchmark.getParameterCount:·gc.alloc.rate.norm avgt
25 ≈ 10⁻⁷ B/op
MethodToStringBenchmark.getParameterCount:·gc.count avgt
25 ≈ 0 counts
MethodToStringBenchmark.getParameterTypes avgt
25 7,299 ± 0,410 ns/op
MethodToStringBenchmark.getParameterTypes:·gc.alloc.rate avgt
25 1999,454 ± 89,929 MB/sec
MethodToStringBenchmark.getParameterTypes:·gc.alloc.rate.norm avgt
25 16,000 ± 0,001 B/op
MethodToStringBenchmark.getParameterTypes:·gc.churn.G1_Eden_Space avgt
25 2003,360 ± 91,537 MB/sec
MethodToStringBenchmark.getParameterTypes:·gc.churn.G1_Eden_Space.norm avgt
25 16,030 ± 0,045 B/op
MethodToStringBenchmark.getParameterTypes:·gc.churn.G1_Old_Gen avgt
25 0,004 ± 0,001 MB/sec
MethodToStringBenchmark.getParameterTypes:·gc.churn.G1_Old_Gen.norm avgt
25 ≈ 10⁻⁵ B/op
MethodToStringBenchmark.getParameterTypes:·gc.count avgt
25 2380,000 counts
MethodToStringBenchmark.getParameterTypes:·gc.time avgt
25 1325,000 ms
I've prepared a small patch to replace usage of getParameterTypes() with
getParameterCount() where appropriate in java.base module.
The patch is attached.
Regards,
Sergey Tsypanov
diff --git a/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java b/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java
--- a/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java
+++ b/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java
@@ -281,13 +281,13 @@
switch (m.getName()) {
case "toString":
return (m.getReturnType() == String.class
- && m.getParameterTypes().length == 0);
+ && m.getParameterCount() == 0);
case "hashCode":
return (m.getReturnType() == int.class
- && m.getParameterTypes().length == 0);
+ && m.getParameterCount() == 0);
case "equals":
return (m.getReturnType() == boolean.class
- && m.getParameterTypes().length == 1
+ && m.getParameterCount() == 1
&& m.getParameterTypes()[0] == Object.class);
}
return false;
diff --git a/src/java.base/share/classes/java/lang/reflect/Executable.java b/src/java.base/share/classes/java/lang/reflect/Executable.java
--- a/src/java.base/share/classes/java/lang/reflect/Executable.java
+++ b/src/java.base/share/classes/java/lang/reflect/Executable.java
@@ -378,7 +378,7 @@
private void verifyParameters(final Parameter[] parameters) {
final int mask = Modifier.FINAL | Modifier.SYNTHETIC | Modifier.MANDATED;
- if (getParameterTypes().length != parameters.length)
+ if (getParameterCount() != parameters.length)
throw new MalformedParametersException("Wrong number of parameters in MethodParameters attribute");
for (Parameter parameter : parameters) {
diff --git a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java
--- a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java
+++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java
@@ -593,10 +593,10 @@
Constructor<?> noArgCtor = null;
// public ctors only
for (Constructor<?> c : ex.getClass().getConstructors()) {
- Class<?>[] ps = c.getParameterTypes();
- if (ps.length == 0)
+ int parameterCount = c.getParameterCount();
+ if (parameterCount == 0)
noArgCtor = c;
- else if (ps.length == 1 && ps[0] == Throwable.class)
+ else if (parameterCount == 1 && c.getParameterTypes()[0] == Throwable.class)
return (Throwable)c.newInstance(ex);
}
if (noArgCtor != null) {
diff --git a/src/java.base/share/classes/sun/reflect/annotation/AnnotationType.java b/src/java.base/share/classes/sun/reflect/annotation/AnnotationType.java
--- a/src/java.base/share/classes/sun/reflect/annotation/AnnotationType.java
+++ b/src/java.base/share/classes/sun/reflect/annotation/AnnotationType.java
@@ -121,7 +121,7 @@
if (Modifier.isPublic(method.getModifiers()) &&
Modifier.isAbstract(method.getModifiers()) &&
!method.isSynthetic()) {
- if (method.getParameterTypes().length != 0) {
+ if (method.getParameterCount() != 0) {
throw new IllegalArgumentException(method + " has params");
}
String name = method.getName();