Affects Version/s: 7.0.0 DXP FP50, 7.0.X, 7.1.X, Master
After undeploying modules GC will not remove classes nor decrease metaspace size.
Steps to reproduce
- Setup Liferay DXP bundle
- Deploy the attached hello-world-portlet-126.96.36.199.war
- Undeploy the porltet (delete it from osgi/war)
- Wait at least 1 minute (see additional information for reason)
- Execute a GC through JVisualVM or similar
- Create a heapdump through JVisualVM or by using jmap (e.g. jmap -dump:format=b,file=FILENAME.hprof <pid>)
- Open created heapdump in Eclipse MAT
- Click on the second down arrow in the top bar, then: Java Basics -> Class Loader Explorer (the second down arrow is called: "Open Query Browser")
- Find hello-world-portlet, right click on it, then: Class Loader -> Path to GC Roots -> exclude all Phantom/Weak/Soft etc. references
- Open the tree until you see "com.liferay.petra.lang.ClassLoaderPool"
Expected result: The hello-world-portlet shouldn't show up in the Class Loader Explorer
Actual result: The hello-world-portlet's classloader is being referenced by com.liferay.petra.lang.ClassLoaderPool
Liferay 7.0 de-55
Reproduced on Master (06dd26d1f7e6d672ddc8551698af56e41d028095)
- The reason why we have to wait at least 1 minute in step 4 is because during that time a "RequiredPluginsUtil" thread is still executing and holding reference to the classloader. After a minute the thread terminates, and as such, is a false positive.
- The main issue is that when deploying a WAB module, the WAB's classloader gets added with two different contextNames. This results in the unregister methods to fail to completely remove the classLoader from its own reference.
If we look at the register and unregister methods, we'll see the following logic (see corresponding sources):
Register: add classloader -> contextName mapping and contextName -> classloader mapping
Unregister1: Remove classLoader -> contextName mapping, and if it was present, remove contextName -> classLoader mapping
Unregister2: Remove contextName -> classLoader mapping, and if it was present, remove classLoader -> contextName mapping
Straightforward, however, with WAB portlets, the register method is called twice, with different contextNames. Once with hello-world-portlet and once with hello-world-portlet_7.0.1 (or something similar), but with the same Classloader.
This is a problem, because during register, the following happens:
- Add "hello-world-portlet" -> classloader
- Add classLoader -> "hello-world-portlet"
- Add "hello-world-portlet_7.0.1" -> classloader
- Update classloader -> "hello-world-portlet_7.0.1"
Just to clarify: at the end, _classLoaders will contain two entries for the WAB, whereas _contextNames will only contain one. At this point, during unregister, "hello-world-portlet_7.0.1" will be removed from both, however, "hello-world-portlet" mapping will remain in there.
The root cause of this is due to different ways of getting the contextName when registering the plugin.
The second execution comes from WebBundleDeployer.doStart -> _initWabBundle -> ... -> PluginContextListener.contextInitialized, where the contextName is decided by the standard javax.servlet.ServletContext.getServletContextName() method call.
To debug the issue, simply put a breakpoint in the register and unregister methods within ClassLoaderPool, and check the contents of the static _classLoaders and _contextNames maps.