Остерегайтесь утечек памяти в рекурсии

Java
Остерегайтесь утечек памяти в рекурсии

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

Среда приложения

JDK 1.7 + Spring 4.3 + mybatis + oracle

Устранение неполадок

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

    private void queryAllData(Request request, List querData, int count, String path, List allData) {
        if (CollectionUtils.isEmpty(querData)) {
            return;
        }
        allData.addAll(querData);
        // 总 List 大于一定指定数量将数据刷新到文件
        if (allData.size() > 20000) {
            saveToFile(request, allData, path);
        }
        // 判断下一个偏移量 是否大于 总数
        request.setPageNo(request.getPageNo() + 1);
        // 查询下一页数据
        List  newQueryData = queryDao.selectDataByPage(request);
        queryAllData(request, newQueryData, count, path, allData);
    }

вqueryDao.selectDataByPageНайдите методы для разбиения на страницы. Целью этого метода является рекурсивный поиск данных подкачки.Если страница данных пуста, это означает, что запрос завершен, и все данные были запрошены на данный момент.

Почему бы просто не выполнитьselect * from table where a=xxПодобные данные напрямую узнать все данные?

Поскольку писатель, я запросил общие данные, которые удовлетворяли условию, поэтому, если все данные запрашиваются напрямую, в основном беспокоит то, что память стека напрямую, что приводит кOOMошибка.

После написания кода, его развертывания в Интернете и последующего экспорта данных просто оставьте его в покое и займитесь другими делами. Через некоторое время я вернулся, чтобы увидеть результаты экспорта данных.На этот раз я был удивлен.Программа еще не закончилась, и данные были экспортированы только около 3/4. В это время я понял, что должна быть проблема с программой, поэтому внимательно проверил код и ничего не нашел.

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

Heap after gc

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

老年代内存占用情况

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

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

Зная причину, мы можем найти проблему по ходу дела. Я снова следовал коду, но, к сожалению, не увидел проблемы. ЭтоallDataНабор данных становится все больше и больше, чем вызвано это явление? тщательно проверилsaveToFileлогика кода.

        List<String> lines = Lists.newArrayListWithExpectedSize(allData.size());
        for (Data data : allData) {
            String line = process(data);
            lines.add(line);
        }
        String fileName = "xx.txt";
         try {
            log.info("文件开始输出,输出行数{}", lines.size());
            FileUtils.writeLines(new File(fileName), "utf-8", lines, true);
            allData.clear();
            lines = null;
        } catch (IOException e) {
            log.error("文件输出失败", e);
            // 输出失败,先不管了,将数据继续保存集合中
        }

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

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


    private void queryAllData(Request request, List querData, int count, String path, List allData) {
        if (CollectionUtils.isEmpty(querData)) {
            return;
        }
        allData.addAll(querData);
        // queryData 放入到 allData 中后,将 querData 结合清空。
        querData.clear();
        // 总 List 大于一定指定数量将数据刷新到文件
        if (allData.size() > 20000) {
            saveToFile(request, allData, path);
        }
        // 判断下一个偏移量 是否大于 总数
        request.setPageNo(request.getPageNo() + 1);
        // 查询下一页数据
        newQueryData = queryDao.selectDataByPage(request);
        queryAllData(request, newQueryData, count, path, allData);

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

После запуска vjtop я смотрел на использование кучи памяти. Затем обнаруживается, что пространство Эдема продолжает подниматься до тех пор, пока оно не будет близко к заполнению, а затем происходит Малый GC, и пространство Эдема быстро опустошается. Память о старом районе не занята настолько преувеличенно, чтобы быть близкой к полной. Занимает около 1/5 памяти. Улучшение, как и ожидалось, и через определенный период времени данные экспортируются.

анализировать

Теперь разберем, почему происходит утечка памяти.

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

Что такое стек виртуальной машины?

Выдержки, объясненные в книге Подробная виртуальная машина Java

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

Когда поток Java выполняет метод, на рисунке показана структура данных стека виртуальной машины jvm.

虚拟栈

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

递归中栈

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

## Суммировать

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

  1. Если фактическая работа программы слишком далека от ожидаемой, то можно не думать об этом, должно быть что-то не так, и быстро садиться в машину, чтобы проверить это.
  2. Вывод журнала необходимых узлов для запуска программы необходимо распечатать. Когда вышеприведенная программа была впервые написана, ее было не так сложно обдумать из-за субъективного смысла, и вскоре она была завершена и развернута. И, наконец, проверьте журнал, так как там нет необходимого вывода журнала, я не знаю, что программа там застряла.
  3. Вам нужно знать некоторые инструменты, связанные с JVM, вы можете вовремя проверять условия, связанные с JVM, такие как использование памяти. Как и в примере из этой статьи, мы можем сделать дамп памяти и проанализировать, где происходит утечка памяти. К сожалению, я только на уровне понимания в этом плане, но я не знаю, как начать, когда я его использую, поэтому мне приходится прибегать к некоторым готовым инструментам с открытым исходным кодом, чтобы завершить его. После этого мне нужно компенсировать эту оперативную способность, хахаха.
  4. В этой статье вместо рекурсивного режима используется цикл While, проблема может быть возможной. Утечки памяти в рекурсивном методе могут быть более скрытыми, и ими легко пренебречь.Когда студенты в следующий раз будут писать рекурсивные методы, они должны не только обратить внимание на глубину рекурсивного метода, но также отметить, что этот процесс должен освобождать неиспользуемые объекты в время, не допускайте утечек памяти.

Ну статья наверное такая, увидимся в следующей статье.

Справочные статьи и веб-сайты

  1. Подробная глава о памяти кучи виртуальной машины Java
  2. Подробное объяснение кучи, стека и области методов в Java JVM
  3. веб-сайт анализа журнала gc
  4. Инструмент для просмотра информации о процессе JVM -- vjtop

Если вы считаете, что это хорошо, пожалуйста, поставьте автору большой палец вверх~ Спасибо.

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