[GRADLE-2193] Often PermGen out of memory in daemon builds Created: 22/Mar/12  Updated: 10/Feb/17  Resolved: 10/Feb/17

Status: Resolved
Project: Gradle
Affects Version/s: 1.0-milestone-9
Fix Version/s: None

Type: Bug
Reporter: Jarek Potiuk Assignee: Unassigned
Resolution: Won't Fix Votes: 15


 Description   

We run quite a big test ser for our Ameba (Apphance Mobile Build Automation) system. We have a number of plugins and quite extensive (180 now) number of tests running on Jenkins. About half of the tests is using Tooling API to either build project's models or perform test builds. Often (8/10 times) we get one of the tests failing with

Caused by: org.gradle.launcher.daemon.client.DaemonDisappearedException: Gradle build daemon disappeared unexpectedly (it may have been stopped, killed or may have crashed)
at org.gradle.launcher.daemon.client.DaemonClient.handleDaemonDisappearance(DaemonClient.java:186)

When you look in ~/gradle/daemon/1.0-milestone-9/ you find that the case is most likely:

02:33:49.773 [INFO] [org.gradle.launcher.daemon.server.exec.ForwardClientInput] The daemon will no longer process any standard input.
EMMA: collecting runtime coverage data ...

client disconnection detected, stopping the daemon
java.lang.OutOfMemoryError: PermGen space

We tried to play a bit with the options (gradle.properties) and set MaxPermSize higher and add some options recommended for solving similiar problems in Application servers:

org.gradle.jvmargs=-XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8

But contrary to documentation they seem to be not used when daemon is launched (though utf-8 seems to be correcto) :

[org.gradle.launcher.daemon.server.DomainRegistryUpdater] Advertised daemon context: DefaultDaemonContext[uid=27410d7f-e130-45f1-a359-57d50ca9ae44,javaHome=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home,daemonRegistryDir=/Users/jenkins/.gradle/daemon,pid=41265,idleTimeout=10800000,daemonOpts=-XX:MaxPermSize=256m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx1024m,-Dfile.encoding=UTF-8]

We have a couple of hprof that daemon dumped - I can send links to them privately (they are huge - 500 MB each) . You can also try yourself:

https://github.com/apphance/Apphance-MobilE-Build-Automation



 Comments   
Comment by Jarek Potiuk [ 24/Mar/12 ]

Update: setting GRADLE_OPTS with -XX=MaxPermSize=512m does not work as well. The main project has it properly set, but daemons still start with -XX:MaxPermSize=256m. Any way to change it for daemons. It is really annoying now - we cannot make our builds green in Jenkins..

Comment by Jarek Potiuk [ 24/Mar/12 ]

Another comment. It seems that the properties are properly passed to the actual daemon executing gradle, but all the daemons spawned by tooling api (i run builds inside the build) do not get the options.

502 28976 25735 0 0:00.15 ?? 0:01.25 /System/Library/Frameworks/JavaVM.framework/Home/bin/java -Xdock:name=Gradle -Xdock:icon=/private/var/lib/gradle-1.0-milestone-9/media/gradle.icns -Xmx1024m -Xms256m -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.appname=gradle -classpath /private/var/lib/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.GradleMain --info --stacktrace --daemon clean cleanTest assemble groovydoc check
502 28985 28976 0 0:01.93 ?? 0:40.13 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8 -cp /private/var/lib/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.0-milestone-9 /Users/jenkins/.gradle/daemon 10800000 7964178e-74d2-4fdd-b506-47b06964e029 -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8

502 28998 28985 0 0:00.23 ?? 0:03.44 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -Demma.coverage.out.file=/Users/jenkins/workspace/AMEBA/build/tmp/emma/metadata.emma -Demma.coverage.out.merge=true -Dfile.encoding=UTF-8 -ea -cp /Users/jenkins/.gradle/caches/1.0-milestone-9/workerMain/classes org.gradle.process.internal.launcher.GradleWorkerMain
502 28999 28998 0 0:00.77 ?? 0:13.46 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8 -cp /Users/jenkins/.gradle/wrapper/dists/gradle-1.0-milestone-9-bin/7ilkmgo2rn79vvfvd51rqf17ks/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.0-milestone-9 /Users/jenkins/.gradle/daemon 10800000 8fa75f35-2529-477f-b375-18bacaf789c6 -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8

Comment by Jarek Potiuk [ 24/Mar/12 ]

Yet another comment. I think I managed to fix the project by specifying custom options at BuildLauncher's setJVMArguments. But we had many such launchers in our code (and more to come most likely) so the solution is less-then-ideal. It would be really good if we can control daemon options from single place.

Comment by Szczepan Faber [ 26/Mar/12 ]

Hey Jarek!

I'm not sure we can solve the PermGen - isn't it because your build is doing lots of stuff?

Can you provide more information on the problem with the 'gradle.properties' not honored by the daemon? I does work according to our integration tests so I'd like to make sure there is no problem with that. How do you configure the memory in the gradle.properties and where does the file live wrt the project dir, etc.

>It would be really good if we can control daemon options from single place

Provided the 'gradle.properties' work - does it solve the problem? If not, what kind of 'single place' configuration do you need? You're welcome to develop your own layer on top of the GradleConnector that will configure necessary jvm opts as needed.

On the side note, the rc-1 brings some very cool stuff: you can pass arbitrary command line parameters. This should increase the usability of the tooling api a lot. Check it out at http://wiki.gradle.org/display/GRADLE/Gradle+1.0-rc-1+Release+Notes

Hope that helps!

Comment by Jarek Potiuk [ 31/Mar/12 ]

Indeed - I tried it on a small test project I built, and could not reproduce it. But I think I nailed it now and it might be potentially serious issue if I am right ... I think there are actually two problems....

1) (small) You cannot specify "additional" JVM params in buildLauncher, nor read the current params - whenever you use setJVM params you always override gradle.properties setting

2) (more serious) When you run 'setJVMArgs' build through launcher inside a test, it seems that all or some of the further tests are run in the JVM in which the new args were set - even if it is not specified in the first launcher.

Some more details:

I am not 100% sure though if I correctly interpret the output, so let me describe the problem in detail.
In one of the tests I needed to set system property at gradle execution time , by specifying '-Dproperty=value' using setJVMArguments

buildLauncher.setJvmArguments('-Dproperty=value')
buildLauncher.run()

It did seem to work for that particular test, but..... There is no "appendJVMArguments" method but only "setJVMArguments" so ALL the JVM args set in gradle.properties are OVERRIDDEN by the new arguments... In this case -XX:MaxPermSize was set to default 256m. This means that even if I set arguments in gradle.properties, for this particular test they are not taken into account.. Moreover there is not a "getJVMArguments" either, so I cannot even do setJVMArguments(getJVMArguments() + "-Dproperty=value'). This is problem no. 1 then: there should be possibility to add new argument rather than override all of them....

So now the more serious problem no. 2:

It looks like all or some of the further tests run with GradleConnector are actually using the new daemon spawned for this particular test: even if it's JVM arguments are different than the default ones.... That caused than in our case all the tests were using default PermSize rather than 512m ..... And failed because indeed they were doing a lot..

I created a small project to demonstrate it and I think it actually shows that behaviour:

https://github.com/potiuk/gradle-jvm-options-problem

When I run 'gradle test', it seems that it spawns that for the first test it spawns daemon with default PermSize (as requested in setJVM args) and then reuses the same worker for the second test (which does not specify JVM args). The second test seems to use the same daemon.

The tests are here: https://github.com/potiuk/gradle-jvm-options-problem/blob/master/src/test/groovy/TestBuild.groovy
Gradle test daemon properties are set in https://github.com/potiuk/gradle-jvm-options-problem/blob/master/gradle.properties
I also put in the repo result of running the test (gradle cleanTest test --debug >res.txt): https://raw.github.com/potiuk/gradle-jvm-options-problem/master/res.txt

And the processes running after the test (ps -eaf | grep java >psres.txt): https://raw.github.com/potiuk/gradle-jvm-options-problem/master/psres.txt

And highlights from this file:

1) Starting the daemon for the build itself:
01:27:49.612 [INFO] [org.gradle.launcher.daemon.client.DefaultDaemonStarter] Starting daemon process: workingDir = /Users/potiuk/.gradle/daemon/1.0-milestone-9, daemonArgs: [/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java, -XX:MaxPermSize=512m, -XX:+CMSClassUnloadingEnabled, -XX:+CMSPermGenSweepingEnabled, -XX:+HeapDumpOnOutOfMemoryError, -Xmx1024m, -Dfile.encoding=utf-8, -cp, /data/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar, org.gradle.launcher.daemon.bootstrap.GradleDaemon, 1.0-milestone-9, /Users/potiuk/.gradle/daemon, 120000, 25750109-26d6-45da-9ee5-9974663352dc, -XX:MaxPermSize=512m, -XX:+CMSClassUnloadingEnabled, -XX:+CMSPermGenSweepingEnabled, -XX:+HeapDumpOnOutOfMemoryError, -Xmx1024m, -Dfile.encoding=utf-8]

This is fine - according to gradle.properties' setup.

2) There is a line about creating process for Worker 1: but only one.. and it seems both tests are using this worker...
01:28:01.035 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] creating process builder for Gradle Worker 1 but we can see only few parameters there:
01:28:01.036 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] in directory /Users/potiuk/test2
01:28:01.036 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] with argument#0 = -Dfile.encoding=UTF-8
01:28:01.037 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] with argument#1 = -ea
01:28:01.038 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] with argument#2 = -cp
01:28:01.038 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] with argument#3 = /Users/potiuk/.gradle/caches/1.0-milestone-9/workerMain/classes
01:28:01.039 [DEBUG] [org.gradle.process.internal.ProcessBuilderFactory] with argument#4 = org.gradle.process.internal.launcher.GradleWorkerMain

3) It looks like in the logs both tests are run by the same Worker 1, but in this case (different JVM args) there should be two different processes in fact. As you see - the second process does not have the 512m permgen specified

501 24899 1 0 12:27AM ttys000 0:12.78 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -XX:MaxPermSize=128m -Dfile.encoding=utf-8 -cp /Users/potiuk/.gradle/wrapper/dists/gradle-1.0-milestone-9-bin/7ilkmgo2rn79vvfvd51rqf17ks/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.0-milestone-9 /Users/potiuk/.gradle/daemon 10800000 be225ac9-7bd8-4d09-9c2a-407390fe0ef4 -XX:MaxPermSize=128m -Dfile.encoding=utf-8
501 24903 1 0 12:27AM ttys000 0:12.51 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8 -cp /Users/potiuk/.gradle/wrapper/dists/gradle-1.0-milestone-9-bin/7ilkmgo2rn79vvfvd51rqf17ks/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.0-milestone-9 /Users/potiuk/.gradle/daemon 10800000 f864042a-6071-401e-93b0-8ed9b314190c -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8

4) If I remove the setJVMArgs line everything looks the same, except the PS output shows two processes with -XX:MaxPermSize=512m

501 25367 1 0 1:37AM ttys000 0:14.46 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8 -cp /Users/potiuk/.gradle/wrapper/dists/gradle-1.0-milestone-9-bin/7ilkmgo2rn79vvfvd51rqf17ks/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.0-milestone-9 /Users/potiuk/.gradle/daemon 10800000 1f47233c-ca1b-4b92-adca-5c6edea6893c -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8
501 25368 1 0 1:37AM ttys000 0:13.26 /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8 -cp /Users/potiuk/.gradle/wrapper/dists/gradle-1.0-milestone-9-bin/7ilkmgo2rn79vvfvd51rqf17ks/gradle-1.0-milestone-9/lib/gradle-launcher-1.0-milestone-9.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.0-milestone-9 /Users/potiuk/.gradle/daemon 10800000 04a6e441-a986-40be-ae18-17fe88627243 -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=utf-8

I interpret it in the following way: the first test creates a new process with JVMs overridden and then all (or at least some) of the further tests are using this process to run, no matter what JVM args they have.

Comment by Szczepan Faber [ 02/Apr/12 ]

It did seem to work for that particular test, but..... There is no "appendJVMArguments"

I can see that as a missing capability. At the moment it is not possible to have some 'global' defaults in gradle.properties and yet, configure something extra per BuildLauncher.setJvmArguments(). I think it is a reasonable use case. I'll discuss that with the team.

It looks like all or some of the further tests run with GradleConnector are actually using the new daemon spawned for this particular test

This is expected, though not documented as nicely as it should I admit. I've just added some more info on that matter to the daemon and tooling api chapter.

Tooling API uses the daemon behind the hood. So it is expected that subsequent task runs via the tooling api are executed within the same long-living process.

At the moment, the daemon is reused when:

  • it is idle
  • its jvm args and java home and gradle version match

@Jarek, does above explain the behavior you see?

Comment by Jarek Potiuk [ 02/Apr/12 ]

Yeah - for "daemon-per JVM args" .... This is how I understood it... But I think we have another problem... Which was revealed in our ameba tests...

It looks that for some reason the 2nd test https://github.com/potiuk/gradle-jvm-options-problem/blob/master/src/test/groovy/TestBuild.groovy uses the same daemon process as the 1st one - even though they have different JVM args...

How can I see it:

After I run the tests I can see two processes: 1) -XX:MaxPermSize=128m 2) -XX:MaxPermSize=512m.
But I would expect 3 processes instead :
1) -XX:MaxPermSize=128m 2) -XX:MaxPermSize=512m 3) -XX:MaxPermSize=512m

1) and 2) would be for each of the tests, where 3) is for the main 'gradle tests' process (busy while running the tests).

If I remove the setJvmArguments() I see 2 processes (as expected) - 1 for the tests and one for the main process...

j.

Comment by Szczepan Faber [ 03/Apr/12 ]

Hey,

Most system properties don't cause the new daemon to be forked. Check out the docs here: http://gradle.org/docs/nightly/userguide/userguide_single.html#gradle_daemon (the implementation notes section). I'm updating the docs as we have this conversation so that it's clear what's going on with the daemon.

Generally we wanted to balance out the need for forking extra daemons. We don't want the machine to be swarmed by the daemons, that's why we fork a new only if we must. Most system properties can be changed for the already running process so we don't really need another daemon to satisfy extra system property.

Does it answer the question?

Comment by Jarek Potiuk [ 03/Apr/12 ]

I know I am being stubborn but in this case it is more than just system property that the two runs are different. In conjunction with the lack of "appendJVMArguments" problem. The method setJVMArguments('-Dprop=value') does not only set system property, but it also changes -XXMaxPermSize setting (from the one specified in gradle.properties to default 128m).
Test one should be run with -XXMaxPermSize=512m (from gradle.properties) and the second with (-Dprop=value -XXMaxPermSize=128m). And different MaxPermSize is, I guess, enough reason to have different daemons....

Effectivelly: by running setJVMArgs() on one connector in one test, I influence the JVMArgs of the other test (and they should be pretty much isolated) ....

Comment by Szczepan Faber [ 03/Apr/12 ]

I agree this behavior is somewhat unhandy. We'll try to figure out some way to tidy this up.

In meantime, I guess you can use setJvmArgs (with some refactoring to avoid duplication instead of gradle.properties.

Effectivelly: by running setJVMArgs() on one connector in one test, I influence the JVMArgs of the other test (and they should be pretty much isolated) ....
{quote)

That should not be the case. If you setJvmArgs() in one test:

  1. the settings from gradle.properties are not used
  2. the default memory settings are used (unless setJvmArgs overrides them)
  3. it should not affect any other tests: if a different test has 'different' jvm args new daemon will be forked

Unless I'm not getting something right

Comment by Jarek Potiuk [ 03/Apr/12 ]

I already used setJvm everywhere ...

But for the isolation.. maybe I am not clear but my observation shows that they do influence each other. Just run 'gradle test' in the project https://github.com/potiuk/gradle-jvm-options-problem and you will see that there is NO daemon forked for the second test at all. It looks like the second test is run using already existing (-Dprop=value -XXMaxPermSize=128m) process which is forked from the first test. You can see it because after running it you will only see two processes: the main process and the forked -128m process. I expect that in this case both tests would spawn its own process and we should end up with 3 processes (main, -128m for first test , -512m for second test)

Comment by Szczepan Faber [ 04/Apr/12 ]

Ok, I know what you mean This behavior is a consequence of the fact that effectively both tests have different memory settings. One test uses the default memory settings, the other one uses memory settings from gradle.properties. Hence 2 processes.

I think the correct way to resolve the problem is to have a single process in this use case. However, single process or 2 processes is truly an implementation detail. The core problem is that default values from 'gradle.properties' are not used which is rather unexpected. I think I want to solve it by making the jvmargs property merging smartly its values. Yet, I need to think about it a bit more.

Thanks a lot of patience and reporting this problem!

Comment by Jarek Potiuk [ 04/Apr/12 ]

Cool. As said - it's not a problem for now for us - we workarounded it by refactoring already week ago, but would be nice to fix for others who might have similar problems - clearing the paths for others. It's very difficult to find what's the root cause it if you happen to have this problem (which is quite likely for relatively big projects which want to do extensive testing). Thanks for being patient in listening to complaints .. .

Comment by Joel Pearson [ 03/Jun/12 ]

I'm not sure if this is the same problem. But I've noticed that "org.gradle.jvmargs=-XX:MaxPermSize=512m" doesn't make it through to the gradle workers (in gradle.properties or on the command line). It only makes it as far as the gradle daemon process. However adding:

test {
  jvmArgs '-XX:MaxPermSize=256m'
}

to build.gradle does make its way to the workers.

I took that above snippet from this similar issue GRADLE-1957.

Should I create a new issue for this? Or is it the same problem?

Comment by Benjamin Muschko [ 15/Nov/16 ]

As announced on the Gradle blog we are planning to completely migrate issues from JIRA to GitHub.

We intend to prioritize issues that are actionable and impactful while working more closely with the community. Many of our JIRA issues are inactionable or irrelevant. We would like to request your help to ensure we can appropriately prioritize JIRA issues you’ve contributed to.

Please confirm that you still advocate for your JIRA issue before December 10th, 2016 by:

  • Checking that your issues contain requisite context, impact, behaviors, and examples as described in our published guidelines.
  • Leave a comment on the JIRA issue or open a new GitHub issue confirming that the above is complete.

We look forward to collaborating with you more closely on GitHub. Thank you for your contribution to Gradle!

Comment by Benjamin Muschko [ 10/Feb/17 ]

Thanks again for reporting this issue. We haven't heard back from you after our inquiry from November 15th. We are closing this issue now. Please create an issue on GitHub if you still feel passionate about getting it resolved.

Generated at Wed Jun 30 12:15:15 CDT 2021 using Jira 8.4.2#804003-sha1:d21414fc212e3af190e92c2d2ac41299b89402cf.