JavaRush /Java блог /Random /Кофе-брейк #235. 10 распространенных утечек памяти в Java...

Кофе-брейк #235. 10 распространенных утечек памяти в Java и способы их устранения

Статья из группы Random
Источник: Medium В этом руководстве изложены оптимальные решения по поиску и устранению утечек памяти в Java на этапе кодирования. Кофе-брейк #235. 10 распространенных утечек памяти в Java и способы их устранения - 1В перечень распространенных утечек памяти в Java входят:
  1. Статические поля и коллекции.
  2. Незакрытые ресурсы.
  3. Переменные ThreadLocal.
  4. Неограниченное кэширование.
  5. Неправильное использование слушателей событий (Event Listeners).
  6. Неубранные корни сборки мусора.
  7. Неуправляемые пулы потоков.
  8. Неправильное использование шаблона Singleton.
  9. Глубокие и сложные графы объектов.
  10. Сторонние библиотеки.
Поскольку с утечками памяти лучше (и дешевле всего) всего бороться на этапе кодирования, давайте рассмотрим оптимальные способы их устранения.

1. Статические поля и коллекции

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

Пример: статическая HashMap

Рассмотрим следующий фрагмент кода, в котором объект User помещается в статическую HashMap и никогда не удаляется.

public class User {
    private String userName;

    // статическая HashMap, хранящая объекты пользователя
    private static Map<String, User> users = new HashMap<>();

    // Constructor
    public User(String userName) {
        this.userName = userName;
        users.put(userName, this);
    }

    // другие методы
}
Объекты User, помещенные в статическую HashMap под названием users, никогда не будут удалены сборщиком мусора, если только они не будут явно удалены из HashMap.

Решение проблемы:

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

private static Map<String, User> users = new WeakHashMap<>();

2. Незакрытые ресурсы

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

Пример: FileInputStream

Рассмотрим следующий фрагмент кода, который считывает данные из файла:

public void readDataFromFile(String filePath) {
    try {
        FileInputStream fis = new FileInputStream(filePath);
        
        // Чтение данных из файла
    } catch (IOException e) {
        e.printStackTrace();
    }
}
В данном примере FileInputStream не закрывается после использования, что приводит к утечке памяти.

Решение проблемы:

Всегда закрывайте ресурсы после того, как они больше не нужны. В Java 7 появилась функция try-with-resources, которая автоматически закрывает ресурсы при завершении блока.

public void readDataFromFile(String filePath) {
    try (FileInputStream fis = new FileInputStream(filePath)) {

        // Чтение данных из файла

    } catch (IOException e) {
        e.printStackTrace();
    }
}

3. Переменные ThreadLocal

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

Пример: Пользовательская переменная ThreadLocal


public class CustomThreadLocal {
    public static final ThreadLocal<SimpleDateFormat> dateFormatter =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public String formatDate(Date date) {
        return dateFormatter.get().format(date);
    }
}
В этом примере мы сохраняем объект SimpleDateFormat в переменной ThreadLocal. Но если поток не управляется, то объект SimpleDateFormat никогда не будет удален сборщиком мусора.

Решение проблемы:

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

public void cleanup() {
    dateFormatter.remove();
}

4. Неограниченное кэширование

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

Пример: кэш на основе HashMap


public class SimpleCache {
    private final Map<String, BigDecimal> cache = new HashMap<>();

    public BigDecimal getValue(String key) {
        BigDecimal value = cache.get(key);
        if (value == null) {
            value = calculateValue(key);
            cache.put(key, value);
        }
        return value;
    }

    private BigDecimal calculateValue(String key) {
        // Длительная операция по вычислению значения
        return new BigDecimal("123.45");
    }
}
Здесь мы для хранения результатов используем простой кеш на основе HashMap. Однако этот кеш не имеет ограничений, что приводит к утечкам памяти при слишком большом количестве записей.

Решение проблемы:

Ограничьте размер кеша и используйте подходящую политику замещения данных. Такие библиотеки, как Google Guava, предоставляют разработчикам настраиваемые решения для кэширования. Вот пример использования CacheBuilder от Guava:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

public class LimitedCache {
    private final Cache<String, BigDecimal> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build();

    public BigDecimal getValue(String key) {
        BigDecimal value = cache.getIfPresent(key);
        if (value == null) {
            value = calculateValue(key);
            cache.put(key, value);
        }
        return value;
    }

    private BigDecimal calculateValue(String key) {
        // Длительная операция по вычислению значения 
        return new BigDecimal("123.45");
    }
}
В этом решении размер кэша ограничен 1000 записями, а политика замещения настроена на удаление наиболее старых записей, когда кэш достигает максимального размера.

5. Неправильное использование слушателей событий (Event Listeners)

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

Пример: слушатель анонимных действий


class MyButton {
    private List<ActionListener> listeners = new ArrayList<>();

    public void addActionListener(ActionListener listener) {
        listeners.add(listener);
    }

    public void doAction() {
        for (ActionListener listener : listeners) {
            listener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Click"));
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyButton button = new MyButton();

        for (int i = 0; i < 10; i++) {
            // Добавление новых анонимных слушателей
            button.addActionListener(e -> System.out.println("Button clicked"));
        }
    }
}
Здесь мы создаем и добавляем слушателей анонимных действий к пользовательской кнопке. Однако эти слушатели потом никогда не удаляются, что и приводит к утечкам памяти.

Решение проблемы:

Чтобы предотвратить утечку памяти, вызванную слушателями событий, убедитесь, что они удаляются, когда больше не нужны. Одним из решений является использование WeakReference для хранения слушателя событий. Другой подход заключается в том, чтобы гарантировать, что любые долгоживущие или большие объекты, удерживающие слушатель, удалят его, как только они закончат с ним работать:

button.removeActionListener(listener);

6. Неубранные корни сборки мусора

Корни (Roots) сборки мусора — это объекты, которые всегда доступны и, следовательно, никогда не подвергаются сборке мусора. Некоторые из них включают статические переменные, потоки и локальные переменные в основном потоке. Если корни сборки мусора содержат ссылки на ненужные объекты, они предотвращают сборку мусора для этих объектов.

Пример: объект не собран мусором


public static List<BigDecimal> numbers = new ArrayList<>();

public void getData() {
    while (dataAvailable()) {
        BigDecimal number = getNextNumber();
        numbers.add(number);
    }

    processData(numbers);
}
В этом примере статическая переменная numbers содержит ссылки на объекты. Пока список numbers не очищен, он вызывает утечку памяти в приложении.

Решение проблемы:

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

public void getData() {
    while (dataAvailable()) {
        BigDecimal number = getNextNumber();
        numbers.add(number);
    }

    processData(numbers);
    numbers.clear();  // Освобождаем память
}

7. Плохо управляемые пулы потоков

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

Пример: незакрытые исполнители (Executors)


public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                // Какие-то действия
            });
        }
    }
}
Здесь мы не отключаем ExecutorService должным образом, что приводит к утечкам памяти.

Решение проблемы:

Чтобы устранить утечки памяти, связанные с пулами потоков, убедитесь, что ресурсы высвобождаются, а потоки завершаются контролируемым образом. Завершите работу ExecutorService правильно, как только все задачи будут выполнены:

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                // Какие-то действия
            });
        }

        // Завершение работы ExecutorService
        executorService.shutdown();
    }
}

8. Неправильное использование шаблона Singleton

Объекты-синглтоны предназначены для того, чтобы иметь только один экземпляр в течение жизненного цикла приложения. Однако неправильное использование шаблона Singleton может привести к утечке памяти.

Пример: объект Singleton


public class Singleton {
    private static final Singleton instance = new Singleton();

    private List<BigDecimal> data = new ArrayList<>();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

    public void addData(BigDecimal value) {
        data.add(value);
    }

    // Другие методы
}
В этом примере объект Singleton содержит ссылки на объекты BigDecimal через свой список data. Этот список может расти бесконечно, что и приводит к утечкам памяти.

Решение проблемы:

Чтобы избежать утечек памяти, связанных с объектами Singleton, будьте осторожны при использовании шаблона и убедитесь, что вы освобождаете или ограничиваете ресурсы, потребляемые экземпляром Singleton:

public class Singleton {
    private static final Singleton instance = new Singleton();

    private List<BigDecimal> data = new ArrayList<>();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }

    public void addData(BigDecimal value) {
        data.add(value);
    }

    public void clearData() {
        data.clear();
    }

    // Другие методы
}
В улучшенном решении метод clearData добавлен для освобождения ресурсов, удерживаемых экземпляром Singleton.

9. Глубокие и сложные графы объектов

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

Пример: клиент и заказы


public class Customer {
    private List<Order> orders = new ArrayList<>();

    public void addOrder(Order order) {
        orders.add(order);
    }

    // Геттеры и сеттеры
}
public class Order {
    private List<Item> items = new ArrayList<>();

    public void addItem(Item item) {
        items.add(item);
    }

    // Геттеры и сеттеры
}
public class Item {
    private String name;
    private BigDecimal price;

    // Геттеры и сеттеры
}
В данном примере объекты Customer имеют ссылки на объекты Order, а объекты Order имеют ссылки на объекты Item. Если объект Customer больше не нужен, но не отделен от графа объектов должным образом, то могут возникнуть утечки памяти.

Решение проблемы:

Убедитесь, что вы правильно управляете ссылками на объекты и отношениями. Чтобы избежать таких утечек памяти, используйте такие методы, как шаблон Observer или слабые ссылки (weak references):

import java.lang.ref.WeakReference;

public class Customer {
    private List<WeakReference<Order>> orders = new ArrayList<>();

    public void addOrder(Order order) {
        orders.add(new WeakReference<>(order));
    }

    // Геттеры и сеттеры
}

10. Сторонние библиотеки

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

Пример: Анализ XML

Некоторые синтаксические анализаторы XML, такие как Xerces, могут вызывать утечки памяти при использовании пользовательских EntityResolvers.

Решение проблемы:

Чтобы избежать утечек памяти из-за сторонних библиотек:
  1. Обновляйте библиотеки до последней стабильной версии.
  2. Изучите работу библиотеки и любые потенциальные проблемы с памятью.
  3. Настройте библиотеку в соответствии с рекомендациями и рекомендациями.
В случае анализатора XML настройка EntityResolver или переход на другую реализацию анализатора XML поможет вам избежать утечек памяти.

// Настройка EntityResolver
public class CustomEntityResolver implements EntityResolver {
    // Реализация для разрешения сущностей
}
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ