Skip to content

WW-5537 Resolve classloader/memory leaks during Tomcat hot deployment#1632

Open
lukaszlenart wants to merge 17 commits intomainfrom
WW-5537-classloader-leak-fixes
Open

WW-5537 Resolve classloader/memory leaks during Tomcat hot deployment#1632
lukaszlenart wants to merge 17 commits intomainfrom
WW-5537-classloader-leak-fixes

Conversation

@lukaszlenart
Copy link
Member

Summary

Port of #1631 (Struts 6) to Struts 7, with Java 17 improvements.

Fixes classloader/memory leaks that cause OutOfMemoryError (Metaspace) during Tomcat hot redeployment (WW-5537):

  • Introduces InternalDestroyable / ContextAwareDestroyable interfaces with container-based discovery so static caches, daemon threads, and shared references are cleaned up when Dispatcher.cleanup() runs
  • Clears OGNL, Component, ScopeInterceptor, DefaultFileManager, DebugUtils, FreeMarker, and JSON plugin caches
  • Stops the FinalizableReferenceQueue daemon thread with proper join() and classloader cleanup
  • Replaces ContainerHolder plain ThreadLocal with generation-counter pattern (AtomicLong) for safe cross-thread invalidation during undeploy

Java 17 improvements over Struts 6 (Java 8) version

Pattern Struts 6 (Java 8) Struts 7 (Java 17)
Generation counter volatile long with non-atomic ++ AtomicLong (thread-safe)
CachedContainer Inner class record
Type checks Old-style casts Pattern matching instanceof

Changes

Area What
Core InternalDestroyable / ContextAwareDestroyable interfaces
Core 6 adapter classes: Component, OGNL, ScopeInterceptor, FinalizableReferenceQueue, FreeMarker, DebugUtils destroyables
Core ContainerHolder ThreadLocal → ThreadLocal + AtomicLong generation counter
Core Dispatcher.cleanup() refactored into focused destroy methods with container discovery
Core FinalizableReferenceQueue: volatile instance, join(5000), setContextClassLoader(null)
Core ScopeInterceptor.clearLocks(): synchronized block for thread safety
Core PrepareOperations: per-request ContainerHolder.clear()
JSON plugin JSONCacheDestroyable registered in struts-plugin.xml
Showcase log4j-web for proper Log4j2 lifecycle in servlet container

Test plan

  • DispatcherCleanupTest — 8 tests verifying InternalDestroyable discovery and all static caches cleared after dispatcher.cleanup()
  • ContainerHolderTest — 4 tests verifying store/get, clear, invalidateAll cross-thread and same-thread
  • Full core test suite: 2903 tests passing
  • JSON plugin test suite: passing
  • Manual verification: deploy WAR on Tomcat, redeploy 5+ times, confirm no Metaspace growth

🤖 Generated with Claude Code

lukaszlenart and others added 12 commits March 23, 2026 10:16
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…er null

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…estroyable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…alDestroyable discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e pattern matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lukaszlenart lukaszlenart force-pushed the WW-5537-classloader-leak-fixes branch from b56ea78 to 9097c07 Compare March 23, 2026 09:16
lukaszlenart and others added 2 commits March 23, 2026 10:18
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Container now exposes a destroy() method that clears factories, injectors,
constructors, and ThreadLocals. This releases Class<?> keys and JDK
DelegatingClassLoader instances that pin the webapp classloader.

DefaultConfiguration.destroy() calls container.destroy() and
reloadContainer() delegates to destroy() to avoid duplication.

Also fixes JSONCacheDestroyable referencing non-existent DefaultJSONWriter
(renamed to StrutsJSONWriter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lukaszlenart lukaszlenart force-pushed the WW-5537-classloader-leak-fixes branch from 20b8476 to 1296e4f Compare March 23, 2026 15:30
lukaszlenart and others added 2 commits March 23, 2026 18:33
…om reloadContainer

factories must remain intact because existing code holds direct
references to the Container after destroyConfiguration() and expects
it to still resolve dependencies (e.g. during configuration reload).

reloadContainer() reverted to clearing packageContexts/loadedFileNames
directly — calling destroy() there nulled the container reference and
cleared state needed during the bootstrap transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test failures were caused by factories.clear() in
Container.destroy(), not by calling destroy() from reloadContainer().
Now that factories.clear() is removed, destroy() is safe to call
here — it clears packageContexts, loadedFileNames, and the container's
reflection caches in one place.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lukaszlenart lukaszlenart marked this pull request as ready for review March 23, 2026 17:49
…y method comments

- Replace volatile field with AtomicReference in FinalizableReferenceQueue
  for proper thread safety using getAndSet()
- Add comments to empty destroy() implementations in test mocks
- Replace deprecated new URL() with URI.toURL() in DispatcherCleanupTest
- Add comments to empty listener methods in DispatcherCleanupTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant