Применяйте ценные уроки внутренней оптимизации Log4j2 для улучшения собственной кодовой базы.

Любой, кто работает с Java, видел несколько кодовых баз, использующих Log4j2 для ведения журнала. Но задумывались ли вы когда-нибудь, чем Log4j2 выделяется среди своих конкурентов? Шум вокруг города предполагает, что замечательная скорость Log4j2 и эффективность использования памяти являются ключевыми факторами.

В этой статье мы углубимся во внутреннюю работу Log4j2, раскроем его мощные методы, обеспечивающие молниеносную производительность при минимальном потреблении памяти. Понимая и применяя эти методы в своей собственной кодовой базе, вы также можете повысить производительность своего приложения.

Итак, без промедления, давайте отправимся в это проницательное путешествие.

Повторное использование внутренних объектов

В стандартных веб-приложениях на основе Java во время обработки запроса создается множество внутренних объектов. По мере того, как сервер обрабатывает все большее количество запросов, объем создаваемых объектов соответственно увеличивается, что приводит к бесполезным затратам времени на выделение пространства и сборку мусора. Ситуация ухудшается, когда создание объектов оказывается дорогостоящим и ресурсоемким. Эти проблемы значительно влияют на производительность и общую эффективность приложения.

К счастью, Log4j2 распознал эту проблему и эффективно решил ее, внедрив разумный подход к повторному использованию внутренних объектов, когда это возможно. В следующих разделах мы рассмотрим конкретные классы в Log4j2, которые иллюстрируют эти инновационные методы, проливая свет на то, как Log4j2 решает проблему напрямую и устанавливает эталон для эффективной обработки объектов.

Давайте подробнее рассмотрим ключевой класс в кодовой базе Log4j2 под названием ReusableMessageFactory. Упрощенная для ясности структура класса выглядит следующим образом:

public final class ReusableMessageFactory implements MessageFactory2, Serializable {

    private static ThreadLocal<ReusableSimpleMessage> threadLocalSimpleMessage = new ThreadLocal();

    public ReusableMessageFactory() {
    }
    
    public Message newMessage(final CharSequence charSequence) {
        ReusableSimpleMessage result = getSimple();
        result.set(charSequence);
        return result;
    }

    private static ReusableSimpleMessage getSimple() {
        ReusableSimpleMessage result = (ReusableSimpleMessage)threadLocalSimpleMessage.get();
        if (result == null) {
            result = new ReusableSimpleMessage();
            threadLocalSimpleMessage.set(result);
        }

        return result;
    }

    public static void release(final Message message) {
        if (message instanceof Clearable) {
            ((Clearable)message).clear();
        }

    }
}

Предоставленный фрагмент кода демонстрирует ReusableMessageFactory, ключевой компонент, отвечающий за создание объектов различных типов повторно используемых классов. Хотя эти классы существуют в нескольких вариантах, мы сосредоточимся на самой простой реализации, которая называется ReusableSimpleMessage. Ниже вы найдете упрощенную версию этого класса:

public class ReusableSimpleMessage implements ReusableMessage, CharSequence, ParameterVisitable, Clearable {

    private CharSequence charSequence;

    public ReusableSimpleMessage() {
    }

    public void set(final CharSequence charSequence) {
        this.charSequence = charSequence;
    }

    public void clear() {
        this.charSequence = null;
    }
}

Обратите внимание, что этот класс реализует интерфейс с именем Clearable, который является общим для всех повторно используемых классов. Это пригодится в более поздней части.

Теперь давайте сосредоточимся на трех важных аспектах фрагментов кода, которыми я поделился.

Первым заметным элементом является использование ThreadLocal для кэширования объекта типа ReusableSimpleMessage для каждого потока. Этот подход оптимизирует производительность за счет исключения создания избыточных объектов и повторного использования существующих экземпляров.

private static ThreadLocal<ReusableSimpleMessage> threadLocalSimpleMessage = new ThreadLocal();

Далее идет метод newMessage(final CharSequence charSequence). Если экземпляр типа ReusableSimpleMessage еще не существует в кеше ThreadLocal, новый объект создается с использованием CharSequence, предоставленного клиентом, и сохраняется. Однако, если кэшированный объект доступен, он извлекается и возвращается.

public Message newMessage(final CharSequence charSequence) {
        ReusableSimpleMessage result = getSimple();
        result.set(charSequence);
        return result;
}

private static ReusableSimpleMessage getSimple() {
        ReusableSimpleMessage result = (ReusableSimpleMessage)threadLocalSimpleMessage.get();
        if (result == null) {
            result = new ReusableSimpleMessage();
            threadLocalSimpleMessage.set(result);
        }

        return result;
}

Третий и самый важный аспект — метод release(final Message message). Когда внешние классы завершают использование объекта типа ReusableSimpleMessage для своих внутренних операций, вызывается этот метод. Его реализация гарантирует, что любые ресурсы или данные, хранящиеся в объекте, будут правильно очищены или сброшены. Вы бы заметили здесь использование интерфейса Clearable. Если передан какой-либо другой подкласс типа Message, который не реализует интерфейс Clearable, то он будет проигнорирован.

Выполняя эту очистку, объект становится готовым для повторного использования в последующих запросах.

public static void release(final Message message) {
        if (message instanceof Clearable) {
            ((Clearable)message).clear();
        }

}

Как упоминалось ранее, в Log4j2 используются различные повторно используемые классы, и каждый тип класса, включая ReusableSimpleMessage, включает собственную реализацию метода clear(). Следование этому методу значительно снижает нагрузку на сборщик мусора и повышает производительность вашего приложения.

Оптимизация методов Varargs

В Java varargs (переменные аргументы) — это функция, которая позволяет методу принимать переменное количество аргументов одного типа.

// Example
Message newMessage(String message, Object... params);

Это обеспечивает гибкость, когда вы заранее не знаете, сколько аргументов будет передано методу. Использование этой функции облегчает вашу жизнь как разработчика, но у нее есть обратная сторона.

Более глубокое изучение того, как это работает, покажет вам, что функциональность varargs — это просто синтаксический сахар по сравнению с использованием массивов переменного размера. Каждый раз, когда вызывается метод varargs, должен создаваться массив для хранения переменных аргументов. Эта операция выделения массива включает в себя выделение памяти и инициализацию, что требует времени и ресурсов. Кроме того, все переменные аргументы необходимо скопировать во вновь созданный массив, что влечет за собой дополнительные накладные расходы. Проблема становится более заметной, когда метод вызывается часто или когда используется большое количество аргументов.

Чтобы смягчить эти проблемы с производительностью, Log4j2 использует другой подход. Он предлагает перегруженные методы, которые имеют предопределенное количество параметров до определенного предела. Предоставляя несколько методов с фиксированным количеством параметров, Log4j2 в некоторых случаях избегает необходимости в varargs. Давайте рассмотрим несколько примеров кода из библиотеки Log4j2, чтобы лучше понять это.

Давайте посмотрим на интерфейс под названием MessageFactory2. Код для того же приведен ниже:

public interface MessageFactory2 extends MessageFactory {
    Message newMessage(CharSequence charSequence);

    Message newMessage(String message, Object p0);

    Message newMessage(String message, Object p0, Object p1);

    Message newMessage(String message, Object p0, Object p1, Object p2);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8);

    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9);
}

Пока мы смотрим на него, давайте также посмотрим на интерфейс MessageFactory, который расширен на MessageFactory2.

public interface MessageFactory {
    Message newMessage(Object message);

    Message newMessage(String message);

    Message newMessage(String message, Object... params);
}

Как вы могли заметить, MessageFactory содержит метод с именем newMessage(String message, Object... params) Этот метод принимает String и список необязательных параметров типа Object.

Интересно то, что этот метод дополнительно перегружен в подинтерфейсе с именем MessageFactory2.

Преимущество этого подхода по сравнению с типичным методом varargs становится очевидным, когда клиент выполняет вызов регистрации с определенным количеством аргументов. В таких случаях компилятор может напрямую определить соответствующий перегруженный метод для вызова, обрабатывая его как вызов любого другого метода. Это устраняет накладные расходы, связанные с varargs, и повышает производительность.

Кроме того, если передано больше аргументов, чем поддерживается перегруженными методами, они легко обрабатываются методом varargs, определенным в родительском интерфейсе.

Предлагая эти перегруженные методы, Log4j2 обеспечивает более эффективную альтернативу традиционному использованию varargs. Он оптимизирует процесс разрешения методов и устраняет необходимость создания и копирования массивов, что приводит к повышению скорости выполнения и снижению потребления памяти.

Использование межпоточной связи без блокировки

Log4j2 предоставляет реализацию асинхронных регистраторов. Использование этого повышает пропускную способность приложения, поскольку клиентам больше не нужно ждать завершения вызова ведения журнала, прежде чем продолжить.

Хотя сложная работа этого механизма выходит за рамки нашего сегодняшнего обсуждения, он включает в себя взаимодействие между потоками для передачи данных из основного потока-производителя в различные вспомогательные потоки, ответственные за запись данных в соответствующие места назначения (файлы, консоли и т. д.). .

Для достижения межпотоковой связи могут использоваться различные подходы, одним из наиболее часто используемых методов является использование очереди блокирующего массива. Однако этот подход имеет присущие ему ограничения. Это приводит к конкуренции за потоки, что негативно влияет на пропускную способность ведения журналов даже в асинхронном режиме. Причина этого в том, что несколько потоков конкурируют за чтение или запись данных из очереди, что требует использования блокировок. Чтобы преодолеть эту проблему, Log4j2 использует другую стратегию, используя неблокирующую структуру данных для межпотокового взаимодействия. Здесь в дело вступает библиотека disruptor.

Эта библиотека была создана для реализации способа безблокировочной связи между потоками. В основе этого лежит структура данных кольцевого буфера, которая представляет собой не что иное, как массив фиксированного размера. Производители запрашивают слоты в кольцевом буфере и публикуют свои события, увеличивая порядковый номер. Потребители, с другой стороны, отслеживают свой прогресс, отслеживая порядковый номер, который они обработали. Это позволяет выполнять параллельную обработку без необходимости блокировок или синхронизированных блоков.

Опять же, более подробное изучение того, как это работает внутри, выходит за рамки сегодняшнего разговора, ключевая концепция заключается в том, что эту библиотеку можно использовать не только для асинхронного ведения журнала в Log4j2, но и в других сценариях, требующих эффективного взаимодействия между потоками. и лучшую пропускную способность.

Подробнее об асинхронном логировании Log4j2 читайте здесь

Уроки, извлеченные из внутренней оптимизации Log4j2, дают ценную информацию для любого Java-приложения. Применяя такие методы, как повторное использование объектов, оптимизация varargs и использование библиотеки LMAX Disruptor, разработчики могут повысить производительность и эффективность своих собственных приложений.

Спасибо за чтение. Не забудьте похлопать и поделиться, если вам понравилась эта статья. Удачного кодирования!