Не все объекты в Java размещаются в куче.

Java задняя часть JVM переводчик

Некоторое время назад я написал статью «Углубленный анализ принципов компиляции Java» для любителей гольфа планеты, в которой представил разницу и принципы компиляции javac и JIT-компиляции в Java. И упомянуто в тексте: В дополнение к функции кэширования, JIT-компиляция также будет выполнять различные оптимизации кода, такие как: анализ выхода, устранение блокировки, расширение блокировки, встраивание методов, устранение проверки нулевого значения, устранение обнаружения типа, общие удаление подвыражений и т. д.

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

Стратегия выделения памяти JVM

Структура памяти и метод выделения памяти JVM не являются предметом этой статьи, и здесь дается только краткий обзор. Вот некоторые известные нам здравые смыслы:

1. Согласно спецификации виртуальной машины Java, память, управляемая виртуальной машиной Java, включает в себя области методов, стеки виртуальных машин, собственные стеки методов, кучи, программные счетчики и т.п.

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

3. Некоторые основные типы переменных данных (целые/короткие/длинные/байтовые/плавающие/двойные/логические/символьные) и ссылки на объекты хранятся в стеке.

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

5. Ссылочные переменные массива хранятся в памяти стека, а элементы массива — в памяти кучи.

В "Углубленном понимании виртуальной машины Java" есть такое описание памяти кучи Java:

Однако с развитием периода JIT-компиляции и постепенной зрелостью технологии escape-анализа технология оптимизации выделения стека и скалярной замены приведет к некоторым тонким изменениям, и все объекты будут размещены в куче и постепенно станут менее «абсолютными». . . .

Это лишь краткое упоминание, а глубокого анализа нет.Многие люди видят, что из-за того, что они не понимают JIT, escape-анализа и других технологий, они не могут толком понять смысл вышеприведенного абзаца.

PS: По умолчанию все знают, что такое JIT, те, кто не знает, могут сначала погуглить или присоединиться к моей планете знаний и прочитать эксклюзивную статью для игроков в гольф.

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

анализ побега

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

Основное поведение escape-анализа заключается в анализе динамической области действия объектов: когда объект определен в методе, на него могут ссылаться внешние методы, например передавать его в качестве параметра вызова в другие места, что называется побегом метода.

Например:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

StringBuffer sb — это внутренняя переменная метода, в приведенном выше коде sb возвращается напрямую, так что этот StringBuffer может быть изменен другими методами, так что его область видимости находится не только внутри метода, хотя это и локальная переменная, она Говорят, что он убегает к методу снаружи. К нему могут обращаться даже внешние потоки, например присваивание переменным класса или переменным экземпляра, к которым можно получить доступ в других потоках, что называется выходом из потока.

Если приведенный выше код хочет, чтобы StringBuffer sb не экранировал метод, его можно написать так:

public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

Не возвращайте StringBuffer напрямую, тогда StringBuffer не ускользнет от метода.

Используя escape-анализ, компилятор может оптимизировать код следующим образом:

1. Синхронизация отсутствует. Если обнаружено, что объект доступен только из одного потока, то операции с этим объектом могут выполняться независимо от синхронизации.

2. Преобразовать выделение кучи в выделение стека. Если объект выделяется в подпрограмме, чтобы указатель на объект никогда не исчезал, объект может быть кандидатом на выделение в стеке, а не в куче.

3. Отдельный объект или скалярная замена. Некоторым объектам может не понадобиться существовать как непрерывная структура памяти для доступа, поэтому часть (или весь) объекта может храниться не в памяти, а в регистрах ЦП.

Вышеупомянутое содержание о синхронном пропуске, я в "Глубокое понимание многопоточности (5) - технология оптимизации блокировок виртуальной машины Java«Введенная, то есть технология устранения блокировок в оптимизации блокировок также опирается на технологию анализа побегов.

В этой статье в основном представлено второе применение escape-анализа: преобразование распределения кучи в выделение стека.

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

Когда код Java выполняется, вы можете указать, следует ли включить анализ побега с помощью параметров JVM.-XX:+DoEscapeAnalysis: Указывает, что включен анализ побега.-XX:-DoEscapeAnalysis: Указывает, что escape-анализ отключен. Начиная с jdk 1.7, escape-анализ запущен по умолчанию. Если вы хотите отключить его, вам нужно указать-XX:-DoEscapeAnalysis

Выделение памяти объекта в стеке

Мы знаем, что в общем случае выделение памяти для объектов и элементов массива выполняется в куче памяти. Но по мере взросления JIT-компиляторов многие оптимизации делают эту стратегию распределения не абсолютной. JIT-компилятор может решить, следует ли преобразовывать выделение памяти объекта из кучи в стек, по результатам escape-анализа во время компиляции.

Давайте посмотрим на следующий код:

public static void main(String[] args) {
    long a1 = System.currentTimeMillis();
    for (int i = 0; i < 1000000; i++) {
        alloc();
    }
    // 查看执行时间
    long a2 = System.currentTimeMillis();
    System.out.println("cost " + (a2 - a1) + " ms");
    // 为了方便查看堆内存中对象个数,线程sleep
    try {
        Thread.sleep(100000);
    } catch (InterruptedException e1) {
        e1.printStackTrace();
    }
}

private static void alloc() {
    User user = new User();
}

static class User {

}

На самом деле содержание кода очень простое, то есть используйте цикл for для создания в коде 1 миллиона объектов User.

Мы определяем объект User в методе alloc, но не обращаемся к нему вне метода. То есть объект не выходит за пределы alloc. После JIT-анализа побегов можно оптимизировать распределение памяти.

Мы указываем следующие аргументы JVM и запускаем:

-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 

распечатать в программеcost XX msПосле того, как код работает до конца, мы используем[jmap][1]команда, чтобы увидеть, сколько объектов User находится в текущей памяти кучи:

➜  ~ jps
2809 StackAllocTest
2810 Jps
➜  ~ jmap -histo 2809

 num     #instances         #bytes  class name
----------------------------------------------
   1:           524       87282184  [I
   2:       1000000       16000000  StackAllocTest$User
   3:          6806        2093136  [B
   4:          8006        1320872  [C
   5:          4188         100512  java.lang.String
   6:           581          66304  java.lang.Class

Из приведенных выше результатов выполнения jmap мы видим, что всего было создано 1 миллион куч.StackAllocTest$Userпример.

В случае отключения escape-анализа (-XX:-DoEscapeAnalysis), хотя созданный в методе alloc объект User не выходит за пределы метода, он все равно размещается в куче памяти. То есть, если нет оптимизации JIT-компилятора, нет технологии escape-анализа, так и должно быть при нормальных обстоятельствах. т. е. все объекты размещаются в куче памяти.

Далее мы включаем escape-анализ, а затем выполняем приведенный выше код.

-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError 

распечатать в программеcost XX msПосле того, как код работает до конца, мы используемjmapкоманда, чтобы увидеть, сколько объектов User находится в текущей памяти кучи:

➜  ~ jps
709
2858 Launcher
2859 StackAllocTest
2860 Jps
➜  ~ jmap -histo 2859

 num     #instances         #bytes  class name
----------------------------------------------
   1:           524      101944280  [I
   2:          6806        2093136  [B
   3:         83619        1337904  StackAllocTest$User
   4:          8006        1320872  [C
   5:          4188         100512  java.lang.String
   6:           581          66304  java.lang.Class

Из приведенных выше результатов печати видно, что после включения escape-анализа (-XX:+DoEscapeAnalysis) остается только более 80 000 динамической памяти.StackAllocTest$Userобъект.也就是说在经过JIT优化之后,堆内存中分配的对象数量,从100万降到了8万。

В дополнение к описанному выше методу проверки количества объектов через jmap, читатели также могут попробовать уменьшить объем кучи памяти, затем выполнить приведенный выше код и выполнить анализ в соответствии с количеством сборщиков мусора. Именно потому, что многие выделения кучи оптимизированы для выделения стека, количество сборщиков мусора было значительно уменьшено.

Суммировать

Итак, если кто-то спросит вас позже: все ли объекты и массивы выделяют место в куче памяти?

Тогда вы можете сказать ему: не обязательно, с развитием JIT-компилятора, во время компиляции, если JIT подвергается анализу escape-анализа и обнаруживает, что некоторые объекты не имеют метода escape-последовательности, тогда возможно, что выделение памяти в куче будет оптимизировано для выделения памяти в стеке. . Но это не абсолютно. Как мы видели ранее, не все объекты User не размещаются в куче после включения escape-анализа.

wechat