Как обнаружить и удалить (во время сеанса) неиспользуемые bean-компоненты @ViewScoped, которые не могут быть удалены сборщиком мусора

EDIT: проблема, поднятая этим вопросом, очень хорошо объяснена и подтверждена в этой статье codebulb.ch, включая некоторое сравнение между JSF @ViewScoped, CDI @ViewSCoped и Omnifaces @ViewScoped, а также четкое заявление о том, что JSF @ViewScoped является «дырявым по дизайну» : 24 мая 2015 г. Java Сравнение областей действия компонентов EE 7, часть 2 из 2


РЕДАКТИРОВАТЬ: 2017-12-05 Тестовый пример, использованный для этого вопроса, по-прежнему чрезвычайно полезен, однако выводы, касающиеся сборки мусора в исходном сообщении (и изображениях), были основаны на JVisualVM, и с тех пор я обнаружил, что они недействительны. . Вместо этого используйте профилировщик NetBeans! Теперь я получаю полностью согласованные результаты для OmniFaces ViewScoped с тестовым приложением при принудительном сборе мусора из профилировщика NetBeans вместо JVisualVM, подключенного к GlassFish/Payara, где я все еще получаю ссылки удерживается (даже после вызова @PreDestroy) полем sessionListeners типа com.sun.web.server.WebContainerListener в пределах ContainerBase$ContainerBackgroundProcessor, и они не будут выполнять сборку мусора.


Известно, что в JSF2.2 для страницы, которая использует bean-компонент @ViewScoped, переход от него (или его повторная загрузка) с использованием любого из следующих методов приведет к тому, что экземпляры bean-компонента @ViewScoped «подвиснут» в сеансе, поэтому что он не будет собирать мусор, что приведет к бесконечному увеличению памяти кучи (пока спровоцировано GET):

  • Использование ссылки h: для ПОЛУЧЕНИЯ новой страницы.

  • Использование h:outputLink (или HTML-тега A) для ПОЛУЧЕНИЯ новой страницы.

  • Перезагрузка страницы в браузере с помощью команды или кнопки RELOAD.

  • Перезагрузка страницы с помощью клавиши ENTER на URL-адресе браузера (также GET).

Напротив, прохождение через навигационную систему JSF с использованием, скажем, h:commandButton приводит к выпуску bean-компонента @ViewScoped, который может быть удален сборщиком мусора.

Это объясняется (от BalusC) в JSF 2.1. Метод ViewScopedBean @PreDestroy не вызывается и продемонстрирован для JSF2.2 и Mojarra 2.2.9 в моем небольшом примере проекта NetBeans по адресу https://stackoverflow.com/a/30410401/679457, этот проект иллюстрирует различные случаи навигации и является доступно для скачивания здесь. (EDIT: 2015-05-28: полный код теперь также доступен здесь ниже.)

[EDIT: 2016-11-13 Теперь также есть улучшенное тестовое веб-приложение с полными инструкциями и сравнением с OmniFaces @ViewScoped и таблицей результатов на GitHub: https://github.com/webelcomau/JSFviewScopedNav]

Я повторяю здесь изображение index.html, в котором обобщаются варианты навигации и результаты для кучи памяти:

введите здесь описание изображения

В: Как я могу обнаружить такие "висячие/висячие" bean-компоненты @ViewScoped, вызванные навигацией GET, и удалить их или иным образом сделать их сборщиком мусора?

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



person Webel IT Australia - upvoter    schedule 23.05.2015    source источник
comment
window.onbeforeunload. Я имею в виду это для OmniFaces 2.2 @ViewScoped.   -  person BalusC    schedule 23.05.2015
comment
@BalusC Спасибо, я обязательно попробую ваш OmniFaces2.2 ViewScoped (понимаю, что вы сейчас на 2.1-RC2).   -  person Webel IT Australia - upvoter    schedule 28.05.2015
comment
Вы правы: нет причин для вызова обработчика: запросы GET не должны возвращаться на сервер, и в результате никакие компоненты на стороне сервера не будут запускаться. Только ajax, как намекнул BalusC, может выполнить эту работу. Я попробую что-нибудь и дам образец   -  person kolossus    schedule 28.05.2015
comment
Простой тестовый проект, который я продемонстрировал здесь, предназначен, конечно, только для исследования этой проблемы в большом веб-приложении, которое активно использует ViewScoped и в настоящее время страдает от проблем с памятью (при определенных обстоятельствах). Учитывая очевидный интерес сообщества JSF к недавнему решению проблемы, связанной с тем, что bean-компоненты ViewScoped никогда не освобождаются в конце сеанса (java.net/jira/browse/JAVASERVERFACES-2561, теперь решена в последней версии Mojarra) Я подозреваю, что эта проблема, описанная здесь, также представляет большой интерес, поэтому, пожалуйста, продолжайте настаивать, любые предложения приветствуются.   -  person Webel IT Australia - upvoter    schedule 29.05.2015
comment
@BalusC Новое тестовое веб-приложение, сравнивающее другие формы компонентов JSF @ViewScoped с OmniFaces 2.5.1 здесь github.com/webelcomau/JSFviewScopedNav и связанный вопрос, связанный с OmniFaces, с таблицами результатов: -be-gar">JSF: Mojarra против OmniFaces @ViewScoped: @PreDestroy вызывается, но bean-компонент не может быть удален сборщиком мусора   -  person Webel IT Australia - upvoter    schedule 13.11.2016


Ответы (2)


По сути, вы хотите, чтобы состояние представления JSF и все bean-компоненты с областью представления были уничтожены во время выгрузки окна. Решение было реализовано в аннотации OmniFaces @ViewScoped, которая подробно описана в его документации, как показано ниже:

Могут быть случаи, когда желательно немедленно уничтожить bean-компонент с областью видимости, а также при вызове события браузера unload. т.е. когда пользователь уходит с помощью GET или закрывает вкладку/окно браузера. Ни одна из аннотаций области представления JSF 2.2 не поддерживает это. Начиная с OmniFaces 2.2, эта аннотация области представления CDI гарантирует, что аннотированный метод @PreDestroy также вызывается при выгрузке браузера. Этот трюк выполняется с помощью синхронного запроса XHR через автоматически подключаемый вспомогательный скрипт omnifaces:unload.js. Однако есть небольшая оговорка: в медленной сети и/или слабом серверном оборудовании может быть заметная задержка между действием конечного пользователя по выгрузке страницы и желаемым результатом. Если это нежелательно, то лучше придерживаться собственных аннотаций области представления JSF 2.2 и принять отложенное уничтожение.

Начиная с OmniFaces 2.3, выгрузка была дополнительно улучшена, чтобы также физически удалить связанное состояние представления JSF из внутренней карты LRU реализации JSF в случае сохранения состояния на стороне сервера, тем самым еще больше снижая риск на ViewExpiredException для других представлений, которые были созданы/открыты ранее . В качестве побочного эффекта этого изменения аннотированный метод @PreDestroy любого стандартного компонента JSF с областью видимости, на который ссылается тот же вид, что и компонент OmniFaces CDI с областью видимости, также будет гарантированно вызываться при выгрузке браузера.

Вы можете найти соответствующий исходный код здесь:

Скрипт выгрузки запустится во время события beforeunload окна, если только оно не вызвано отправкой формы на основе JSF (ajax). Что касается отправки командной ссылки и/или ajax, это зависит от реализации. В настоящее время распознаются Mojarra, MyFaces и PrimeFaces.

Сценарий выгрузки будет триггер navigator.sendBeacon в современных браузерах и вернуться к синхронному XHR (асинхронный вызов завершится ошибкой, поскольку страница может быть выгружена раньше, чем запрос действительно попадет на сервер).

var url = form.action;
var query = "omnifaces.event=unload&id=" + id + "&" + VIEW_STATE_PARAM + "=" + encodeURIComponent(form[VIEW_STATE_PARAM].value);
var contentType = "application/x-www-form-urlencoded";

if (navigator.sendBeacon) {
    // Synchronous XHR is deprecated during unload event, modern browsers offer Beacon API for this which will basically fire-and-forget the request.
    navigator.sendBeacon(url, new Blob([query], {type: contentType}));
}
else {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", url, false);
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    xhr.setRequestHeader("Content-Type", contentType);
    xhr.send(query);
}

Обработчик представления выгрузки будет явно уничтожить все компоненты @ViewScoped, включая стандартные JSF (обратите внимание, что сценарий выгрузки инициализируется только тогда, когда представление ссылается хотя бы на один компонент OmniFaces @ViewScoped).

context.getApplication().publishEvent(context, PreDestroyViewMapEvent.class, UIViewRoot.class, createdView);

Однако это не разрушает физическое состояние просмотра JSF в сеансе HTTP и, следовательно, приведенный ниже вариант использования потерпит неудачу:

  1. Установите количество физических представлений равным 3 (в Mojarra используйте параметр контекста com.sun.faces.numberOfLogicalViews, а в MyFaces используйте параметр контекста org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION).
  2. Создайте страницу, которая ссылается на стандартный компонент JSF @ViewScoped.
  3. Откройте эту страницу во вкладке и держите ее открытой все время.
  4. Откройте ту же страницу в другой вкладке и сразу же закройте эту вкладку.
  5. Откройте ту же страницу в другой вкладке и сразу же закройте эту вкладку.
  6. Откройте ту же страницу в другой вкладке и сразу же закройте эту вкладку.
  7. Отправьте форму на первой вкладке.

Это не удастся с ViewExpiredException, потому что состояния представления JSF ранее закрытых вкладок физически не уничтожаются во время PreDestroyViewMapEvent. Они все еще остаются на сеансе. OmniFaces @ViewScoped фактически уничтожит их. Однако разрушение состояния представления JSF зависит от реализации. Это объясняет, по крайней мере, довольно хакерский код в классе Hacks, который должен достичь этого.

Интеграционный тест для этого конкретного случая можно найти в ViewScopedIT#destroyViewState() на ViewScopedIT.xhtml, который равен в настоящее время работает с WildFly 10.0.0, TomEE 7.0.1 и Payara 4.1.1.163.


В двух словах: просто замените javax.faces.view.ViewScoped на org.omnifaces.cdi.ViewScoped. Остальное прозрачно.

import javax.inject.Named;
import org.omnifaces.cdi.ViewScoped;

@Named
@ViewScoped
public class Bean implements Serializable {}

По крайней мере, я попытался предложить общедоступный метод API для физического уничтожения состояния представления JSF. Возможно, это появится в JSF 2.3, и тогда я смогу устранить шаблон в классе Hacks OmniFaces. Как только все будет отполировано в OmniFaces, возможно, в конечном итоге оно войдет в JSF, но не раньше 2.4.

person BalusC    schedule 11.11.2016
comment
Спасибо за ваш подробный ответ. Я возобновляю расследование этого и испытания с последними версиями OmniFaces, по крайней мере, в изолированном тестовом приложении. Миграция моего производственного приложения путем замены javax.faces.view.ViewScoped на org.omnifaces.cdi.ViewScoped — это важное решение, так как потребуется много тестов, чтобы увидеть, есть ли какие-либо другие побочные эффекты. Насколько я могу судить, он не попал в JSF2.3. В: Есть ли у вас какие-либо новости о том, планируется ли это все еще для официального JSF (и если да, то когда это, вероятно, будет доступно)? [Что не означает, что я действительно не буду использовать версию OmniFaces.] - person Webel IT Australia - upvoter; 04.12.2017
comment
Я обязательно рассмотрю это для JSF.next. По крайней мере, я могу сказать, что несколько производственных приложений, над которыми я работал, очень выиграли от этого. В одном конкретном веб-приложении, где интенсивно использовался собственный JSF @ViewScoped, использование памяти даже уменьшилось на 80%. - person BalusC; 04.12.2017
comment
Спасибо за это обнадеживающее утверждение, я возобновляю оценку org.omnifaces.cdi.ViewScoped в специальном тестовом приложении с таблицей результатов -but-bean-cant-be-gar">описано здесь (где я отметил, что @PreDestroy вызывается в большинстве случаев, но по какой-то причине спровоцированная сборка мусора не работает, может быть из-за чего-то еще). Я также попробую это в ветке/форке моего основного веб-приложения (после некоторых измерений использования памяти). Я приму ваш ответ, как только увижу сборку мусора. - person Webel IT Australia - upvoter; 05.12.2017
comment
Огромный прогресс (и ответ полностью принят)! Теперь я получаю полностью согласованные результаты с моим тестовым приложением при принудительном сборке мусора, всегда оставляя только 1 компонент представления на основе omnifaces (для 1 открытой вкладки браузера) после принудительного сбора мусора, когда я использую NetBeans8.2 Profiler (встроенный) вместо JVisualVM, подключенного к GlassFish/Payara, где я получаю ссылки, все еще удерживаемые (даже после вызова @PreDestroy) по полю sessionListeners типа com.sun.web.server.WebContainerListener в ContainerBase$ContainerBackgroundProcessor, и они не будут GC. - person Webel IT Australia - upvoter; 05.12.2017
comment
Подтверждение PreDestroy всегда вызывается и может выполнять GC для всех случаев навигации с помощью OmniFaces-2.6.6 @ViewScoped, полная последовательность результатов с таблицами сравнения при ответе на собственный вопрос здесь. - person Webel IT Australia - upvoter; 05.12.2017

Итак, я кое-что насобирал.

Принцип

Сейчас неактуальные bean-компоненты с областью видимости сидят там, тратя впустую время и пространство каждого, потому что в случае навигации GET, используя любой из выделенных вами элементов управления, сервер не участвует. Если сервер не задействован, он не может знать, что bean-компоненты с областью видимости теперь избыточны (то есть до тех пор, пока сеанс не будет завершен). Итак, здесь нужен способ сообщить серверной стороне, что представление, из которого вы осуществляете навигацию, должно завершить свои bean-компоненты в области представления.

Ограничения

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

  1. beforeunload или unload в <h:body/> было бы идеально, если бы не следующие проблемы

  2. Вы не можете отправить запрос ajax в onclick элемента управления, а также перемещаться в том же элементе управления. Во всяком случае, не без грязного всплывающего окна. Таким образом, навигация onclick в h:button или h:link невозможна.

Грязный компромисс

Инициируйте запрос ajax onclick, и пусть PhaseListener выполнит реальную навигацию и очистку области видимости.

Рецепт

  1. 1 PhaseListener (здесь также подойдет ViewHandler; я использую первый, потому что его проще настроить)

  2. 1 оболочка вокруг JSF js API

  3. Средняя порция стыда

Посмотрим:

  1. PhaseListener

    public ViewScopedCleaner implements PhaseListener{
    
        public void afterPhase(PhaseEvent evt){
             FacesContext ctxt = event.getFacesContext();
             NavigationHandler navHandler = ctxt.getApplication().getNavigationHanler();
             boolean isAjax =  ctx.getPartialViewContext().isAjaxRequest(); //determine that it's an ajax request
             Object target = ctxt.getExternalContext().getRequestParameterMap().get("target"); //get the destination URL
    
                    if(target !=null && !target.toString().equals("")&&isAjax ){
                         ctxt.getViewRoot().getViewMap().clear(); //clear the map
                         navHandler.handleNavigation(ctxt,null,target);//navigate
                     }
    
        }
    
        public PhaseId getPhaseId(){
            return PhaseId.APPLY_REQUEST_VALUES;
        }
    
    }
    
  2. JS-оболочка

     function cleanViewScope(){
      jsf.ajax.request(this, null, {execute: 'someButton', target: this.href});
       return false;
      }
    
  3. Собираем это вместе

      <script>
         function cleanViewScope(){
             jsf.ajax.request(this, null, {execute: 'someButton', target: this.href}); return false;
          }
      </script>  
    
     <f:phaseListener type="com.you.test.ViewScopedCleaner" />
     <h:link onclick="cleanViewScope();" value="h:link: GET: done" outcome="done?faces-redirect=true"/>
    

Задачи

  1. Расширьте h:link, возможно, добавьте атрибут для настройки поведения очистки

  2. Способ передачи целевого URL вызывает подозрения; может открыть дыру

person kolossus    schedule 24.05.2015
comment
@kolussus Спасибо за ваш ответ (и за другие ваши вклады здесь, в stackoverflow). Приятно напомнить о возможности ConfigurableNavigationHandler, но, как показано (с парой незначительных исправлений опечаток), похоже, что она не работает. Часть очистки карты просмотра вызывается только при !isPostback, но ни в одном из методов навигации GET функция handleNavigation() никогда не вызывается. Многочисленные bean-компоненты ViewScoped по-прежнему остаются в куче и не могут быть собраны мусором ни для одного из методов навигации GET. Теперь я включил полный код текущего теста в свой вопрос. - person Webel IT Australia - upvoter; 28.05.2015
comment
Ваш javascript имеет неправильный формат «jsf.ajax.request(this, null, {this,event, {target:this.href});». Со ссылкой на jsDoc для jsf.ajax.request(source,event,options) Я попробовал "jsf.ajax.request(this, null, {target:this.href});' но this.href является «неопределенным» и выдает ошибку JS «typeerror undefined не является функцией (оценка «context.element.hasAttribute (type))». Спасибо за ваши усилия, но, пожалуйста, сначала проверьте свой код и ответы, ни один ответ еще не сработал. - person Webel IT Australia - upvoter; 14.06.2015
comment
Это опечатка @WebelITAustralia; вызвано повторным набором кода на моем телефоне; тот, который любой, у кого есть базовый текстовый редактор, сможет обнаружить и исправить. Это не отменяет принципа, лежащего в основе ответа, и я не вижу, чтобы кто-то еще пытался. Вы можете игнорировать ответ/предложение; это ваша прерогатива. - person kolossus; 14.06.2015
comment
Я понял проблему (до того, как вы ответили), когда я использую этот встроенный 'onclick=jsf.ajax.request(this, null, {target:this.href}); вернуть ложь;' он работает (вызывает прослушиватель фазы с isAjax true), контекст для «this» и «this.href» неверен при помещении в функцию JS: cleanViewScope. Эта часть вашего комментария совершенно не нужна, я не вижу, чтобы кто-то еще пытался. Вы можете игнорировать ответ/предложение; это ваша прерогатива и защита. Даже когда вы добровольно помогаете бесплатно, ввод ответов вне IDE (во всем вашем коде были ошибки) тратит ваше и мое время. - person Webel IT Australia - upvoter; 14.06.2015
comment
Я даю вашему ответу 1, но на самом деле он не полностью отвечает на вопрос, который я задал. Как обнаружить и удалить (во время сеанса) неиспользуемые bean-компоненты @ViewScoped, которые не могут быть собраны мусором, потому что он улавливает только некоторые случаи. Несмотря на то, что я большой поклонник JSF, это, безусловно, серьезный недостаток технологии, поскольку массивный рост кучи памяти в качестве побочного эффекта простого нажатия на навигационные ссылки или кнопки нелеп и является серьезной головной болью для моего проекта. Окончательное решение, возможно, связано с тем, что над ним работает команда JSF. Все остальные предложения по другим стратегиям приветствуются. - person Webel IT Australia - upvoter; 14.06.2015
comment
Если кто-то еще придет и увидит, что я отметил ваш текущий ответ как +1 (правильный), а затем слепо скопирует его в свою IDE, они обнаружат: 1. Что есть несколько простых опечаток переменных (IDE предупредит их об этом) , легко фиксируется; 2. То, что это не работает, потому что функция JavaScript написана неправильно (даже если кто-то должен уметь читать между строк и исправлять). Поэтому, пожалуйста, отредактируйте свой ответ и исправьте функцию JavaScript, чтобы она работала (я также проверю ее снова в конце), чтобы на этот раз справедливо заработать балл, даже с вашей репутацией. - person Webel IT Australia - upvoter; 14.06.2015
comment
Кажется, вы отредактировали его (спасибо за это), но он все еще не работает с этим сейчас в функции JS 'jsf.ajax.request(this, null, {execute: 'someButton', target: this.href}); '. Чтобы ответ был правильным (работал правильно), он должен показать, на что ссылается 'showButton' и как. Как написано, он не вызывает PhaseListener из h:link, как показано. - person Webel IT Australia - upvoter; 14.06.2015
comment
Я понимаю @WebelITAustralia. Пожалуйста, отзовите свой голос. То, что здесь работает для меня в настройке JSF2.2/glassfish-4/Chrome. Если вы хотите обсудить особенности того, что вы видите в консоли JS/браузера, это можно сделать в чате. - person kolossus; 14.06.2015
comment
«Пожалуйста, отзовите свой голос». Почему ? Голосование за «Этот ответ полезен», а не за 100% правильный ответ (который работает). Вы пишете, что «то, что здесь работает для меня», но в сценарии JS, как указано в вашем ответе, это относится к «выполнить: someButton», в то время как под ссылкой h: у вас нет ссылки на someButton, у вас есть «h: ссылка onclick = cleanViewScope (); значение = h: ссылка: ПОЛУЧИТЬ: сделано, результат = сделано? Faces-redirect = true '. Опять же, я не могу проголосовать за правильный (в отличие от полезного) ответ, если код, указанный в ответе, не работает. Пожалуйста, проверьте, что у вас есть в вашем собственном тесте против него. - person Webel IT Australia - upvoter; 15.06.2015
comment
И JavaScript, который у вас есть в вашем текущем ответе на стек, имеет 'function cleanViewScope () {jsf.ajax.request (this, null, {execute: 'someButton', target: this.href}); вернуть ложь; }", где "this.href" равно "undefined", вероятно, из-за неправильного контекста, "this" к моменту вызова имеет значение "[object Window]". Вы написали: То, что здесь работает для меня на установке JSF2.2/glassfish-4/Chrome. . Это та же самая установка, что и у меня, и я не вижу, как ваш текущий ответ в том виде, в котором он написан, может работать с ‹h:link onclick=cleanViewScope(); value=h:link: GET: done result=done?faces-redirect=true'›. - person Webel IT Australia - upvoter; 15.06.2015
comment
Проблема, поднятая этим вопросом, очень хорошо объяснена и подтверждена в этой статье codebulb.ch, включая некоторое сравнение с Omnifaces @ViewScoped и четкое заявление о том, что JSF @ViewScoped является «дырявым по дизайну»: 24 мая 2015 г. Сравнение областей Java EE 7 Bean, часть 2 из 2 - person Webel IT Australia - upvoter; 11.11.2016