Некоторое время назад в связи с потребностями бизнеса необходимо было запросить все данные, удовлетворяющие условиям, из базы данных, а затем импортировать их в файл. Поэтому я просто написал программу для запроса всех условий и последующей записи в файл. Но на самом деле я обнаружил, что после того, как программа запустилась, я сразу увидел некоторые данные, записанные в файл, но потом она работала все медленнее и медленнее, поэтому я проанализировал и проверил это.
Среда приложения
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, а затем вы можете просмотреть использование памяти кучи в различные моменты времени. Анализ показан на рис.
На этом рисунке показано использование памяти кучи после сборки мусора. Видно, что память кучи не сбрасывается очень быстро после полной сборки мусора, и вскоре начинается следующая полная сборка мусора. Таким образом, можно примерно увидеть, что программа не закончила работу в течение ожидаемого времени, потому что куча была занята слишком сильно, что продолжало вызывать полный сборщик мусора, а поток приложения продолжал приостанавливаться. Затем мы рассмотрим старое поколение динамической памяти.
Как показано на рисунке выше, занятое пространство динамической памяти в старом поколении продолжает увеличиваться до тех пор, пока оно не будет почти заполнено, что приводит к полному сбору мусора, что не улучшает ситуацию, после чего занятость памяти становится близкой к полной.
Таким образом, мы можем знать, что в программе есть утечка памяти.
Зная причину, мы можем найти проблему по ходу дела. Я снова следовал коду, но, к сожалению, не увидел проблемы. Это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 пространство не может быть создано. Последнее явление, которое мы видим, это очень медленное выполнение программы.
## Суммировать
Эта проблема не кажется сложной по своей природе, но устранение проблемы, когда она действительно возникает, занимает много времени. Ниже мы подытожим процесс.
- Если фактическая работа программы слишком далека от ожидаемой, то можно не думать об этом, должно быть что-то не так, и быстро садиться в машину, чтобы проверить это.
- Вывод журнала необходимых узлов для запуска программы необходимо распечатать. Когда вышеприведенная программа была впервые написана, ее было не так сложно обдумать из-за субъективного смысла, и вскоре она была завершена и развернута. И, наконец, проверьте журнал, так как там нет необходимого вывода журнала, я не знаю, что программа там застряла.
- Вам нужно знать некоторые инструменты, связанные с JVM, вы можете вовремя проверять условия, связанные с JVM, такие как использование памяти. Как и в примере из этой статьи, мы можем сделать дамп памяти и проанализировать, где происходит утечка памяти. К сожалению, я только на уровне понимания в этом плане, но я не знаю, как начать, когда я его использую, поэтому мне приходится прибегать к некоторым готовым инструментам с открытым исходным кодом, чтобы завершить его. После этого мне нужно компенсировать эту оперативную способность, хахаха.
- В этой статье вместо рекурсивного режима используется цикл While, проблема может быть возможной. Утечки памяти в рекурсивном методе могут быть более скрытыми, и ими легко пренебречь.Когда студенты в следующий раз будут писать рекурсивные методы, они должны не только обратить внимание на глубину рекурсивного метода, но также отметить, что этот процесс должен освобождать неиспользуемые объекты в время, не допускайте утечек памяти.
Ну статья наверное такая, увидимся в следующей статье.
Справочные статьи и веб-сайты
- Подробная глава о памяти кучи виртуальной машины Java
- Подробное объяснение кучи, стека и области методов в Java JVM
- веб-сайт анализа журнала gc
- Инструмент для просмотра информации о процессе JVM -- vjtop
Если вы считаете, что это хорошо, пожалуйста, поставьте автору большой палец вверх~ Спасибо.
Читатели, которым понравилась эта статья, добро пожаловать в долгое нажатие, чтобы следить за сообщением программы номера подписки ~ позвольте мне поделиться программой с вами.