W3docs

Советы по производительности Java

Практические советы по производительности Java — сначала измерение, избегание преждевременной оптимизации, распространённые микрооптимизации.

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

Сначала измеряйте, потом оптимизируйте

Догадки о производительности — верный способ потратить часы на ускорение кода, который никогда не был медленным. Профилируйте реальную нагрузку, найдите горячий путь (тот небольшой участок кода, где тратится большая часть времени), и только после этого оптимизируйте его. Для быстрых экспериментов System.nanoTime() даёт разницу по «настенным часам»; для серьёзных бенчмарков используйте инструмент вроде JMH (Java Microbenchmark Harness), который прогревает JIT и учитывает погрешности измерений.

long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");

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

Строим строки с помощью StringBuilder

Поскольку строки неизменяемы, s += x внутри цикла создаёт новую строку и копирует все предыдущие символы на каждой итерации — это работа O(n²). StringBuilder хранит растущий буфер и дополняет его на месте, превращая ту же задачу в O(n).

// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
    csv += field + ",";
}

// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
    sb.append(field).append(',');
}
String csv2 = sb.toString();

Одиночная конкатенация a + b + c вне цикла не проблема — компилятор уже преобразует её в один StringBuilder (смотрите конкатенацию строк, чтобы понять, что делает компилятор). Проблема — конкатенация внутри цикла, где каждый проход добавляет ещё одну полную копию.

Выбирайте правильную структуру данных

Наибольший выигрыш обычно даёт алгоритмический выбор, а не микрооптимизации. Поиск значения в ArrayList перебирает все элементы (O(n)); HashMap или HashSet делают это за примерно постоянное время (O(1)). Выбирайте коллекцию, которая соответствует тому, как вы на самом деле обращаетесь к данным.

ПотребностьИспользуйтеСтоимость поиска
Доступ по индексу, добавление в конецArrayListO(1) по индексу, O(n) по значению
Поиск по ключу/значениюHashMapO(1) в среднем
Проверка вхождения, без дубликатовHashSetO(1) в среднем
Отсортированные ключиTreeMapO(log n)
Частые вставки/удаления с концовArrayDequeO(1) на концах

Если вы знаете финальный размер, передайте его в конструктор: new ArrayList<>(10_000) или new HashMap<>(capacity). Это позволяет избежать повторного перераспределения памяти и копирования при росте коллекции.

Избегайте лишнего создания объектов

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

// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i;   // slow, allocates boxes

// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i;     // fast

Другие простые выигрыши: кешируйте скомпилированные объекты Pattern вместо вызова String.matches() в цикле, переиспользуйте DateTimeFormatter (он потокобезопасен и неизменяем), а в самых горячих внутренних циклах, где важно выделение памяти, предпочитайте расширенный for потокам.

java— editable, runs on the server

Что можно вынести из запуска:

  • Same result? true доказывает, что StringBuilder производит ровно ту же строку, что и +=, поэтому замена влияет только на скорость, но не на корректность.
  • Напечатанное соотношение «x slower» показывает, что конкатенация в цикле обходится во много раз дороже добавления в буфер, поскольку каждый += копирует всю строку целиком.
  • Both lists size 100000: true подтверждает, что ArrayList с предварительно заданным размером в итоге идентичен тому, что рос динамически — подсказка конструктору влияет на выделение памяти, а не на содержимое.
  • Pre-sized faster? true показывает, что указание ArrayList ёмкости заранее позволяет избежать повторных шагов изменения размера и копирования.
  • Map lookups found: 50000 in ... ms демонстрирует, что 50 000 обращений к HashMap завершаются примерно за миллисекунду — вот цена выбора доступа O(1) вместо сканирования списка O(n).

Распространённые ловушки

Несколько ошибок превращают «очевидно быстрый» код в противоположность:

  • Доверие единственному замеру. JIT ещё не прогрелся, а ОС могла переключиться на другую задачу прямо в момент измерения. Повторите работу тысячи раз или используйте JMH, прежде чем доверять числу.
  • Микрооптимизация холодного кода. Метод, запускаемый один раз при старте, ничего не выиграет от более компактного цикла. Тратьте усилия только на горячий путь, на который указывает профайлер.
  • Построение строк через + в цикле. Самое распространённое устранимое замедление — используйте StringBuilder всякий раз, когда конкатенируете строки внутри цикла.
  • Скрытый автобоксинг. List<Integer>, Map<Integer, Integer> или случайный аккумулятор типа Long упаковывает каждое значение. В горячем числовом цикле предпочитайте примитивы и массивы примитивов.
  • Оптимизация до того, как код работает. Сначала правильность, потом скорость. Понятный код, который можно профилировать, лучше хитрого кода, о котором невозможно рассуждать.

Итоги

  • Измеряйте прежде, чем что-либо менять — профилируйте реальную нагрузку и оптимизируйте только горячий путь.
  • Стройте строки с помощью StringBuilder, а не +=, внутри циклов.
  • Подбирайте коллекцию под паттерн доступа: HashMap/HashSet для поиска, ArrayList для индексного доступа; задавайте размер заранее, если количество элементов известно.
  • Избегайте лишнего выделения памяти: предпочитайте примитивы, переиспользуйте неизменяемые объекты и помните, что каждый объект добавляет работу сборщику мусора.

Практика

Практика
Почему использование '+=' для построения String внутри цикла медленнее, чем StringBuilder?
Почему использование '+=' для построения String внутри цикла медленнее, чем StringBuilder?
Was this page helpful?