Dynamic Plugin Unload Fails: TimerThread with Global JNI Root Holds PluginClassLoader via AccessControlContext

Our plugin cannot be dynamically unloaded due to a classloader leak that appears to originate from IntelliJ platform internals, not our plugin code.

Memory Leak Pattern:

ROOT: Global JNI
└── java.util.TimerThread
    └── inheritedAccessControlContext: java.security.AccessControlContext  
        └── context: java.security.ProtectionDomain[]
            └── ProtectionDomain
                └── classloader: com.intellij.ide.plugins.cl.PluginClassLoader

Logs:

2025-08-27 15:24:42,915 [  46664]   INFO - #c.i.o.p.UnindexedFilesScannerExecutor - [3e6fb5e] Task finished (scanning id=1): com.intellij.util.indexing.UnindexedFilesScannerExecutorImpl$ScheduledScanningTask@58f1f07
2025-08-27 15:24:42,915 [  46664]   INFO - #c.i.u.i.d.ProjectIndexingDependenciesService - Complete token: com.intellij.util.indexing.dependencies.IncompleteTaskToken@28d85479, successful: true
2025-08-27 15:24:43,462 [  47211]   INFO - #c.i.i.p.DynamicPlugins - Snapshot analysis result: Root 1:
  ROOT: Global JNI
  (root): java.util.TimerThread
  inheritedAccessControlContext: java.security.AccessControlContext
  context: java.security.ProtectionDomain[]
  []: java.security.ProtectionDomain
* classloader: com.intellij.ide.plugins.cl.PluginClassLoader


2025-08-27 15:24:43,498 [  47247]   INFO - #c.i.i.p.DynamicPlugins - Plugin dev.sweep.assistant is not unload-safe because class loader cannot be unloaded. Memory snapshot created at /Users/kevinlu/unload-dev.sweep.assistant-27.08.2025_15.24.40.hprof
2025-08-27 15:24:43,545 [  47294]   INFO - #c.i.o.p.DumbServiceImpl - enter dumb mode [autocomplete-playground]
2025-08-27 15:24:43,551 [  47300] SEVERE - #c.i.i.p.PluginManager - getService(...) must not be null
java.lang.NullPointerException: getService(...) must not be null
2025-08-27 15:24:43,551 [  47300] SEVERE - #c.i.i.p.PluginManager - IntelliJ IDEA 2025.1  Build #IC-251.23774.435

Key Details:

  • The TimerThread has no clear parent caller or owner in the reference chain
  • The leak appears to be caused by IntelliJ’s internal systems (image loading, icon caching) capturing our plugin’s security context during background operations
  • Our plugin follows all documented best practices for dynamic plugin support and properly implements Disposable
  • All plugin services are correctly disposed, but IntelliJ internals still hold references

Environment:

  • The leak prevents dynamic plugin reloading and requires IDE restart

This appears to be a platform issue where IntelliJ’s internal Timer/background systems capture plugin security contexts, but no documented solution exists for this specific leak pattern.

Also posted on YouTrack: https://youtrack.jetbrains.com/issue/IJPL-204652/Dynamic-Plugin-Unload-Fails-TimerThread-with-Global-JNI-Root-Holds-PluginClassLoader-via-AccessControlContext

1 Like