Paul,
One thing that I noticed is that your HouseKeeper is using its own scheduler, rather than using one that it obtains from the server. Any chance you can change that and run again and see if the problem disappears?
As you can see from the HouseKeeper code, every time the scavenge cycle completes, it schedules another cycle iff the scheduler is still running. See line 72. It does that in a finally block, so even if the scavenge threw some kind of exception/error, the next scavenge should be scheduled. You may have to checkout and build the jetty 9.4.x branch locally and try putting in a catch and/or some printlns to see what is going on, although I would have expected some log output if there was an error.
You also say the app is keeping its own list of sessions - is that a full reference to the session or just the session id? If the former, I wouldn't recommend you do that, as the management of the lifecycle of that object is not under the control of the app, the container needs to manage it.
The server dump from your original email looks a little odd - I don't see the normal dump for the 2 webapps /bc and /bdm - I would expect to see a ContextHandlerCollection and a normal dump of the 2 contexts. That would also show the configuration setup for the SessionHandler with the default session maxIdleTime.
Another thing to look into is the Session objects themselves. After this problem happens, do you see log lines like "Timer expired for session for session xyz"? Or ""Inspecting session xyz, valid=true"? Or "Session xyz is candidate for expiry"? What about " Expiring xyz" ? Can you look at the heap dump for the SessionHandler._candidateSessionIdsForExpiry and see if it has any entries for expired sessions? What are the values for the fields of the sessions that you say are still in the DefaultSessionCache?
regards
Jan