Mobile Development 16 min read

Analyzing and Optimizing Kapt Memory Consumption in Android Projects

This article examines the memory‑intensive behavior of Kotlin's Kapt annotation‑processing tool in large Android builds, explains its internal two‑step stub‑generation and Java‑apt workflow, analyzes OOM root causes with VisualVM, and presents a practical source‑filtering fix that dramatically reduces compile time and memory usage.

ByteDance Terminal Technology
ByteDance Terminal Technology
ByteDance Terminal Technology
Analyzing and Optimizing Kapt Memory Consumption in Android Projects

When integrating Hilt into the large Volcano Android project, the Kapt annotation‑processing step repeatedly triggered Out‑Of‑Memory (OOM) errors on a machine with 16 GB RAM, prompting an in‑depth investigation of Kapt’s inner workings.

Kapt principle : Kapt replaces the traditional annotationProcessor dependency with the kapt plugin, generating two Gradle tasks – kaptGenerateStubs${variant}kotlin and kapt${variant}Kotlin . The first task creates Java stub files from Kotlin sources, and the second invokes the standard Java APT (JSR‑269) on those stubs.

Typical usage looks like:

kapt "groupId:artifactId:version"
apply plugin: 'kotlin-kapt'

During stub generation each .kt file yields a corresponding .java stub that contains only the ABI‑level signatures needed by the Java compiler; method bodies are omitted.

Annotation‑processing flow : KaptTask discovers all registered processors, parses source files into ASTs, runs the JDK’s annotation processing (which may spawn multiple rounds), and writes generated Java files. After each round the JDK’s TreeScanner called treeCleaner nullifies symbol references, but some references (e.g., in log.diagFormatter ) remain, preventing garbage collection.

Memory analysis with VisualVM revealed a massive growth of Scope$Entry[] objects. Further inspection showed that a huge proportion of the inputs were auto‑generated R.java files, which contain only constant fields and have no annotations. Because these files are added to javaSourceRoots , they unnecessarily participate in stub generation and annotation processing, inflating memory usage.

Filtering out the generated R.java files solved the problem. The Kotlin source‑root getter was modified as follows:

@get:Internal
protected val javaSourceRoots: Set
get() = unfilteredJavaSourceRoots.filterTo(HashSet(), ::isRootAllowed)
        .filterTo(HashSet()) { !(it.absolutePath.contains("generated/not_namespaced_r_class_sources/")) }

Results : After applying the filter, the Kapt task that previously took 18 minutes and exhausted >13 GB of RAM completed in 15 seconds, saving more than 13 GB of memory. In other projects the compilation time dropped from ~31 seconds to ~1.4 seconds – a 20× speedup.

Recommendations :

Limit the scope of Kapt by adding it only to modules that truly need annotation processing.

Adopt build‑time optimization tools (e.g., ByteX, any‑register) to reduce incremental compile overhead.

Consider alternatives such as the Transform API or the upcoming Kotlin Symbol Processing (KSP) to avoid the two‑step stub generation.

Upgrade the JDK version when possible; JDK 9 already mitigates the OOM issue.

Recruitment : The Build Infra team is hiring for positions in Beijing, Shanghai, and Hangzhou. Interested candidates should email [email protected] with the subject format “Name‑Devops‑Build Infra”.

AndroidBuild OptimizationGradleKotlinmemory leakannotation-processingKapt
ByteDance Terminal Technology
Written by

ByteDance Terminal Technology

Official account of ByteDance Terminal Technology, sharing technical insights and team updates.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.