-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Build plugin execution leaking classes into the Eclipse JVM #1214
Comments
@odrotbohm have you checked (e.g. with JProvfler or VisualVM) that the classes are retained (e.g. after a GC?) if yes, generally the next step is to look at the GC_ root that is preventing the classes from being garbage collected. Beside this it could help if the maven plugin participates in the Incremental build e.g. with BuildContext: |
I'm exploring the application using YourKit but have only seen the forest for the trees so far. Yes, the classes are retained after GCs. So, the number of loaded classes grows unbounded and ultimately results in significant off-heap memory consumption. Yes, the plugin has already supported incremental builds (I actually contributed a draft of that) for a while. The individual compilations don't have a drastic effect on the number of loaded classes. However, a full project update sometimes adds up to a 1000 of them. 😕 |
I think then you should look at the classloader of the retained class and check what instance holds this so it can't be garbage collected. Yourkit seem to have also an option to show GC roots: https://www.yourkit.com/docs/dotnet-profiler/2022.9/help/gc_roots.jsp. |
I don't know what bytebuddy plugin does, but loads it classes through the classloader of the plugin? If yes, it probably better would use an own URLClassloader that is disposed after the work is done. Neverless I would have assumed that m2e detatches the classrealm of the plugin, but need to further investigate on this. Both have pro and con of course, e.g. when classes are garbage collected they might need to be loaded on each call again... |
That is pretty much what it does. It's a rather small mojo and all class loading is happening here: https://github.com/raphw/byte-buddy/blob/master/byte-buddy-maven-plugin/src/main/java/net/bytebuddy/build/maven/ClassLoaderResolver.java#L135 The class loaders are later closed. I would not understand how this code is leaking classes. |
|
That's the plugins class loader, and that's intended as it's needed to run the plugin. Isn't the plugin class loader managed by Eclipse? The Url class loader is closed and discarded at the end of the plugin execution. |
Well you asked why these classes are leaking into eclipse and that's the reason :-) So from the javadoc the method behaves wrong, it claims to construct a classlaoder of a maven coordinate but in fact it creates one that loads all classes reachable from the maven-plugin AND additional from the maven coordinate. So if you need that, but not really ever need to use the classes directly, you should just not pass a parent here, as you are leaking classes from your maven plugin into that loader. If you need this (why?) then you it depends on the way it is used what might be a better strategy. At least m2e do not really "manages" this, but I'll check if we probably can release some reference, but this will just help with the symptoms and then byte-buddy would load all these classes on each incremental run as well (what might slow things down considerably), so then you probably want to cache this on the build context ;-) |
Now I am confused. Why would that cause 800 classes to leak on every execution? The Byte Buddy Mojo needs to provide the Byte Buddy dependency to its plugins. If the class loader is acting as a parent, the new children can request Byte Buddy classes from the parent, but will load the plugin classes into the new, later discarded loader. If there's a leak, it would be the Maven plugin class loader. If the Maven plugin loader is reused, it should not cause s loading of additional classes later. |
I am far from an expert on this and feel free to ignore me on this but isn't the problem we're seeing the opposite of what @laeubi is describing? Wouldn't we want the loaded ByteBuddy classes to be reused in a parent classloader? Instead we seem to see completely new |
I don#t know as I don't know anything about the byte-buddy plugin, but each class you load from the plugin-realm will not be garbage collected when you close your URLClassloader, that will only free the open files, and possibly allows any no longer referenced class from being garbage collected.
No it does not, please read about the default classloader delegation model here:
So any class sis first searched by the parent by default, and that parent might cache the class as it will never be unrenferenced when you close the URL one.
Where exactly do you see this new realm created? Each plugin has an own realm that has a parent of the project, and as said these might be cached. But one can create a new ClassRealm (with self-first strategy) and only add dedicated packages from the plugin realm. |
In the YourKit screenshot posted above, which, admittedly, wasn't really obvious. Almost every of the So I was naively thinking that m2e should either reuse one particular instance or not hold on to the ones not used anymore? |
If I read the GC root path correctly, it looks like the main mojo class ( |
Its a bit hard to tell from a screenshot, but maybe we can narrow this is a bit down:
As far as I know m2e not directly "manages" the realms but calls maven for this, so it is possible that we manually have to "unlink" thinks after an mojo execution, but first we should make sure what happens when and if its expected or not. |
The classes contained in each To a noob like me, this looks like instead of reusing the
See the inspection of two of those "identical" I've uploaded the heap dump here, in case you are eager to explore it yourself. The project to look out for is |
I'll try to take a look at it the next days...
You need a dev environment: https://github.com/eclipse-m2e/m2e-core/blob/master/CONTRIBUTING.md#-developer-resources then you can start an eclipse instance in debug mode from it and just import your project you like to take a deeper look. |
One thing I noticed, the cleanup code in |
Thanks for that and the pointers, I'll see what I can do. |
The container should effectively never be disposed. |
I got to debug the build of the project and I see a To me, it seems like that, if we want to reuse the underlying core Maven abstraction instances, we have to make sure, that we pass a |
A couple more details: the |
@odrotbohm thanks for the analysis, do you like to provide a PR for this? If you can implement/fix the caching, it should use some kind of |
I'm not confident to do that (yet), as this could be solved in a couple of ways involving parties not involved in the discussion yet, and I don't know what their stance on the topic is. For example, I am totally unaware which of the instances I am seeing in the debug session are and can be cached, how expensive it is to recreate them etc. I think the main culprit is that the Do you think that's an adequate approach? I'm sure the design of |
As said the container should stay as it is, but plugin (or project)-realms might be disposed if no longer needed. |
I don't think it can as otherwise the I'm sure there's a way to be more precise and only wipe the container instance for the project under update, but I didn't want to mess with the way the instances are managed, as that looked rather complex and – as I tried to indicate above – in place for a reason. But conceptually, I think it would be consistent to also re-initialize the plexus container to get a fresh start for the project under update, wouldn't it? |
So if there is no way for |
I know, I know. I was just trying to find out whether the |
@odrotbohm I now have taken a look at As you said, the Do you like to report a bugreport to plexus/sisu so it could probably fixed? Even worse, |
I've filed a ticket here. |
Thanks for the fix, @laeubi! Any chance someone could trigger a nightly build? I'd love to use this in my current dev installation. 🙇 |
It should already be deployed: https://ci.eclipse.org/m2e/job/m2e/job/master/ You can install it from here: |
The update site shows a 2.1.3 build available from this morning. However, my installation already has a 2.2.0 (likely snapshot from Jan, 3rd) installed. Did you downgrade the version number for master at some point? 🤔 |
It looks like it's been this commit. I've uninstalled the 2.2 version, reinstalled 2.1.2 and trying to upgrade to the 2.1.3 snapshots now results in this:
I unfortunately need the snapshots as 2.1.2 is pretty badly broken due to #1150. :/ |
That's correct. Just created #1218 to bump the version.
That is very like because we recently upgraded to use gson 2.10. Before we used the gson version provided by our deps but probably nobody else provide gson 2.10 now. With #1218 gson 2.10 is also included into the m2e-repository. |
Thanks for the quick turnaround, Hannes. Awesome team! 🙇 |
m2e executes build plugins both during project updates and also during incremental compilation of sources of a project. A sample project available here shows how the execution of e.g. the Bytebuddy Maven Plugin causes ~800 classes being added to the Eclipse JVM for each full project update and a low to mid two-digit value for each compilation.
I spoke to @raphw (author of the Bytebuddy Maven Plugin) and he couldn't think of any obvious problem within the plugin itself. I was wondering if this symptom could be related to how m2e handles the instances of the plugins. It seemed to reuse the instances between incremental compilation, but I am totally just observing this from the outside.
I'd be willing to help to diagnose this further if you have any pointers which code is actually creating and maintaining the build plugin instances, which object instances to look out for in a heap dump etc.
The text was updated successfully, but these errors were encountered: