Learn Groovy - Performance & Optimization
Series/Learn Groovy/Episode 16
Episode 16 of 23

Learn Groovy - Performance & Optimization

This episode covers optimizing Groovy performance: evaluating the trade-off between dynamic and static, using @CompileStatic, leveraging invoke dynamic, and profiling the JVM to find bottlenecks and minimize runtime overhead.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Groovy is praised for its flexibility, but flexibility has a price: runtime overhead for dynamic dispatch. Episode 16 covers how to measure, understand, and optimize the performance of Groovy scripts.

You'll learn the dynamic-versus-static trade-off, use @CompileStatic, understand the role of invoke dynamic on the JVM, and profile the JVM to find bottlenecks and minimize runtime overhead.

The Dynamic vs Static Trade-Off

The Cost of Dynamic Dispatch

When Groovy calls a method dynamically, the JVM can't immediately know which method is being executed. The lookup happens at runtime through mechanisms like CallSite. This is very flexible, but it adds overhead compared to Java's static calls.

Two main techniques reduce the overhead:

  • Invoke dynamic (indy): Groovy's default strategy that uses special JVM instructions, far faster than the old call-site mechanism.
  • @CompileStatic: forces the compiler to generate Java-like bytecode, removing dynamic dispatch entirely.

When Optimization Is Needed

  • One-off scripts that run in seconds don't need optimization.
  • Code in the hot path — called millions of times — is worth optimizing.
  • Measure first with profiling before deciding.

Using @CompileStatic

Usage Basics

@CompileStatic tells the Groovy compiler to perform type checking and generate static bytecode:

Method with @CompileStatic
import groovy.transform.CompileStatic
 
@CompileStatic
int totalNilai(List<Integer> nilai) {
    nilai.sum()
}
 
println totalNilai([10, 20, 30])

@CompileStatic on a method makes the compiler check the types of List<Integer> and nilai.sum() at compile time. int totalNilai(List<Integer> nilai) produces bytecode equivalent to Java for this method.

Levels of Application

  • Method: type checking only for that method.
  • Class: all methods inside the class are statically checked.
  • Script: the entire script is statically checked.

The higher the level, the greater the overhead savings, but the fewer dynamic features are available. Methods that use methodMissing or metaprogramming aren't compatible with @CompileStatic.

Performance Comparison

The performance difference can be significant in the hot path. A simple benchmark:

Dynamic vs static benchmark
import groovy.transform.CompileStatic
 
@CompileStatic
long loopStatis() {
    long total = 0
    for (int i = 0; i < 1_000_000; i++) {
        total += i
    }
    total
}
 
long loopDinamis() {
    long total = 0
    for (i in 0..<1_000_000) {
        total += i
    }
    total
}
 
def waktu = { closure ->
    def mulai = System.nanoTime()
    closure()
    (System.nanoTime() - mulai) / 1e6
}
 
println "Statis: ${waktu { loopStatis() }} ms"
println "Dinamis: ${waktu { loopDinamis() }} ms"

def waktu = { closure -> ... } is a timing closure. (System.nanoTime() - mulai) / 1e6 computes the duration in milliseconds. @CompileStatic long loopStatis() is usually faster for loops like this.

Minimizing Runtime Overhead

Avoid Temporary Objects

One of the hidden overheads is the creation of temporary objects:

  • Reuse existing collections instead of creating new ones every iteration.
  • Use primitive int, long, and double in @CompileStatic methods.
  • Avoid excessive GString interpolation in hot loops; use StringBuilder.

GString and StringBuilder

GString is very convenient, but in a hot path using StringBuilder is faster:

StringBuilder for the hot path
@CompileStatic
String bangunPesan(int n) {
    def sb = new StringBuilder()
    for (int i = 0; i < n; i++) {
        sb.append("baris ").append(i).append("\n")
    }
    sb.toString()
}
 
println bangunPesan(3)

sb.append("baris ").append(i) concatenates strings without creating intermediate objects. @CompileStatic String bangunPesan(int n) ensures the StringBuilder calls are statically compiled.

JVM Profiling

Measuring with jcmd and jconsole

Profiling starts with measurement. The JDK ships with built-in tools:

Measure memory with jcmd
jcmd <pid> GC.heap_info
jconsole

jcmd <pid> GC.heap_info shows the heap state of a JVM process, and jconsole opens a real-time monitoring GUI. For deeper CPU profiling, use async-profiler or the Java Flight Recorder.

Finding Bottlenecks

  • Measure the baseline execution time before changing anything.
  • Profile CPU to find the busiest methods.
  • Profile memory to find leaks or excessive objects.
  • Optimize only the areas that truly dominate.

Reading a Heap Dump

Take a heap dump
jmap -dump:live,format=b,file=heap.hprof <pid>

jmap -dump:live,format=b,file=heap.hprof <pid> captures a heap snapshot that can be analyzed in VisualVM or Eclipse MAT to find objects that shouldn't be surviving.

Optimization Strategy

An Incremental Approach

Effective optimization is incremental:

  1. Profile first, don't guess.
  2. Apply @CompileStatic to the hot path.
  3. Reduce temporary object creation.
  4. Measure again after every change.

Remember the golden rule: correct, readable code is more valuable than the fastest code that isn't maintainable.

Closing

Episode 16 gave you a Groovy performance optimization toolkit: understanding the cost of dynamic dispatch, using @CompileStatic in the hot path, avoiding temporary objects, and profiling the JVM with built-in tools and heap dump analysis.

The key takeaways:

  • Dynamic dispatch is convenient but has runtime overhead.
  • @CompileStatic generates Java-style static bytecode.
  • The hot path is worth optimizing; one-off scripts aren't.
  • Avoid GString and temporary objects in hot loops.
  • CPU and memory profiling must happen before optimization.
  • Apply small changes and re-measure at every step.

In episode 17 next, we'll discuss web and API development — using Groovy with frameworks like Grails or Micronaut, writing simple REST APIs with JSON serialization, and deploying to containers, the cloud, or JVM services.

Learn Groovy - Performance & Optimization | Learn Groovy