Кэш кода заполнен, и производительность приложения снизилась.

Java JVM сервер переводчик

0 описание проблемы

После того, как приложение работает в течение определенного периода времени, при постоянном увеличении количества посещений вычислительная мощность внезапно падает. А вот из флоу, jstack, gc в принципе нормально. Такое ощущение, что вдруг"состояние здоровья"попал внутрь"Слабое место".

1 Устранение проблемы

  1. В журнале JVM вы можете найти следующий журнал:

    Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
    Java HotSpot(TM) 64-Bit Server VM warning: Try increasing the code cache size using -XX:ReservedCodeCacheSize=.
    ...
    “CompilerThread0” java.lang.OutOfMemoryError: requested 2854248 bytes for Chunk::new. Out of swap space?
    

    Описание CodeCache заполнен. И JIT остановится в это время,Как только JIT остановится, он больше не встанет, ты можешь представить,Если нет возможности JIT большого количества кода, производительность будет низкой..

  2. Проверьте значение кэша кода с помощью следующей команды:

    jinfo -flag ReservedCodeCacheSize
    

2 решить проблему

  1. Выполнимый метод - расширить пространство кеша кода:

    использовать-XX:ReservedCodeCacheSize=Укажите большее пространство для поддержки большего количества компиляций JIT;

  2. Кроме того, еще один возможный метод — включить механизм повторного использования Code Cache:

    Добавив в параметры запуска:-XX:+UseCodeCacheFlushingвключить;

    Если эта опция включена, перед отключением JITТо есть, прежде чем CodeCache будет заполнен, будет выполнена очистка перед закрытием JIT, и некоторые коды CodeCache будут удалены.;

    Если места после очистки по-прежнему нет, JIT все равно будет закрыт. Эта опция отключена по умолчанию;

3 Базовые знания

3.1 JIT-компиляция точно в срок

Когда дело доходит до «компиляции» в Java, об этом легко думать.javacПроцесс компиляции файлов .java в файлы .class компилятором,здесьjavacКомпилятор называется интерфейсным компилятором, а другие интерфейсные компиляторы, такие как Eclipse, инкрементный компилятор в JDT, ECJ и т. д.. Соответственно есть иБэкэнд-компилятор, который преобразует байт-код в машинный код во время выполнения программы.(В настоящее время Java-программы в основномВыполнение интерпретации плюс выполнение компиляции), например компилятор JIT (Just In Time Compiler), который поставляется с виртуальной машиной HotSpot (на стороне клиента и на стороне сервера).

Java-программы изначально выполняются только через интерпретацию интерпретатора,То есть байт-код интерпретируется и выполняется один за другим, и скорость выполнения этого метода относительно низкая., особенно когда метод или блок кода запускается очень часто, эффективность выполнения этого метода очень низкая. Итак, позжеПредставлен JIT-компилятор (компилятор точно в срок) в виртуальной машине., когда виртуальная машина обнаруживает, что метод или блок кода запускается очень часто, достигается определенный порог,Эти коды будут обозначаться как «Код горячей точки» (горячий код)., чтобы повысить эффективность выполнения горячего кода во время выполнения,Виртуальная машина скомпилирует эти коды в машинный код, соответствующий локальной платформе, и выполнит различные уровни оптимизации., эту задачу выполняет JIT-компилятор.

Теперь коммерческие виртуальные машины (такие как: Sun HotSpot, IBM J9) почтиоба содержат как интерпретатор, так и компилятор, JRockit, одна из трех основных коммерческих виртуальных машин, является исключением, у нее нет внутреннего интерпретатора,Следовательно, будут такие недостатки, как долгое время запуска., но это в первую очередь серверно-ориентированное приложение, которое обычно не фокусируется на времени запуска.

И переводчики, и редакторы имеют свои преимущества:

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

Существует два типа «горячего кода», который компилируется JIT-компилятором во время выполнения:

  1. Метод вызывается несколько раз;
  2. Тело цикла, который вызывается несколько раз;

3.2 Code Cache

Как только код Java скомпилирован компилятором в машинный код во время выполнения, скомпилированный код будет выполняться непосредственно при следующем выполнении, то естьСкомпилированный код кэшируется. Область памяти, в которой кэшируется скомпилированный машинный код, называется codeCache.. Это область памяти, независимая от кучи Java.В дополнение к скомпилированному JIT-коду код собственного метода (JNI), используемый Java, также будет храниться в codeCache..

JVM运行时内存区域,堆与非堆划分

Кэш кода используется JVM для хранения скомпилированного и оптимизированного кода JIT C1/C2.. Поскольку он существует в памяти, его размер должен быть ограничен.Максимальный размер кеша кода может быть переданjinfo -flag ReservedCodeCacheSizeполучить,Обычно по умолчанию 48 м на 64-битной машине..

Различные версии JVM и разные методы запуска имеют разные размеры codeCache по умолчанию:

Версия JVM и метод запуска Размер codeCache по умолчанию
32-bit client, Java 8 32 MB
32-bit server, Java 8 48M
32-bit server with Tiered Compilation, Java 8 240 MB
64-bit server, Java 8 48M
64-bit server with Tiered Compilation, Java 8 240 MB
32-bit client, Java 7 32 MB
32-bit server, Java 7 48 MB
32-bit server with Tiered Compilation, Java 7 96 MB
64-bit server, Java 7 48 MB
64-bit server with Tiered Compilation, Java 7 96 MB

3.3 Многоуровневая компиляция

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

В дополнение к чистому режиму компиляции и смешанному по умолчанию,Многоуровневый метод компиляции был представлен начиная с JDK6u25..

Hotspot JVM имеет два встроенных компилятора, а именноКомпилятор C1, используемый при запуске в режиме клиентаиКомпилятор C2, используемый при запуске в режиме сервера.

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

  2. Цель компилятора C1 — заставить программу как можно быстрее перейти к стадии компиляции и выполнения.,Следовательно, статистической информации, которую нужно собрать перед компиляцией, намного меньше, чем у C2, и скорость компиляции тоже намного выше.. Компромисс заключается в том, что скомпилированный объектный код менее эффективен, чем компиляция C2.

несмотря на это,Эффективность выполнения компиляции C1 также имеет огромные преимущества по сравнению с интерпретируемым выполнением.. Многоуровневый метод компиляции является компромиссным.В начале запуска системы код с высокой частотой выполнения будет сначала скомпилирован компилятором C1, чтобы как можно быстрее начать компиляцию и выполнение.. С течением времени,Некоторый часто выполняемый код будет перекомпилирован компилятором C2., чтобы добиться более высокой производительности.

Включите многоуровневый режим компиляции со следующими параметрами JVM:

-XX:+TieredCompilation 

В JDK8 при запуске в режиме сервера многоуровневая компиляция включена по умолчанию.. должны знать о том,Многоуровневый метод компиляции можно использовать только в режиме сервера., если нужно отключить многоуровневую компиляцию, нужно добавить параметры запуска-XX:-TieredCompilation; если запущен в клиентском режиме,-XX:+TieredCompilationпараметры будут игнорироваться.

3.4 Что делать, если кэш кода заполнен

Со временем, по мере того как компилируется все больше и больше методов, использование codeCache будет постепенно увеличиваться, пока оно не будет исчерпано. Когда кэш кода заполнен, будет напечатан следующий журнал:

До JDK1.7.0_4, вы увидите этот вывод в журнале jvm:

Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
Java HotSpot(TM) 64-Bit Server VM warning: Try increasing the code cache size using -XX:ReservedCodeCacheSize=.
...
“CompilerThread0” java.lang.OutOfMemoryError: requested 2854248 bytes for Chunk::new. Out of swap space?
  1. JIT-компилятор остановлен, и не будет перезапущен,В этот момент он вернется к выполнению интерпретации;

  2. Скомпилированный код по-прежнему выполняется как скомпилированный, но код, который еще не скомпилированМожет быть выполнен только в интерпретируемом виде.

В ответ на эту ситуацию JVM предоставляет более агрессивный метод утилизации codeCache:Speculative flushing.

Этот метод утилизации включен по умолчанию после JDK1.7.0_4., в то время как предыдущие версии нужно было включать с параметром запуска:-XX:+UseCodeCacheFlushing.

При включенной спекулятивной очистке, когда codeCache вот-вот будет исчерпан:

  1. Первая половина компилируемых методов будет помещена в старый список для сбора.;

  2. через определенный промежуток времени,Если метод в старом списке не вызывается, этот метод будет очищен из codeCache;

К сожалению, в JDK1.7, когда codeCache исчерпан, Спекулятивная очистка освобождает место,Но из журнала компиляции JIT-компиляция не вернулась в норму, и общая производительность системы сильно падает, и происходит большое количество тайм-аутов.

Я видел такой баг на официальном сайте Oracle: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8006952 Из-за проблемы алгоритма рециркуляции codeCache,Когда codeCache заполнен, поток компиляции не может продолжаться, потребляет много ресурсов ЦП и замедляет работу системы.. Затронутой ошибкой версией является JDK8, но, судя по информации из других источников в Интернете, у JDK7 должна быть такая же проблема, и она не была исправлена.

Итак, на данный момент,Включение UseCodeCacheFlushing вызовет следующие проблемы:

  1. При заполнении Code Cache работа по очистке выполняется в срочном порядке.он выбрасывает половину старого скомпилированного кода;
  2. Пространство кеша кода уменьшено вдвое,Работа по компиляции метода по-прежнему может не возобновляться;
  3. промывка может вызвать высокую загрузку процессора, тем самым влияя на снижение производительности;

3.6 Настройка кэша кода

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

-XX:ReservedCodeCacheSize=256M

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

Следует отметить, что значение этого codeCache не настолько велико, насколько это возможно..Для 32-битной JVM, максимальный объем памяти, который можно использовать, составляет 4 ГБ. Это 4-гигабайтное пространство памяти включает в себя не только память кучи java, но и память, занимаемую самой JVM, собственную память (например, directBuffer) и codeCache, используемые в программе. Если для codeCache задано слишком большое значение, даже если он используется не так часто, JVM зарезервирует для него это пространство памяти, что приведет к уменьшению объема памяти, которую может использовать само приложение.Для 64-битной JVM, поскольку объем памяти достаточно велик, слишком большое значение параметра codeCache не окажет существенного влияния на приложение.

В JDK 8 предоставляется параметр запуска-XX:+PrintCodeCacheРаспечатайте использование codeCache, когда JVM остановлена.Где max_used — максимальное использование codeCache во время всего запущенного процесса.. Это значение можно использовать для установки разумного размера codeCache, чтобы уменьшить использование памяти и обеспечить нормальную работу приложения.

3.7 Решение проблем

Если причина и следствие проблемы будут выяснены, она будет решена.Как упоминалось выше, как метод чистой компиляции, так и метод многоуровневой компиляции могут решить или облегчить проблему чрезмерной нагрузки после запуска., то у нас есть 2 варианта:

  1. Использовать многоуровневую компиляциюи измените размер codeCache на 256M;

  2. используя чистую компиляциюи измените размер codeCache на 256M;

Побегав некоторое время, выяснилось, чтоС точки зрения управления нагрузкой после запуска лучше использовать метод чистой компиляции., нагрузка практически не растет после запуска, аПосле запуска послойного метода компиляции нагрузка увеличится, но не будет очень высокой, а также уменьшится через небольшой промежуток времени.. Однако с точки зрения времени запуска многоуровневая компиляция примерно на 10 секунд короче исходного метода запуска по умолчанию (исходный запуск занимал 110-130 секунд), а время запуска режима чистой компиляции удваивается, достигая 250 секунд или даже больше. Таким образом, похоже, что подход многоуровневой компиляции является лучшим выбором.

Однако JDK 7 плохо справляется с повторным использованием codeCache. Даже если мы установим codeCache на 256M, установленный порог тревоги в 200M легко достигается онлайн. И как только codeCache будет заполнен, это приведет к замедлению работы системы.Итак, мы нацелены на JDK 8..

Тесты показывают, чтоJDK 8 значительно улучшил переработку codeCache.. Не только рост codeCache относительно плавный, но и когда использование достигает 75%, интенсивность рециркуляции значительно увеличивается, а использование codeCache колеблется вокруг этого значения и медленно растет.Самое главное, что JIT-компиляция по-прежнему работает нормально, и скорость работы системы не снижается..

3.8 Просмотр кеша кода во время выполнения

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

import java.io.File;

import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import com.sun.tools.attach.VirtualMachine;

public class CodeCacheUsage {

    private static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";

    public static void main(String[] args) throws Exception {
        if(args.length != 1) {
            System.err.println("Must enter one arg: pid");
            System.exit(0);
        }
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        JMXConnector connector = null;
        try {
            String connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);

            if (connectorAddress == null) {
                String agent = vm.getSystemProperties().getProperty("java.home")
                                        + File.separator
                                        + "lib"
                                        + File.separator + "management-agent.jar";
                vm.loadAgent(agent);

                connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
            }

            JMXServiceURL url = new JMXServiceURL(connectorAddress);
            connector = JMXConnectorFactory.connect(url);
            MBeanServerConnection mbeanConn = connector.getMBeanServerConnection();
            ObjectName name = new ObjectName("java.lang:type=MemoryPool,name=Code Cache");
            System.out.println(mbeanConn.getAttribute(name, "Usage"));
        } finally {
            if(connector != null)
                connector.close();
            vm.detach();
        }
    }
}

Передайте pid, и после выполнения приведенного выше кода будет выведена информация, похожая на следующую:

javax.management.openmbean.CompositeDataSupport(compositeType=javax.management.openmbean.CompositeType(name=java.lang.management.MemoryUsage,items=
(
(itemName=committed,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long)),
(itemName=init,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long)),
(itemName=max,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long)),
(itemName=used,itemType=javax.management.openmbean.SimpleType(name=java.lang.Long))
)),
contents={committed=50331648, init=2555904, max=50331648, used=48281152})

Приведенная выше информация показывает, что когда область кэша кода инициализирована, она равна 2555904, максимальное значение равно 50331648, занято 50331648 и использовано 48281152.