Plugin not unload-safe because class loader cannot be unloaded

Answered

Hello,

I'm developing the HasteIt plugin for IDEA-based IDEs and I'm trying to figure out, why my plugin can't be unloaded correctly. The log show the following message:

> Plugin de.lukweb.hasteit is not unload-safe because class loader cannot be unloaded

My plugin dependens on another Gradle module called ShareBase to share code between two plugins (HasteIt and DiscordBeam), could this cause the class loader problem? Or is there another method to support code sharing and dynamic reloading?

Best regards,
Lukas

12 comments
Comment actions Permalink

I have the same issue. I debugged `runIde` and found that my plugin class loader has tons of referring objects and all of them are from `kotlinx.serializer`. Therefore class loader can't unload and dynamic plugin doesn't work.

0
Comment actions Permalink

Try setting registry option ide.plugins.snapshot.on.unload.fail=true  and set -XX:+UnlockDiagnosticVMOptions in VM options, you'll get memory snapshot to trace why it failed exactly. Please note that 2020.1 contains a number of known platform issues around unloading that can't be fixed on plugin side.

 

See also https://www.jetbrains.org/intellij/sdk/docs/basics/ide_development_instance.html#enabling-auto-reload

3
Comment actions Permalink

Thanks for your help Yann.

It took a little bit of time for me to find the location of memory dumps, that's why I thought it would be good to share this knowledge.

You can find the dumps (example name: `unload-de.lukweb.hasteit-31.05.2020_18.23.48.hprof)` in the most cases in your home / user folder. See DynamicPlugins.kt:467 for more information.

0
Comment actions Permalink

Thanks, the dump location is now logged in idea.log and also shown as notification in UI.

2
Comment actions Permalink

Yann Cebron Any tips on what to look in the `.hprof` to determine why the class loader cannot be unloaded?

0
Comment actions Permalink

Figured it out

Step 1

build.gradle.kts

tasks {
runIde {
jvmArgs = listOf(
"-XX:+UnlockDiagnosticVMOptions"
)
}
}
Step 2
  • Run the Run Plugin run configuration. (Debug won’t work)
  • In development IDE enable registry setting ide.plugins.snapshot.on.unload.fail
    Tip: cmd+shift+a
Step 3
  • Make a change to your plugin’s code.
  • Run buildPlugin Gradle task.
  • Click on the Run > Run Plugin tool window.
  • Focus the idea.log tab.
  • On the right, there is a drop-down for the logging levels. Select all.
  • Focus the development IDE.
  • This will trigger your plugin to reload.
  • Check the idea.log tab.
  • You should see something like below:
2021-02-12 01:29:23,752 [ 181225] INFO - lij.ide.plugins.DynamicPlugins - Snapshot analysis result: Root 1:
ROOT: Global JNI
sun.lwawt.macosx.CMenuBar.target
com.apple.laf.ScreenMenuBar.fSwingBar
com.intellij.openapi.wm.impl.IdeMenuBar.myNewVisibleActions
java.util.ArrayList.elementData
java.lang.Object[]
com.github.livejs.plugins.live.actions.LiveActionGroup.<class>
com.github.livejs.plugins.live.actions.LiveActionGroup.<loader>
* com.intellij.ide.plugins.cl.PluginClassLoader

Dynamic plugins cannot reload if they use native dependencies or if a native dependency references something from the plugin.

From above we can see that our ActionGroup class is being referenced from the JNI, which prevents the PluginClassLoader from being GC’d.

This is because of the native code that controls the macOS menubar.

src/resources/META-INF/plugin.xml

<group id="Live.Main
class="com.github.livejs.plugins.live.actions.LiveActionGroup"
text="Live">
<add-to-group group-id="MainMenu" anchor="last"/>
</group>

So the problem is that plugins cannot reload if they modify the MainMenu.

If we don’t add to MainMenu, then our plugin reloads fine.

1
Comment actions Permalink

[Edit] I have no idea why this is downvoted, but it appears that it didn't work for someone. Please let me know if you find something to be improved in this workflow.

Original post:

Vaughan Rouesnel What I recently did, was (because Dmitry Jemerov showed that he used it) to install YourKit (I have also tried with Eclipse MAT, IntelliJ and visualvm but none worked) which has a free trial, open the hprof file, go to Class loader, find the class loader which references your plugin things and click Paths from GC Roots. The classes that are mentioned there, were not unloaded successfully for whatever reason. (However, even on a partially successful unload, I see classes present here, so not sure what that means).

It was largely trial and error for me though, and reasoning about code (e.g., need to make sure to stop long-running coroutines, and pay attention to the 'Stateful extension' inspection), so not sure if there's a better way.

Also make sure to use latest 211 EAP, that fixed a lot of issues for me.

-1
Comment actions Permalink

Hi, I'm running into the same problem with some code that's basically a Kotlin'ized version of the comparing references sample.  The CriQuickFix private static class from the java example is now an object member

object CriQuickFix : LocalQuickFix { ...

After setting everything up per the notes, it appears that the offending JNI ref is that object, but not sure why that would have any native dependencies.  It's the Java code, transformed to kotlin on paste with a little cleanup

  com.intellij.util.lang.PathClassLoader.classes
java.util.Vector.elementData
java.lang.Object[]
com.intellij.openapi.util.Disposer.ourTree
com.intellij.openapi.util.ObjectTree.myObject2NodeMap
it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap.key
java.lang.Object[]
com.intellij.analysis.problemsView.toolWindow.HighlightingWatcher.problems
java.util.LinkedHashMap.head
java.util.LinkedHashMap$Entry.key
com.intellij.openapi.editor.impl.RangeHighlighterImpl.myErrorStripeTooltip
com.intellij.codeInsight.daemon.impl.HighlightInfo.quickFixActionMarkers
com.intellij.util.containers.LockFreeCopyOnWriteArrayList.value
java.lang.Object[]
com.intellij.openapi.util.Pair.first
com.intellij.codeInsight.daemon.impl.HighlightInfo$IntentionActionDescriptor.myAction
com.intellij.codeInspection.ex.QuickFixWrapper.myFix
<myplugin>.ComparingReferencesInspection$CriQuickFix.<class>
<myplugin>.ComparingReferencesInspection$CriQuickFix.<loader>
* com.intellij.ide.plugins.cl.PluginClassLoader



0
Comment actions Permalink

Erich Oliphant, please use `class` instead of `object`

0
Comment actions Permalink
 

Hi, I actually tried that and had gotten it to work.  But I was still having intermittent issues, with "Sticky Class" as the root vs JNI.  It appears that (for inspections at least) you can't have a file in an active tab that triggers the inspection when you nav back to IntelliJ.   If I say click to the README, build the plugin, jump back to IJ, then click Offender.java, reload seems to work just fine

 

0

Please sign in to leave a comment.