поиск проблемы
Сегодня я обнаружил, что онлайн-приложение использует очень много памяти, но очень низкое использование ЦП.
использоватьps
Командование, вы видите, что процесс 19793 занимает 4,9 ГБ памяти, но его загрузка процессора составляет менее 5%, есть проблема.
# ps -aux | grep 19793
user 19793 1.6 9.9 23864228 4904664 ? Sl Oct03 268:52
Я решил, что в этом приложении должна быть утечка памяти, и начал искать и устранять проблему.
Процесс устранения утечек памяти обычно выглядит следующим образом:
- Используйте jmap, чтобы увидеть, какие объекты имеют большое количество объектов и занимают много памяти.
- Анализировать файлы дампа и использование кучи
- Найдите вызывающий процесс определенных классов и связанных кодов и шаг за шагом найдите проблему.
Использование и знакомство с инструментом здесь повторяться не буду, приведу цитату блогерастатья
Расположение проблемы и устранение неполадок
1. Используйте jmap для просмотра использования кучи
Команда выполненияjmap -hive 19793
Просмотрите ситуацию экземпляра объекта, как показано на рисунке:
нашел здесьStandardSession
На самом деле существует 1,4 миллиона экземпляров.StandardSession
Это конкретная реализация сеанса tomcat.Означает ли это, что у Tomcat есть утечка памяти.
2. Понять принцип реализации и повторного использования Tomcat Session
Tomcat используетStandardManager
Сеанс службы управления, иStandardSession
Данные для каждого объекта Session сохраняются.
StandardManager
КаждыйSession
Независимо от того, истек ли срок действия экземпляра, если он истекает, он будет переработан.
Посмотрите непосредственно на исходный код здесь, чтобы понять, как Tomcat управляет сеансом.
// 具体的检测代码在父类 ManagerBase 中
public StandardManager extends ManagerBase {
// ... 忽略不必要的代码
}
public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
//Session实例都是保存在这个Map中的,key 值是 sessionId
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
// 定时运行函数,Tomcat 有一个守护线程,会定时的遍历运行每个容器的 backgroundProcess 函数,
// 一般需要定时执行的代码,都会实现这个函数,让Tomcat统一调用,这样也方便管理
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
public void processExpires() {
//记录当前时间
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();
int expireHere = 0 ;
//遍历所有session,查看是否过期
for (int i = 0; i < sessions.length; i++) {
//判断session是否过期,这里可以看出实际判断是否过期的实现在 session 类中
if ( sessions[i]!=null && !sessions[i].isValid() ) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
processingTime += ( timeEnd - timeNow );}
}
Взгляните на код для StandardSession здесь
// 看看StandardSession 怎么判断 session 是否过期的
public class StandardSession implements HttpSession, Session, Serializable {
//最后活跃时间
protected volatile long lastAccessedTime = creationTime;
// 过期时间,-1 为用不过期
protected volatile int maxInactiveInterval = -1;
// 记录该实例是否已做过期处理
protected volatile boolean isValid = false;
@Override
public boolean isValid() {
//判断是否已经做过期处理
if (!this.isValid) {
return false;
}
//这里开始判断session是否有过期
if (maxInactiveInterval > 0) {
//getIdleTimeInternal 函数是计算最后一次使用时间到当前的间隔
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
//如果时间间隔大于过期时间,进行清除处理
//具体的清除就不贴了,简单的说就是执行 manager 的 sessions.remove(obj) 操作,并且做一下其他的处理
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}
return this.isValid;
}
}
через вышеуказанноеmanager
а такжеSession
Код, вы можете четко знать логику обработки истечения срока действия сеанса, а затем в чем проблема, из-за которой объект сеанса не может быть переработан.
3. Посмотрите, есть ли проблема с вашим кодом
Вообще говоря, объект не перерабатывается, на него нужно где-то ссылаться, вот как он используется в моем коде. На самом деле я использую действие сеанса только в одном перехватчике.
Код сеанса, примененный в моем проекте
// 这是拦截器的一个函数,每个请求进来,必须经过拦截器处理,如果某些方面验证错误,则直接返回错误信息给客户端
public boolean preHandle(HttpServletRequest request, Object handler) throws IOException {
// 获取该请求的 Session对象
HttpSession httpSession = request.getSession();
// 获取请求的参数,并操作 httpSession
// 这里 setMaxInactiveInterval 表示设置该session的过期时间,1800s
String sessionUin = (String) httpSession.getAttribute("uin");
httpSession.setAttribute("uin", uin);
httpSession.setMaxInactiveInterval(1800);
// 其他处理逻辑 ...
return true;
}
Чтобы быть разумным, мой код не может вызвать утечку памяти.Я столкнулся с ошибкой в Tomcat?Я немного взволнован, чтобы думать об этом, и продолжаю искать причину.
4. Экспортируйте информацию о стеке онлайн-процесса и просмотрите значение экземпляра StandardSession.
Экспорт информации о стеке процессов:jmap -dump:format=b,file=tomcatDump 19793
использоватьjhat
Взгляните на статус экземпляра StandardSession.
Здесь вы можете увидеть StandardSessionisValid = false
, указывающий, что экземпляр подвергся обработке истечения срока действия кэша,
увидеть, когда к нему в последний раз обращалисьlastAccessedTime: 1570329063605
, преобразовать метку времени, время2019-10-06 10:31:03:605
, а текущее время2019-10-13
, давно пора, что происходит?
Это не кажется правильным, поищите в Интернете, может быть, у кого-то еще была такая же проблема. Гугление не нашло вообще никого с этим заболеванием.
Я собирался поискать другой метод, но обнаружил, что у кого-то такая же проблема, как у меня. Но при ближайшем рассмотрении оказалось, что это ошибка tomcat6, и разработчик tomcat попросил его обновиться до tomcat7. В проекте используется tomcat9, и эта проблема давно исправлена.
5. Еще раз проверьте использование стека проекта.
На следующий день я еще немного помедлил, сказав, что проблема не решена.
Просмотр количества экземпляров в проекте
> jmap -hive 19793
num #instances #bytes class name
----------------------------------------------
1: 37494 76896680 [I
2: 25378 20727448 [B
3: 171462 19284664 [C
4: 141175 3388200 java.lang.String
5: 561 2513408 [Ljava.util.concurrent.ConcurrentHashMap$Node;
6: 77525 2480800 java.util.HashMap$Node
7: 38859 2247400 [Ljava.lang.Object;
8: 20021 1761848 java.lang.reflect.Method
9: 14842 1651912 java.lang.Class
10: 51005 1632160 java.util.concurrent.ConcurrentHashMap$Node
11: 18588 1567464 [Ljava.util.HashMap$Node;
12: 29526 1181040 java.util.LinkedHashMap$Entry
13: 13645 764120 java.util.LinkedHashMap
14: 36894 763928 [Ljava.lang.Class;
15: 22800 729600 com.mysql.cj.conf.BooleanProperty
16: 14720 706560 java.util.HashMap
17: 37818 605088 java.lang.Object
18: 18016 432384 java.util.ArrayList
Нани, почему все мои 1,4 миллиона экземпляров StandardSession исчезли? Посмотрите на использование памяти приложением, оно все то же самое, занимает почти 5гб места, что-то не так.
посмотрите на использование стека
> jmap -heap 19793
using thread-local object allocation.
Parallel GC with 18 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
//...省略部分不必要的东西
Heap Usage:
PS Young Generation
Eden Space:
capacity = 3287285760 (3135.0MB)
used = 53116712 (50.656044006347656MB)
free = 3234169048 (3084.3439559936523MB)
1.6158227753220944% used
//...省略部分不必要的东西
PS Old Generation
capacity = 1083703296 (1033.5MB)
used = 62036632 (59.162742614746094MB)
free = 1021666664 (974.3372573852539MB)
5.724503397653226% used
Проанализируйте эту информацию:
-
Eden Space
: Использование кучи молодого поколения-
capacity
: общий размер, текущий размер кучи составляет 3,1 ГБ. -
used
: используемое пространство, в настоящее время используется 50 МБ -
free
: Свободное место на данный момент свободно 3,08 ГБ 使用率为 1.6%
-
-
PS Old Generation
: использование кучи старого поколения-
capacity
: общий размер, текущий размер кучи 1 ГБ -
used
: используемое пространство, в настоящее время используется 59 МБ -
free
: свободное место на данный момент свободно 974 МБ 使用率为 5.7%
-
Почему не освобождается столько свободной памяти и что случилось. Подождите, здесь не упомянуты два важных параметра.
-
Heap Configuration
: информация о конфигурации кучи-
MinHeapFreeRatio
: Минимальное свободное пространство кучи. Если свободное пространство кучи меньше этого значения, JVM выполнит обработку расширения. -
MaxHeapFreeRatio
: максимальное свободное пространство кучи. Если свободное пространство кучи превышает это значение, JVM сожмет пространство кучи.
-
6. Местонахождение проблемы и решение
На данный момент вы знаете проблему.Максимальное свободное отношение кучи равно 100, что означает, что когда уровень использования кучи равен 0%, память кучи будет сжата.Это никогда не будет сжимать память кучи.
Когда JVM выполняет сборку мусора, ненужные экземпляры очищаются, но из-за конфигурации пространство не будет сжиматься, поэтому приложение занимает много места и становится все больше и больше.
Решение состоит в том, чтобы добавить его при запуске-XXMinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=60
.
7. Небольшое расширение
Вот расширение конфигурации двух куч Java в проекте.
- Стабильная куча Java
-Xms равно -Xmx, JVM вначале выделяет наибольшую память кучи, поэтому нет необходимости часто расширять память кучи во время выполнения. Это очень удобно в проектах с высокой пропускной способностью. Нет необходимости часто расширять кучу или выполнять частую обработку сборки мусора, что может уменьшить количество сборок мусора и общее время.
-Xms 和 -Xmx 相等时
, конфигурация MinHeapFreeRatio и MaxHeapFreeRatio будет недействительной. (Это не требует динамического расширения размера кучи, даже если это настроено)
- Турбулентная куча Java
Если обработка не выполняется, JVM настроит этот режим по умолчанию, то есть -Xms изначально является относительно небольшим значением.Когда системе требуется больше места в куче, она будет увеличивать размер кучи до тех пор, пока -Xms не станет равным - Хмкс
Суммировать
На данный момент это событие «утечки памяти» закончилось, на самом деле это не утечка памяти.
В начале проблема была локализована неправильно, я думал, что это причина Tomcat, я также намеренно пошел, чтобы понять механизм управления сессиями Tomcat и реализацию кода. К счастью, я нашел проблему позже, и я не тратил слишком много времени в неправильном направлении, иначе я не мог бы найти конкретную причину, если бы я перевернул исходный код Tomcat.
Кроме того, почему 1,4 миллиона экземпляров StandardSession просрочены, но не выпущены, это связано с тем, что системной памяти все еще относительно достаточно, и эти экземпляры были переведены в старое поколение после нескольких второстепенных GC (срок действия сеанса проекта составляет 5 часов), если не выполнить FullGC, данные в старом поколении не будут отсортированы. На следующий день я обнаружил, что экземпляр был очищен, потому что я запустил команду jmap -dump, которая заставила JVM выполнить FullGC, поэтому бесполезные экземпляры были освобождены.
Справочная статья: