Помните анализ утечки памяти JAVA

Java задняя часть модульный тест VisualVM

Аннотация: Эта статья принадлежит оригиналу, добро пожаловать на перепечатку, перепечатку, пожалуйста, сохраните источник:GitHub.com/Jason GE ng88…

текущая среда

  1. jdk == 1.8
  2. httpasyncclient == 4.1.3

кодовый адрес

гит-адрес:GitHub.com/Jason GE ng88…

задний план

Не так давно был запущен новый проект.Этот проект представляет собой систему измерения давления.Просто его можно рассматривать как способ непрерывной отправки запросов в службу путем воспроизведения словаря (данные HTTP-запроса) для достижения цели давления сервис замеров. В процессе тестирования все прошло гладко, после исправления нескольких мелких ошибок он заработал. Когда им воспользовалась первая деловая сторона после выхода в интернет, была обнаружена серьезная проблема.После работы приложения около 10 минут было получено большое количество алармов Full GC.

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

Онлайн-расследование

Во-первых, нам нужно устранить неполадки на сервере. Используйте инструмент jmap, поставляемый с JDK, чтобы проверить, какие объекты существуют в приложении JAVA, а также количество и размер их экземпляров. Конкретные команды следующие:

jmap -histo:live `pid of java`

# 为了便于观察,还是将输出写入文件
jmap -histo:live `pid of java` > /tmp/jmap00

После наблюдения действительно установлено, что экземпляров объектов более 200 000. Согласно бизнес-логике, самый конкретизированный список слов - более 100 000. Как может быть больше 200 000? Мы не нашли этого в коде. место для явного объявления экземпляра для этого. На этом этапе нам нужно дополнительно проанализировать дамп памяти в автономном режиме.Команда дампа выглядит следующим образом:

jmap -dump:format=b,file=heap.dump `pid of java`

Автономный анализ

После загрузки с сервера дампа heap.dump нам необходимо провести глубокий анализ с помощью инструмента. Рекомендуемые инструменты здесь — mat и visualVM.

Лично я предпочитаю использовать для анализа VisualVM, так как помимо анализа файлов дампа в автономном режиме, он также может интегрироваться с IDEA, запускать приложение через IDEA и анализировать загрузку процессора, памяти и GC приложения в режиме реального времени (Ситуация с GC, вам нужно установить визуальный плагин GC в VisualVM). Инструменты показаны следующим образом (Это просто чтобы показать эффект, данные не настоящие):

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

воспроизведение сцены

После анализа мы, наконец, обнаружили проблему с утечкой памяти, вызванную использованием httpasyncclient. httpasyncclient — это инструментарий HTTP, предоставляемый Apache, который в основном предоставляет неблокирующую модель реактора ввода-вывода и реализует функцию асинхронной отправки HTTP-запросов.

Кратко поговорим о причинах конкретной утечки памяти через Демо.

httpasyncclient Введение в использование:

  • maven-зависимости
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
    <version>4.1.3</version>
</dependency>
  • Клиент HttpAsyncClient
public class HttpAsyncClient {

    private CloseableHttpAsyncClient httpclient;

    public HttpAsyncClient() {
        httpclient = HttpAsyncClients.createDefault();
        httpclient.start();
    }

    public void execute(HttpUriRequest request, FutureCallback<HttpResponse> callback){
        httpclient.execute(request, callback);
    }

    public void close() throws IOException {
        httpclient.close();
    }

}

Основная логика:

Основная логика демо такова: сначала создается кеш-лист для сохранения данных запроса, которые необходимо отправить. Затем запрос на отправку берется из списка кеша в циклическом порядке и отправляется клиенту httpasyncclient для отправки.

Конкретный код выглядит следующим образом:

public class ReplayApplication {

    public static void main(String[] args) throws InterruptedException {

		 //创建有内存泄露的回放客户端
        ReplayWithProblem replay1 = new ReplayWithProblem();
        
		 //加载一万条请求数据放入缓存
        List<HttpUriRequest> cache1 = replay1.loadMockRequest(10000);
        
        //开始循环回放
        replay1.start(cache1);

    }
}

Реализация клиента воспроизведения (утечка памяти):

В качестве примера возьмем воспроизведение Baidu, создадим 10 000 фрагментов фиктивных данных и поместим их в список кеша. Во время воспроизведения отправлять запрос каждые 100 мс в цикле while. Конкретный код выглядит следующим образом:

public class ReplayWithProblem {

    public List<HttpUriRequest> loadMockRequest(int n){
    
        List<HttpUriRequest> cache = new ArrayList<HttpUriRequest>(n);
        for (int i = 0; i < n; i++) {
            HttpGet request = new HttpGet("http://www.baidu.com?a="+i);
            cache.add(request);
        }
        return cache;
        
    }

    public void start(List<HttpUriRequest> cache) throws InterruptedException {

        HttpAsyncClient httpClient = new HttpAsyncClient();
        int i = 0;

        while (true){

            final HttpUriRequest request = cache.get(i%cache.size());
            httpClient.execute(request, new FutureCallback<HttpResponse>() {
                public void completed(final HttpResponse response) {
                    System.out.println(request.getRequestLine() + "->" + response.getStatusLine());
                }

                public void failed(final Exception ex) {
                    System.out.println(request.getRequestLine() + "->" + ex);
                }

                public void cancelled() {
                    System.out.println(request.getRequestLine() + " cancelled");
                }

            });
            i++;
            Thread.sleep(100);
        }
    }

}

Анализ памяти:

Запустите приложение ReplayApplication (После установки VisualVM Launcher в IDEA вы можете запустить VisualVM напрямую.), наблюдаемый через VisualVM.

  • Ситуация запуска:

  • Доля объектов памяти в VisualVM до и после 3 минут:

Объяснение: $0 представляет сам объект, а $1 представляет первый внутренний класс в объекте. Итак, ReplayWithProblem$1: представляет класс обратного вызова FutureCallback в классе ReplayWithProblem.

Из него мы можем узнать, что класс FutureCallback будет создаваться непрерывно. Поскольку каждый раз, когда HTTP-запрос отправляется асинхронно, создается класс обратного вызова для получения результата, что логически выглядит нормально. Не волнуйтесь, давайте посмотрим вниз.

  • Ситуация с GC в VisualVM до и через 3 минуты:

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

Однако ссылочное отношение анонимного класса обратного вызова исчезает после завершения обратного вызова http, и его следует повторно использовать в следующем сборщике мусора. Мы проследили исходный код запроса, отправленного httpasyncclient, и обнаружили, что внутренняя реализация заключается в том, чтобы вставить класс обратного вызова в класс запроса http, а класс запроса помещается в очередь кеша, поэтому между классом обратного вызова нет ссылочной связи. Выпущено, большое количество callback-классов было выдвинуто на старую область, что в итоге привело к возникновению Full GC.

  • Анализ основного кода:

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

Найдя причину проблемы, теперь мы оптимизируем код и проверяем наши выводы. так какList<HttpUriRequest> cache1Объект обратного вызова будет сохранен в , поэтому мы не можем кэшировать класс запроса, а только основные данные, и генерировать их динамически при его использовании, чтобы обеспечить своевременную переработку объекта обратного вызова.

код показывает, как показано ниже:

public class ReplayApplication {

    public static void main(String[] args) throws InterruptedException {

        ReplayWithoutProblem replay2 = new ReplayWithoutProblem();
        List<String> cache2 = replay2.loadMockRequest(10000);
        replay2.start(cache2);

    }
}
public class ReplayWithoutProblem {

    public List<String> loadMockRequest(int n){
        List<String> cache = new ArrayList<String>(n);
        for (int i = 0; i < n; i++) {
            cache.add("http://www.baidu.com?a="+i);
        }
        return cache;
    }

    public void start(List<String> cache) throws InterruptedException {

        HttpAsyncClient httpClient = new HttpAsyncClient();
        int i = 0;

        while (true){

            String url = cache.get(i%cache.size());
            final HttpGet request = new HttpGet(url);
            httpClient.execute(request, new FutureCallback<HttpResponse>() {
                public void completed(final HttpResponse response) {
                    System.out.println(request.getRequestLine() + "->" + response.getStatusLine());
                }

                public void failed(final Exception ex) {
                    System.out.println(request.getRequestLine() + "->" + ex);
                }

                public void cancelled() {
                    System.out.println(request.getRequestLine() + " cancelled");
                }

            });
            i++;
            Thread.sleep(100);
        }
    }

}
скопировать код

Проверка результата

  • Ситуация запуска:

  • Доля объектов памяти в VisualVM до и после 3 минут:

  • Ситуация с GC в VisualVM до и через 3 минуты:

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

Суммировать

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

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