Java 11 AppCDS example with Gradle

What is AppCDS and why do I care?

AppCDS is a way to preprocess application classes so they can be memory mapped and shared between JVMs to reduce startup memory usage and time by approximately 20%.

I was investigating ways to speed up initialisation of AWS Lambda functions and discovered this approach via the Internet somewhere. We had noticed that our function runs slower (ie every operation takes an order of magnitude longer) during the first invocation of an instance of that Lambda function (remember that concurrent requests to a Lambda function create new instances, which equate to new JVMs).

We generate a UUID as a static property of the main class that AWS initialises and then log that UUID as our JVM ID. Additionally we have a integer invocationCount property on the class for recording and logging the number of times each JVM has been invoked.

Interestingly and usefully for us, we were able to reproduce the behaviour when running our code locally when configured to run with CPU and Memory limits equivalent to what AWS provides.

How did I get it working?

The first thing I had to do was generate a “proper” JAR from our AWS Lambda function code. It was particularly important that the classpaths were going to be the same everywhere for AppCDS to work. This is change I added to the Gradle build file for the code:

jar {
  manifest {
    attributes(
      'Class-Path': configurations.runtimeClasspath
                      .collect { it.getName() }.join(' '),
      'Main-Class': 'com.acme.lambda.MainHandler'
    )
  }
}

Then thankfully I was just able to follow the instructions in the JEP. First up, generate a list of classes. I added a gradle task to run the application, with matching classpath setup, and output a file containing a list of classes.

task run(type: JavaExec, dependsOn: jar) {
    main 'com.acme.lambda.MainHandler'

    classpath = files(
            sourceSets.main.runtimeClasspath,
            buildDir.toPath()
                .resolve("libs/lambda.jar").toFile()
    )

    // for generating class list
    jvmArgs "-Xshare:off", "-XX:+UseAppCDS", "-XX:DumpLoadedClassList=lambda.lst"
}

Next up, generating the archive of class data:

task generateAppCDSArchive(type: JavaExec, dependsOn: compileJava) {
    classpath = files(
            sourceSets.main.runtimeClasspath,
            buildDir.toPath()
                .resolve("libs/lambda.jar").toFile()
    )

    main 'com.acme.lambda.MainHandler'

    jvmArgs "-Xshare:dump", "-XX:+UseAppCDS", "-XX:SharedClassListFile=lambda.lst",
            "-XX:SharedArchiveFile=lambda.jsa", "-Xlog:class+path=info"
}

And finally, running the application, using the archive:

task runWithAppCDS(type: JavaExec, dependsOn: jar) {
    main 'com.acme.lambda.MainHandler'

    classpath = files(
            sourceSets.main.runtimeClasspath,
            buildDir.toPath()
                .resolve("libs/lambda.jar").toFile()
    )

// for running with AppCDS on
    jvmArgs "-Xshare:on", "-XX:+UseAppCDS", "-XX:SharedArchiveFile=lambda.jsa"
}

Finally running some tests using that final gradle task showed approx a 16% speed up in the first invocations of the Lambda function code.

Next steps would be to create a custom Lambda layer so we can control invocation of our code and provide the relevant AppCDS data to the JVM.