Можно показать (см. ниже переписанный тестовый пример), что дескрипторы методов для 254-ричных методов требуют меньше памяти в Java 17, чем в Java 11, при компиляции и запуске ее инструментами.
Поскольку в сводках релизов для версий Java с 11 по 17 не рекламируются функции, связанные с дескриптором метода или отражением, мне любопытно: какие изменения способствовали меньшему потреблению памяти?
Это переписанный тестовый пример ArityLimits.java
:
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
class ArityLimits
{
public static void main(String[] args) throws Throwable
{
// Pick an implementation with the 1st argument, e.g. core.
final Invocable invocable = (args.length > 0
&& args[0].equalsIgnoreCase("core"))
? new CoreInvoker(ArityLimits::new)
: new HandleInvoker(ArityLimits::new,
MethodHandles.privateLookupIn(
ArityLimits.class,
MethodHandles.lookup()));
// Pick which methods to call with the 2nd argument, e.g.
// $(( 1|2|4|8 )).
final int agenda = (args.length > 1)
? 0xf & Integer.parseInt(args[1])
: (1 | 2 | 4 | 8);
final Map<String, List<?>> arguments = new HashMap<>(8);
if ((agenda & 1) != 0)
arguments.put("passCrunchLongsFix128",
Collections.nCopies(128 - 2 /* (handle, this) */, 0L));
if ((agenda & 2) != 0)
arguments.put("passClassCrunchLongsFix128",
Collections.nCopies(128 - 1 /* (handle) */, 0L));
if ((agenda & 4) != 0)
arguments.put("passCrunchIntsFix255",
Collections.nCopies(255 - 2 /* (handle, this) */, 0));
if ((agenda & 8) != 0)
arguments.put("passClassCrunchIntsFix255",
Collections.nCopies(255 - 1 /* (handle) */, 0));
final Consumer<Method> announcer = ("Linux".equalsIgnoreCase(
System.getProperty("os.name", "")))
? method -> System.err.format("\033[7m%s\033[0m%n",
method.getName())
: method -> System.err.println(method.getName());
for (Method method : ArityLimits.class.getDeclaredMethods()) {
final List<?> methodArgs = arguments.get(
method.getName());
if (methodArgs == null)
continue;
invocable.invoke(method, methodArgs);
announcer.accept(method);
}
}
private interface Invocable
{
void invoke(Method method, List<?> args) throws Throwable;
static boolean warnTooManyArgs(Method method, List<?> args)
{
if (method.isVarArgs() || args.size() < 256)
return false;
System.err.println(method.getName()
.concat(": too many arguments"));
return true;
}
}
private static final class CoreInvoker implements Invocable
{
private final Supplier<?> instanceer;
CoreInvoker(Supplier<?> instanceer)
{
this.instanceer = instanceer;
}
public void invoke(Method method, List<?> args)
throws ReflectiveOperationException
{
if (Invocable.warnTooManyArgs(method, args))
return;
method.invoke((Modifier.isStatic(method.getModifiers()))
? null
: instanceer.get(),
args.toArray());
}
}
private static final class HandleInvoker implements Invocable
{
private final Supplier<?> instanceer;
private final Lookup lookup;
HandleInvoker(Supplier<?> instanceer, Lookup lookup)
{
this.instanceer = instanceer;
this.lookup = lookup;
}
public void invoke(Method method, List<?> args) throws Throwable
{
// Here a handle shall check its parameter constraints.
final MethodHandle mh1 = lookup.unreflect(method);
if (Invocable.warnTooManyArgs(method, args))
return;
final MethodHandle mh2 = (Modifier.isStatic(
method.getModifiers()))
? mh1
: mh1.bindTo(instanceer.get());
mh2.invokeWithArguments(args);
}
}
void passCrunchLongsFix128(/* MethodHandle mh, ArityLimits al, */
long _001, long _002, long _003, long _004, long _005,
long _006, long _007, long _008, long _009, long _010,
long _011, long _012, long _013, long _014, long _015,
long _016, long _017, long _018, long _019, long _020,
long _021, long _022, long _023, long _024, long _025,
long _026, long _027, long _028, long _029, long _030,
long _031, long _032, long _033, long _034, long _035,
long _036, long _037, long _038, long _039, long _040,
long _041, long _042, long _043, long _044, long _045,
long _046, long _047, long _048, long _049, long _050,
long _051, long _052, long _053, long _054, long _055,
long _056, long _057, long _058, long _059, long _060,
long _061, long _062, long _063, long _064, long _065,
long _066, long _067, long _068, long _069, long _070,
long _071, long _072, long _073, long _074, long _075,
long _076, long _077, long _078, long _079, long _080,
long _081, long _082, long _083, long _084, long _085,
long _086, long _087, long _088, long _089, long _090,
long _091, long _092, long _093, long _094, long _095,
long _096, long _097, long _098, long _099, long _100,
long _101, long _102, long _103, long _104, long _105,
long _106, long _107, long _108, long _109, long _110,
long _111, long _112, long _113, long _114, long _115,
long _116, long _117, long _118, long _119, long _120,
long _121, long _122, long _123, long _124, long _125,
long _126) { }
static void passClassCrunchLongsFix128(/* MethodHandle mh, */
long _001, long _002, long _003, long _004, long _005,
long _006, long _007, long _008, long _009, long _010,
long _011, long _012, long _013, long _014, long _015,
long _016, long _017, long _018, long _019, long _020,
long _021, long _022, long _023, long _024, long _025,
long _026, long _027, long _028, long _029, long _030,
long _031, long _032, long _033, long _034, long _035,
long _036, long _037, long _038, long _039, long _040,
long _041, long _042, long _043, long _044, long _045,
long _046, long _047, long _048, long _049, long _050,
long _051, long _052, long _053, long _054, long _055,
long _056, long _057, long _058, long _059, long _060,
long _061, long _062, long _063, long _064, long _065,
long _066, long _067, long _068, long _069, long _070,
long _071, long _072, long _073, long _074, long _075,
long _076, long _077, long _078, long _079, long _080,
long _081, long _082, long _083, long _084, long _085,
long _086, long _087, long _088, long _089, long _090,
long _091, long _092, long _093, long _094, long _095,
long _096, long _097, long _098, long _099, long _100,
long _101, long _102, long _103, long _104, long _105,
long _106, long _107, long _108, long _109, long _110,
long _111, long _112, long _113, long _114, long _115,
long _116, long _117, long _118, long _119, long _120,
long _121, long _122, long _123, long _124, long _125,
long _126, long _127) { }
void passCrunchIntsFix255(/* MethodHandle mh, ArityLimits al, */
int _001, int _002, int _003, int _004, int _005,
int _006, int _007, int _008, int _009, int _010,
int _011, int _012, int _013, int _014, int _015,
int _016, int _017, int _018, int _019, int _020,
int _021, int _022, int _023, int _024, int _025,
int _026, int _027, int _028, int _029, int _030,
int _031, int _032, int _033, int _034, int _035,
int _036, int _037, int _038, int _039, int _040,
int _041, int _042, int _043, int _044, int _045,
int _046, int _047, int _048, int _049, int _050,
int _051, int _052, int _053, int _054, int _055,
int _056, int _057, int _058, int _059, int _060,
int _061, int _062, int _063, int _064, int _065,
int _066, int _067, int _068, int _069, int _070,
int _071, int _072, int _073, int _074, int _075,
int _076, int _077, int _078, int _079, int _080,
int _081, int _082, int _083, int _084, int _085,
int _086, int _087, int _088, int _089, int _090,
int _091, int _092, int _093, int _094, int _095,
int _096, int _097, int _098, int _099, int _100,
int _101, int _102, int _103, int _104, int _105,
int _106, int _107, int _108, int _109, int _110,
int _111, int _112, int _113, int _114, int _115,
int _116, int _117, int _118, int _119, int _120,
int _121, int _122, int _123, int _124, int _125,
int _126, int _127, int _128, int _129, int _130,
int _131, int _132, int _133, int _134, int _135,
int _136, int _137, int _138, int _139, int _140,
int _141, int _142, int _143, int _144, int _145,
int _146, int _147, int _148, int _149, int _150,
int _151, int _152, int _153, int _154, int _155,
int _156, int _157, int _158, int _159, int _160,
int _161, int _162, int _163, int _164, int _165,
int _166, int _167, int _168, int _169, int _170,
int _171, int _172, int _173, int _174, int _175,
int _176, int _177, int _178, int _179, int _180,
int _181, int _182, int _183, int _184, int _185,
int _186, int _187, int _188, int _189, int _190,
int _191, int _192, int _193, int _194, int _195,
int _196, int _197, int _198, int _199, int _200,
int _201, int _202, int _203, int _204, int _205,
int _206, int _207, int _208, int _209, int _210,
int _211, int _212, int _213, int _214, int _215,
int _216, int _217, int _218, int _219, int _220,
int _221, int _222, int _223, int _224, int _225,
int _226, int _227, int _228, int _229, int _230,
int _231, int _232, int _233, int _234, int _235,
int _236, int _237, int _238, int _239, int _240,
int _241, int _242, int _243, int _244, int _245,
int _246, int _247, int _248, int _249, int _250,
int _251, int _252, int _253) { }
static void passClassCrunchIntsFix255(/* MethodHandle mh, */
int _001, int _002, int _003, int _004, int _005,
int _006, int _007, int _008, int _009, int _010,
int _011, int _012, int _013, int _014, int _015,
int _016, int _017, int _018, int _019, int _020,
int _021, int _022, int _023, int _024, int _025,
int _026, int _027, int _028, int _029, int _030,
int _031, int _032, int _033, int _034, int _035,
int _036, int _037, int _038, int _039, int _040,
int _041, int _042, int _043, int _044, int _045,
int _046, int _047, int _048, int _049, int _050,
int _051, int _052, int _053, int _054, int _055,
int _056, int _057, int _058, int _059, int _060,
int _061, int _062, int _063, int _064, int _065,
int _066, int _067, int _068, int _069, int _070,
int _071, int _072, int _073, int _074, int _075,
int _076, int _077, int _078, int _079, int _080,
int _081, int _082, int _083, int _084, int _085,
int _086, int _087, int _088, int _089, int _090,
int _091, int _092, int _093, int _094, int _095,
int _096, int _097, int _098, int _099, int _100,
int _101, int _102, int _103, int _104, int _105,
int _106, int _107, int _108, int _109, int _110,
int _111, int _112, int _113, int _114, int _115,
int _116, int _117, int _118, int _119, int _120,
int _121, int _122, int _123, int _124, int _125,
int _126, int _127, int _128, int _129, int _130,
int _131, int _132, int _133, int _134, int _135,
int _136, int _137, int _138, int _139, int _140,
int _141, int _142, int _143, int _144, int _145,
int _146, int _147, int _148, int _149, int _150,
int _151, int _152, int _153, int _154, int _155,
int _156, int _157, int _158, int _159, int _160,
int _161, int _162, int _163, int _164, int _165,
int _166, int _167, int _168, int _169, int _170,
int _171, int _172, int _173, int _174, int _175,
int _176, int _177, int _178, int _179, int _180,
int _181, int _182, int _183, int _184, int _185,
int _186, int _187, int _188, int _189, int _190,
int _191, int _192, int _193, int _194, int _195,
int _196, int _197, int _198, int _199, int _200,
int _201, int _202, int _203, int _204, int _205,
int _206, int _207, int _208, int _209, int _210,
int _211, int _212, int _213, int _214, int _215,
int _216, int _217, int _218, int _219, int _220,
int _221, int _222, int _223, int _224, int _225,
int _226, int _227, int _228, int _229, int _230,
int _231, int _232, int _233, int _234, int _235,
int _236, int _237, int _238, int _239, int _240,
int _241, int _242, int _243, int _244, int _245,
int _246, int _247, int _248, int _249, int _250,
int _251, int _252, int _253, int _254) { }
}
Это файл аргументов args
:
-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC
-XX:+AlwaysPreTouch
-Xms136m
-Xmx136m
-Xlog:heap*=info,gc=info
Сохраните файлы, например, в каталоге /tmp
и запустите контейнер Docker версии Java 11:
docker run --entrypoint /bin/sh --interactive \
--rm --tty --volume = "/tmp:/tmp" \
--workdir = "/tmp" eclipse-temurin:11-jdk-alpine # It may pull in ~200 MiB.
javac -Xdiags:verbose -Xlint ArityLimits.java
java @args ArityLimits handle $(( 1 | 2 | 4 | 8 )) # 91/136 MiB of heap used
java @args ArityLimits handle 8 # 36/136 (see title)
java @args ArityLimits core $(( 1 | 2 | 4 | 8 )) # 1/136
java @args ArityLimits core 8 # 1/136
rm *.class
exit
Теперь запустите контейнер Docker версии Java 17:
docker run --entrypoint /bin/sh --interactive \
--rm --tty --volume = "/tmp:/tmp" \
--workdir = "/tmp" eclipse-temurin:17-jdk-alpine # It may pull in ~200 MiB.
javac -Xdiags:verbose -Xlint ArityLimits.java
java @args ArityLimits handle $(( 1 | 2 | 4 | 8 )) # 5/136 MiB of heap used
java @args ArityLimits handle 8 # 3/136 (see title)
java @args ArityLimits core $(( 1 | 2 | 4 | 8 )) # 1/136
java @args ArityLimits core 8 # 1/136
rm *.class
exit
Основная причина высокого потребления памяти — это, конечно же, вариант -XX:+UseEpsilonGC
, так как отключение сборщика мусора подразумевает наличие всех временных объектов в памяти, а не их освобождение.
Вы можете просто сделать дамп кучи в конце программы, чтобы увидеть доминирующие объекты и использовать инструмент профилирования для отслеживания распределения. Я нашел довольно много StringBuilder
экземпляров в Java 11 (170 000) и, в свою очередь, byte[]
массивов по сравнению с Java 17 (4 000 StringBuilder
экземпляров). Одним из источников выделений, обнаруженных async-profiler, был jdk.internal.org.objectweb.asm.Type
класс библиотеки ASM, встроенный в Java.
Java 11 использует версию 6 этой библиотеки, тогда как Java 17 использует версию 8. Между этими версиями есть изменение, задокументированное как
небольшие оптимизации в asm.Type
Одним из часто вызываемых методов этого класса является
public String getDescriptor() { StringBuilder buf = new StringBuilder(); getDescriptor(buf); return buf.toString(); }
нам не нужно копаться в фактическом методе реализации, чтобы увидеть, что StringBuilder
выделяется безоговорочно. Напротив, более новая реализация выглядит как
public String getDescriptor() { if (sort == OBJECT) { return valueBuffer.substring(valueBegin - 1, valueEnd + 1); } else if (sort == INTERNAL) { return 'L' + valueBuffer.substring(valueBegin, valueEnd) + ';'; } else { return valueBuffer.substring(valueBegin, valueEnd); } }
который будет выделять StringBuilder
только в некоторых случаях. Это особенно относится к вашему случаю с большим количеством параметров int
или long
, которые окажутся в последней ветви, просто вызывая substring
в сигнатуре метода, чтобы создать строку, равную "I"
или "J"
. Напротив, старый код выделял экземпляр StringBuilder
и массив byte[]
длиной 16 (емкость по умолчанию) в дополнение к строке результата для каждого вызова.
Может быть больше сайтов распределения, различающихся между версиями Java, но я думаю, что это дает вам необходимую информацию о том, где искать и как искать.
Да, такие тестовые случаи разумны, но вы все равно должны быть готовы к большому потреблению памяти. 30 МБ — это не так уж и много, а максимальное количество параметров — крайний случай. Тем не менее, мне было интересно покопаться в этом вопросе.
Выполнение тестовых случаев в короткоживущем разветвленном процессе кажется разумной работой для GC без восстановления памяти, то есть Epsilon.