Некоторое время назад я написал статью «Углубленный анализ принципов компиляции 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-анализа.