javaanttypedefout-of-memorytaskdef

Ant taskdef - running out of PermGen space


Using Ant tasks that create subprojects such as <antcall> and <ant> may cause build failures when invoked repeatedly due to one of the following errors:

The error only occurs when one of the tasks being called is defined using <typedef> or <taskdef> and does not appear when using tasks bundled with Ant such as <javadoc>.

Is there a way to avoid the OutOfMemoryError without increasing the maximum Java heap size? Although increasing the heap size works for a while, the problem still resurfaces if more memory intensive tasks are added.


The following example task and associated build.xml file cause an OutOfMemoryError on my Linux box with the Java heap set to 10 MB (for testing). The Ant task constructs a memory-hungry object (in this case a Guice injector for a Closure Template Soy Module), which is then repeatedly called using <antcall>.

 

CreateGuiceInjectorTask.java

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.template.soy.SoyModule;

import org.apache.tools.ant.Task;

/** Custom Ant task that constructs a Guice injector. */
public final class CreateGuiceInjectorTask extends Task {
  private Injector injector;

  public CreateGuiceInjectorTask() {
    injector = Guice.createInjector(new SoyModule());
  }

  public void execute() {
    System.out.println("Constructed Guice injector...");
  }
}

 

build.xml

<?xml version="1.0" encoding="UTF-8"?>
<project name="out-of-memory-test" basedir=".">

  <property name="build.dir" location="${basedir}/build" />
  <property name="CreateGuiceInjectorTask.jar"
      location="${build.dir}/CreateGuiceInjectorTask.jar" />

  <taskdef name="create-injector"
      classname="CreateGuiceInjectorTask"
      classpath="${CreateGuiceInjectorTask.jar}" />

  <target name="call-create-injector">
    <create-injector />
  </target>

  <target name="test"
      description="Create multiple injectors until out of memory">
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
    <antcall target="call-create-injector" />
  </target>
</project>

 

Test Output:

$ ant test

test:

call-create-injector:
[create-injector] Constructed Guice injector...

call-create-injector:
[create-injector] Constructed Guice injector...

...

call-create-injector:

BUILD FAILED
Could not create type create-injector due to java.lang.OutOfMemoryError: Java heap space

Solution

  • Using Keith Gregory's method described here I was able to work around the memory problem, at least for the simple example code you posted.

    The problem, in outline, is that each time you use a taskdef via an antcall Ant uses a different class loader, hence you eat up your permgen in no time. To confirm this you could modify your class to print the classloader hash code - you'll see its different on each iteration.

    The work-around is to package your taskdef as an antlib and use the antlib namespace to load it. The upshot is that Ant's own classloader is used. For this to work you must place the class on the Ant classpath.

    To test this I put your test class in a package namespace (called memtest), compiled, then added an antlib.xml in the package directory that looks like this:

    <antlib>
      <taskdef name="create-injector" classname="memtest.CreateGuiceInjectorTask" />
    </antlib>
    

    The buildfile project declaration was changed to

    <project name="out-of-memory-test" basedir="." default="test" xmlns:mt="antlib:memtest">
    

    and the target to

    <target name="call-create-injector">
      <mt:create-injector />
    </target>
    

    To test I put everything needed on the Ant classpath. It ran many antcalls successfully, and the debug print of the classloader hash code confirmed the expected single instance was used.

    There's a note in the Ant documentation for the antlib namespace that says "the requirement that the resource is in the default classpath may be removed in future versions of Ant." Depending on how that is implemented the work-around may break in future.