Пожалуйста, перестаньте говорить, что объекты Java размещаются в куче памяти!

Java

Являясь объектно-ориентированным, кроссплатформенным языком, Java всегда была сложной точкой знаний об объектах, памяти и т. д. Поэтому даже новичок в Java должен иметь более или менее некоторые знания о JVM. Можно сказать, что соответствующие знания о JVM — это, по сути, точка знаний, которую должен изучить каждый разработчик Java, а также точка знаний, которую необходимо проверить во время собеседований.

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

1. Куча — это область памяти, совместно используемая потоками, а стек — это область памяти, совместно используемая исключительно потоками.

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

Однако автор может вам ответственно сказать, что ни один из двух приведенных выше выводов не является полностью правильным.

В моей предыдущей статье память кучи Java разделялась потоками! Интервьюер: Вы уверены? «Представленные точки знаний о куче памяти не полностью разделяют потоки, в этой статье обсуждается вторая тема.

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

В "Спецификации виртуальной машины Java" есть такое описание кучи:

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

В «память кучи Java разделяется потоками! Интервьюер: Вы уверены? «В статье мы также представили, что когда объект Java выделяется в куче, он в основном находится в области Eden. Если TLAB активирован, он будет сначала размещен в TLAB, а в некоторых случаях это может быть непосредственно в старости. , правила выделения не фиксированы на 100%, это зависит от того, какой сборщик мусора используется в данный момент, и настроек связанных с памятью параметров в виртуальной машине.

Но в основном придерживаются следующих принципов:

  • Предметы предпочтительно размещаются в районе Эдема
    • Сначала размещается в Eden, если в Eden недостаточно места, будет запущен монитор GC
  • Крупные предметы уходят прямо в старость
    • Для объектов Java, которым требуется большой объем непрерывной памяти, когда объем памяти, необходимый объекту, превышает значение параметра -XX:PretenureSizeThreshold, объект будет напрямую выделять память в старом возрасте.

Однако, несмотря на наличие таких требований в спецификации виртуальной машины, различные производители виртуальных машин могут вносить некоторые оптимизации в распределение памяти объектов при реализации виртуальных машин. Наиболее типичным из них является зрелость технологии JIT в виртуальной машине HotSpot, так что объектам не нужно выделять память в куче.

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

JIT-технология

Все мы знаем, что исходный код Java-программы может быть скомпилирован и преобразован в байт-код java с помощью javac, а JVM переводит его в соответствующие машинные инструкции, интерпретируя байт-код, считывая его один за другим, а также интерпретируя и переводя его один за другим. Это функция традиционного интерпретатора JVM (Interpreter). очевидно,После того, как компилятор Java будет интерпретирован и выполнен, его скорость выполнения обязательно будет намного ниже, чем непосредственное выполнение исполняемого двоичного байт-кода. Для решения этой проблемы эффективности была внедрена технология JIT (Just In Time, своевременная компиляция).

С помощью технологии JIT программы Java по-прежнему интерпретируются и выполняются интерпретатором. Когда JVM обнаруживает, что метод или блок кода запускается очень часто, она будет рассматривать это как «горячий код». Затем JIT преобразует некоторый «горячий код» в машинный код, относящийся к локальной машине, оптимизирует его, а затем кэширует переведенный машинный код для следующего использования.

Обнаружение горячих точек

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

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

Оптимизация компиляции

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

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

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

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

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

Например:

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

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

С помощью escape-анализа мы можем определить, могут ли переменные в методе быть доступны или изменены другими потоками.Основываясь на этой функции, JIT может выполнять некоторые оптимизации:

  • синхронизировать пропустить
  • скалярная замена
  • выделение стека

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

Скалярная замена, выделение стека

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

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

Итак, представьте, что если мы создаем объект внутри тела метода, и объект не выходит за пределы метода, нужно ли размещать объект в куче?

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

Затем, после анализа побега, если обнаруживается, что объект не выходит за рамки закона, что можно сделать, чтобы оптимизировать его и уменьшить вероятность размещения объекта в куче?

Это распределение стека. В HotSopt выделение стека осуществляется не в процессе, а посредством скалярной замены.

Итак, давайте сосредоточимся на том, что такое скалярная замена и как добиться выделения стека с помощью скалярной замены.

скалярная замена

Скаляр — это часть данных, которую нельзя разбить на более мелкие части. Примитивные типы данных в Java являются скалярами. Напротив, те данные, которые можно разложить, называются агрегатами, а объекты в Java — агрегатами, поскольку они могут быть разложены на другие агрегаты и скаляры.

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

public static void main(String[] args) {
   alloc();
}

private static void alloc() {
   Point point = new Point(1,2);
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}

В приведенном выше коде точечный объект не ускользает от метода alloc, и точечный объект может быть разобран на скаляры. Тогда JIT не будет напрямую создавать объект Point, а напрямую будет использовать два скаляра int x, int y для замены объекта Point.

private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}

Видно, что после анализа побегов совокупной суммы Point было установлено, что он не убегал, а был заменен двумя совокупными суммами.

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

Экспериментальное доказательство

Talk Is Cheap, Show Me The Code

Нет данных, нет ББ;

Далее, давайте проведем эксперимент, чтобы увидеть, может ли анализ побега сработать, действительно ли выделение стека произойдет после того, как оно вступит в силу, и каковы преимущества выделения стека?

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

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 

Где -XX:-DoEscapeAnalysis означает отключить анализ побега.

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

➜  ~ 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 

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

➜  ~ 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-оптимизации количество объектов, размещенных в памяти кучи, уменьшается с 1 миллиона до 80 000.

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

Анализ побега незрелый

В предыдущем примере после включения escape-анализа количество объектов изменилось с 1 миллиона до 80 000, но не равно 0, что указывает на то, что JIT-оптимизация не полностью оптимизирована во всех случаях.

Статьи по анализу ускользания были опубликованы в 1999 году, но не были реализованы до JDK 1.6, а технология еще не очень зрелая.

Фундаментальная причина в том, что нет никакой гарантии, что потребление производительности escape-анализа должно быть выше, чем его потребление. Хотя скалярная замена, выделение стека и устранение блокировки могут быть выполнены после анализа побега. Однако сам анализ побега также требует серии сложных анализов, что на самом деле является относительно трудоемким процессом.

Крайним примером является то, что после анализа побега обнаруживается, что нет объекта, который не ускользает. Тогда процесс анализа побега пропадает зря.

Хотя эта технология не очень зрелая, она также является очень важным средством технологии оптимизации компилятора «точно в срок».

Суммировать

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

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

Следовательно, объект должен выделять память в куче, что неправильно.

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