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.

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.
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:
@CompileStatic tells the Groovy compiler to perform type checking and generate static bytecode:
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.
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.
The performance difference can be significant in the hot path. A simple 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.
One of the hidden overheads is the creation of temporary objects:
int, long, and double in @CompileStatic methods.StringBuilder.GString is very convenient, but in a hot path using StringBuilder is faster:
@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.
Profiling starts with measurement. The JDK ships with built-in tools:
jcmd <pid> GC.heap_info
jconsolejcmd <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.
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.
Effective optimization is incremental:
@CompileStatic to the hot path.Remember the golden rule: correct, readable code is more valuable than the fastest code that isn't maintainable.
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:
@CompileStatic generates Java-style static bytecode.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.