задний план
Два дня назад в проекте была обнаружена ошибка. мы используемRocketMQ
, который будет создан после запуска сервисаMQ
потребительский экземпляр. В ходе тестирования было обнаружено, что после запуска службы в течение определенного периода времениRocketMQ
Соединение будет разорвано, так что отношения подписки не могут быть найдены, а данные не могут быть отслежены.
1. Возникновение багов
После обратного отслеживания кода я обнаружил, что логика подписки такая. будетConsumerStarter
Класс зарегистрирован в Spring и переданPostConstruct
Аннотация запускает метод инициализации, полныйMQ
Создание и подписка потребителей.
в коде вышеSubscriber
Класс — это инструментальный класс, написанный коллегой, который инкапсулирует некоторые соединения.RocketMQ
Информация о конфигурации вызывается здесь при использовании. Здесь нет ничего сложного, это просто связьRocketMQ
, завершите создание и подписку.
1. доработать
Приведенный выше код выглядит обыденным, но на самом деле он переписываетfinalize
метод. и выполнен внутриconsumer.shutdown()
,будетRocketMQ
Отключено, вот триггер.
finalize
даObject
метод в . Вызывается, когда GC (сборщик мусора) решает удалить объект, на который не ссылаются другие объекты. переопределение подклассаfinalize
способ распоряжаться системными ресурсами или нести ответственность за операции по очистке.
Вернувшись в проект, он написал так вSubscriber
Когда класс переработан, отключитеRokcketMQ
соединение, что приводит к ошибке. Самый простой способ - поставитьshutdown
Этот код удален, но это не похоже на хорошее решение.
2. Почему он перерабатывается?
В модели памяти Java существует虚拟机栈
, который является частным потоком.
Стек виртуальной машины является частным для потока. Каждый раз, когда создается поток, виртуальная машина создает стек виртуальной машины для потока. Стек виртуальной машины представляет собой модель памяти для выполнения метода Java. Каждый раз, когда вызывается метод, для каждого метода генерируется стек Фрейм (Stack Frame), используемый для хранения таблицы локальных переменных, стека операндов, динамической ссылки, выхода метода и другой информации. Процесс вызова и завершения каждого метода соответствует процессу отправки и извлечения фрейма стека из стека виртуальной машины. Жизненный цикл стека виртуальной машины такой же, как у потока.
надConsumerStarter.init()
метод,Subscriber subscriber = new Subscriber()
определяется как локальная переменная после выполнения метода,subscriber
Переменная не имеет ссылки и затем проверяется сборщиком мусора.
Скоро у меня появятся новые идеи, будуSubscriber
определяется какConsumerStarter
Переменные-члены в классах также в порядке, потому чтоConsumerStarter
зарегистрированSpring
середина. В течение жизненного цикла боба он не будет перерабатываться.
Как указано выше, поместитеsubscriber
Область действия переменной относится к уровню класса, что оказывается вполне приемлемым.
Лучшим решением является использованиеSubscriber
Занятия регистрируются непосредственно наSpring
в, поPostConstruct
Аннотация запускает инициализацию для завершения создания и подписки MQ;PreDestroy
Аннотация завершает выпуск ресурсов. Таким образом, создание и уничтожение ресурсов привязано к жизненному циклу Бина, и проблем не возникает.
Пока что причина и решение этой ошибки есть. Но есть еще одна проблема, которую я тогда не понимал.
2. Потоки и сборка мусора
Чтобы определить, какие объекты являются мусором, в Java используется метод анализа достижимости.
Он проходит рядGC roots
Объект ищется в качестве отправной точки. Поиск вниз от этих узлов, путь, пройденный поиском, называется цепочкой ссылок.Когда объект не связан с корнями GC какой-либо цепочкой ссылок, это доказывает, что объект недоступен.
В приведенном выше примере, хотяSubscriber
На класс больше не ссылаются, и его необходимо переработать, но он все ещеRocketMQ
изConsumer
Экземпляр все еще работает в режиме потока, в этом случаеSubscriber
Классы тоже перерабатываются?
Тогда это становится таким вопросом:Если на объект A больше нет ссылок и он удовлетворяет условиям GC, но в нем все еще есть активный объект потока a1, будет ли этот объект A переработан в это время?
Чтобы проверить эту проблему, автор провел тест.
1. Протестируйте один
Вот ещеSubscriber
Возьмите класс в качестве примера, запустите для него поток и посмотрите, будет ли он по-прежнему подвергаться сборке мусора. Упрощенный код выглядит следующим образом:
Процесс тестирования:启动服务 > Subscriber类被创建 > 线程执行10次循环 > 手动调用GC
Результат выглядит следующим образом:
15:01:07.110 [main] INFO - Subscriber已启动...
15:01:07.112 [Thread-9] INFO - 执行线程次数:0
15:01:07.357 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
15:01:07.568 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
15:01:07.609 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
15:01:07.614 [main] INFO - Started BootMybatisApplication in 3.654 seconds
15:01:08.114 [Thread-9] INFO - 执行线程次数:1
15:01:09.114 [Thread-9] INFO - 执行线程次数:2
15:01:09.302 [http-nio-8081-exec-2] INFO - 调用GC。。。
15:01:10.122 [Thread-9] INFO - 执行线程次数:3
15:01:11.123 [Thread-9] INFO - 执行线程次数:4
15:01:12.123 [Thread-9] INFO - 执行线程次数:5
15:01:12.319 [http-nio-8081-exec-3] INFO - 调用GC。。。
15:01:13.123 [Thread-9] INFO - 执行线程次数:6
15:01:14.124 [Thread-9] INFO - 执行线程次数:7
15:01:15.125 [Thread-9] INFO - 执行线程次数:8
15:01:15.319 [http-nio-8081-exec-5] INFO - 调用GC。。。
15:01:16.125 [Thread-9] INFO - 执行线程次数:9
15:01:17.775 [http-nio-8081-exec-7] INFO - 调用GC。。。
15:01:17.847 [Finalizer] INFO - finalize-------------Subscriber对象被销毁
По результатам, еслиSubscriber
Если в классе есть активный поток, он не будет перезапущен; после того, как поток завершит работу, GC снова вызывается, и он будет перезапущен.Однако не спешите с выводами, давайте проверим что-нибудь еще.
2. Тест 2
На этот раз мы сначала создадим класс потокаThread1
.
Его метод запуска мы сохраняем таким же, как указано выше. затем вSubscriber
Он вызывается потоком в объекте.
Процесс такой же, как и в тесте 1, и на этот раз результат выглядит следующим образом:
14:59:20.193 [main] INFO - Subscriber已启动...
14:59:20.194 [Thread-9] INFO - 执行次数:0
14:59:20.359 [Finalizer] INFO - finalize-------------Subscriber对象被销毁
14:59:20.444 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
14:59:20.699 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
14:59:20.745 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
14:59:20.751 [main] INFO - Started BootMybatisApplication in 3.453 seconds
14:59:21.197 [Thread-9] INFO - 执行次数:1
14:59:22.198 [Thread-9] INFO - 执行次数:2
14:59:23.198 [Thread-9] INFO - 执行次数:3
14:59:24.198 [Thread-9] INFO - 执行次数:4
14:59:25.198 [Thread-9] INFO - 执行次数:5
14:59:26.198 [Thread-9] INFO - 执行次数:6
14:59:27.198 [Thread-9] INFO - 执行次数:7
14:59:28.199 [Thread-9] INFO - 执行次数:8
14:59:29.199 [Thread-9] INFO - 执行次数:9
Из результатов,Subscriber
после создания, потому чтоConsumerStarter.init()
Метод выполняется и уничтожается, на него вообще не влияет поток.
Эй, это интересно. Логически оба теста проходятnew Thread()
Открыта новая тема, почему результат другой?
Немедленно свяжитесь с нами через WeChatИсходный код ТароВеликий Бог, Великий Бог действительно старомоден Он указал с первого взгляда: Вы должны быть внутренней проблемой класса.
3. Анонимный внутренний класс
Вернемся к коду теста 1.
Если вы создаете подобный поток в методе, вы фактически создаете анонимный внутренний класс. Но есть ли в этом что-то особенное? Это предотвратит нормальную переработку объекта.
Мы знаем, что после того, как внутренний класс будет успешно скомпилирован, он сгенерирует файл класса, который не является тем же файлом класса, что и внешний класс. В приведенном выше коде файл класса создается после компиляции с именем:Subscriber$1.class
.
Декомпилировав программное обеспечение, мы можем получить содержимое этого файла класса следующим образом:
Видя это, это уже очень ясно. Хотя этот внутренний класс не является тем же файлом класса, что и внешний класс, он сохраняет ссылку на внешний класс, то есть на этот.Subscriber this$0
.
Пока метод внутреннего класса не завершен, ссылочный экземпляр внешнего класса будет сохранен, поэтому внешний класс не будет повторно использоваться сборщиком мусора.
Итак, вернемся к исходному вопросу:
Если на объект A больше нет ссылок и он удовлетворяет условиям GC, но в нем все еще есть активный объект потока a1, будет ли этот объект A переработан в это время?
Ответ также положительный, если только объект потока a1 все еще не ссылается на объект A.
3. Анонимные внутренние классы и сборка мусора
Во второй части мы рассмотрим взаимосвязь между потоками и сборкой мусора в анонимных внутренних классах, а затем рассмотрим ситуацию, независимую от потоков.
Я нашел статью в Интернете, когда понял, что проблема сборки мусора была вызвана анонимными внутренними классами. Это также проблема анонимной сборки мусора внутреннего класса, исходная ссылка:gc, связанный с анонимным внутренним классом
Автор также проверил код внутри, и это действительно так, как сказал первоначальный автор:
Из результатов предполагается, что анонимный внутренний класс будет связан с объектами, а объект внутреннего класса не будет переработан, что приведет к сбою повторного использования основного объекта;
Но по факту,Это не проблема с анонимными внутренними классами, или вообще не проблема, этоstatic
Проблема с модификатором.
1. Протестируйте один
Используя анонимные внутренние классы, мы должны наследовать родительский класс или реализовать интерфейс, поэтому мы создаем интерфейс небрежно.
public interface TestInterface {
void out();
}
затем вSubscriber
использовать его во внешних классах.
Затем запустите службу, и вывод будет следующим:
16:37:53.626 [main] INFO - Root WebApplicationContext: initialization completed in 1867 ms
16:37:53.710 [main] INFO - Subscriber已启动...
16:37:53.711 [main] INFO - 匿名内部类测试...
16:37:53.866 [Finalizer] INFO - finalize-------------Subscriber对象被销毁
16:37:53.950 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
16:37:54.180 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
16:37:54.224 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
Из результатов,Subscriber
Объект создается, и после использования нет необходимости вызывать вручнуюGC
, уничтожается.
еслиinterface1
Как насчет переменных, определенных как статические?static TestInterface interface1;
Результат выглядит следующим образом:
16:43:20.331 [main] INFO - Root WebApplicationContext: initialization completed in 1826 ms
16:43:20.404 [main] INFO - Subscriber已启动...
16:43:20.405 [main] INFO - 匿名内部类测试...
16:43:20.673 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
16:43:20.955 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
16:43:21.002 [main] INFO - Tomcat started on port(s): 8081 (http) with context path ''
16:43:21.007 [main] INFO - Started BootMybatisApplication in 3.327 seconds
16:43:33.095 [http-nio-8081-exec-1] INFO - Initializing Servlet 'dispatcherServlet'
16:43:33.102 [http-nio-8081-exec-1] INFO - Completed initialization in 7 ms
16:43:33.140 [http-nio-8081-exec-1] INFO - 调用GC。。。
16:43:35.763 [http-nio-8081-exec-2] INFO - 调用GC。。。
если поставитьinterface1
Определен как статический, как вызватьGC
,Subscriber
не будет переработан.
Как это сделать? допустимыйinterface1
После использования переназначьте его на null.interface1=null;
,В этот момент,Subscriber
Класс будет переработан в обычном режиме, как и в тесте 1.
Однако, почему это? Сборка мусора в Java зависит отGC Roots
Начинает нисходящий поиск, когда объект достигаетGC Roots
Когда нет связанной цепочки ссылок, это доказывает, что объект недоступен, в противном случае он доступен и не подлежит переработке.
GC Roots
Содержит следующие объекты:
- Объекты, на которые есть ссылки в стеке виртуальной машины
- Объект, на который ссылается статическое свойство класса в области метода
- Объект, на который ссылается константа в области метода
- Объекты, на которые ссылается JNI [в общем, Native] в локальном стеке методов
То есть, если мы поместим анонимный внутренний классinterface1
определен как статический объект, он будет рассматриваться какGC Root
Объект существует, в это время внутренний класс поддерживает внешний классSubscriber
ссылка, поэтому она не может быть переработана.
2. Тест 2
зачем говорить,Это не проблема с анонимными внутренними классами или вообще не проблема, это проблема со статическим модификатором, мы можем сделать еще один тест.
Что касается анонимных внутренних классов, нам все равно, как они генерируются или как используются, мы запоминаем только одну из их характеристик:
Он сохранит ссылку на внешний класс.
Итак, давайте избавимся от внутреннего класса и создадим обычный класс, который хранит ссылку на вызывающий класс.
затем во внешнем классеSubscriber
, создайте его экземпляр и вызовите:
Наконец, запустите службу, и вывод будет следующим:
17:30:02.331 [main] INFO - Root WebApplicationContext: initialization completed in 1726 ms
17:30:02.443 [main] INFO - Subscriber已启动...
com.viewscenes.netsupervisor.controller.test.Subscriber@7ea899a9
17:30:02.447 [main] INFO - User对象输出信息....
com.viewscenes.netsupervisor.controller.test.Subscriber@7ea899a9
17:30:02.605 [Finalizer] INFO - finalize-------------Subscriber对象被销毁
17:30:02.606 [Finalizer] INFO - finalize-------------User对象被销毁
17:30:02.676 [main] INFO - Initializing ExecutorService 'applicationTaskExecutor'
17:30:02.880 [main] INFO - Starting ProtocolHandler ["http-nio-8081"]
В этом случае он будет переработан в обычном режиме.
Но если в это времяuser
Объекты определяются как статические свойства, независимо от того, чтоGC
, по-прежнему не может быть переработан.
Отсюда можно сделать следующий вывод:
- Анонимный внутренний класс не препятствует нормальному сбору мусора внешнего класса, но его нельзя определить как ссылку на статическое свойство.
- Статический анонимный внутренний класс, причина, по которой внешний класс не может быть переработан в обычном режиме, заключается в том, что он сохраняет ссылку на внешний класс как объект GC Root.
4. Резюме
Эта статья вызвала проблему с моделью памяти и сборкой мусора из-за небольшой ошибки. Поскольку базовые теоретические знания недостаточно сложны, для объяснения причины всего события можно использовать только различные тесты.
Кроме того, хотя Java помогает нам автоматически собирать мусор, мы должны быть осторожны, чтобы не вызвать утечки памяти при написании кода.