Что?! 3 против 36 МБ для неоперативного 254-арного вызова метода через дескриптор метода

Можно показать (см. ниже переписанный тестовый пример), что дескрипторы методов для 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
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
0
78
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основная причина высокого потребления памяти — это, конечно же, вариант -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, но я думаю, что это дает вам необходимую информацию о том, где искать и как искать.

Выполнение тестовых случаев в короткоживущем разветвленном процессе кажется разумной работой для GC без восстановления памяти, то есть Epsilon.

Aliaksei Budavei 11.02.2023 17:27

Да, такие тестовые случаи разумны, но вы все равно должны быть готовы к большому потреблению памяти. 30 МБ — это не так уж и много, а максимальное количество параметров — крайний случай. Тем не менее, мне было интересно покопаться в этом вопросе.

Holger 13.02.2023 10:37

Другие вопросы по теме