Помните о процессе устранения «утечки памяти»

JVM

поиск проблемы

Сегодня я обнаружил, что онлайн-приложение использует очень много памяти, но очень низкое использование ЦП.

использоватьpsКомандование, вы видите, что процесс 19793 занимает 4,9 ГБ памяти, но его загрузка процессора составляет менее 5%, есть проблема.

# ps -aux | grep 19793
user     19793  1.6  9.9 23864228 4904664 ?    Sl   Oct03 268:52 

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

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

  1. Используйте jmap, чтобы увидеть, какие объекты имеют большое количество объектов и занимают много памяти.
  2. Анализировать файлы дампа и использование кучи
  3. Найдите вызывающий процесс определенных классов и связанных кодов и шаг за шагом найдите проблему.

Использование и знакомство с инструментом здесь повторяться не буду, приведу цитату блогерастатья

Расположение проблемы и устранение неполадок

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, поэтому бесполезные экземпляры были освобождены.

Справочная статья: