Performance e um aspecto critico em qualquer aplicacao, seja um app Android que precisa manter 60 FPS ou um servidor backend processando milhares de requisicoes por segundo. Kotlin, rodando na JVM, herda tanto as otimizacoes poderosas do HotSpot quanto as armadilhas de performance comuns a linguagens JVM. Neste guia, vamos explorar tecnicas concretas para identificar gargalos, otimizar codigo Kotlin, configurar a JVM corretamente e aplicar padroes que fazem a diferenca em aplicacoes de producao. Cada dica vem com exemplos reais e mensuracoes para que voce entenda o impacto de cada otimizacao.
Profiling: Identificando Gargalos
Antes de otimizar, e essencial medir. Otimizar sem dados e como atirar no escuro. As principais ferramentas de profiling para Kotlin sao:
O JVisualVM vem com o JDK e permite analisar uso de memoria, threads e CPU em tempo real. O IntelliJ Profiler integra profiling diretamente na IDE, facilitando a correlacao com o codigo. O async-profiler e uma ferramenta de baixo overhead ideal para servidores em producao. O Android Profiler do Android Studio mostra CPU, memoria, rede e energia em apps Android.
// Medicao simples de tempo no codigo
inline fun <T> measureTimeAndReturn(
label: String,
block: () -> T
): T {
val inicio = System.nanoTime()
val resultado = block()
val duracao = (System.nanoTime() - inicio) / 1_000_000.0
println("[$label] Executado em %.2f ms".format(duracao))
return resultado
}
// Uso
val produtos = measureTimeAndReturn("Buscar produtos") {
repository.buscarTodos()
}
Colecoes e Sequencias
Uma das armadilhas mais comuns em Kotlin e o uso incorreto de operacoes encadeadas em colecoes. Cada operacao como map, filter e flatMap cria uma nova lista intermediaria:
// Ineficiente: cria 3 listas intermediarias
val resultado = produtos
.filter { it.ativo } // Lista 1
.map { it.nome.uppercase() } // Lista 2
.sortedBy { it } // Lista 3
.take(10) // Lista 4
// Eficiente: usa Sequence (processamento lazy)
val resultado = produtos.asSequence()
.filter { it.ativo }
.map { it.nome.uppercase() }
.sortedBy { it }
.take(10)
.toList() // Materializa apenas no final
Sequences processam elemento por elemento (pipeline vertical), evitando colecoes intermediarias. A regra pratica: use asSequence() quando tem 3 ou mais operacoes encadeadas ou quando a colecao e grande.
// Benchmark comparativo
fun benchmarkColecoes() {
val lista = (1..1_000_000).toList()
// Com listas: ~45ms, aloca ~3 listas de 1M elementos
val r1 = measureTimeMillis {
lista.filter { it % 2 == 0 }.map { it * 2 }.take(100)
}
// Com sequences: ~1ms, processa apenas 200 elementos
val r2 = measureTimeMillis {
lista.asSequence().filter { it % 2 == 0 }.map { it * 2 }.take(100).toList()
}
println("Listas: ${r1}ms, Sequences: ${r2}ms")
}
Inline Functions e Lambdas
Cada lambda em Kotlin gera uma classe anonima na JVM, com custo de alocacao e invocacao. A keyword inline elimina esse custo:
// Sem inline: gera objeto Function a cada chamada
fun <T> executar(bloco: () -> T): T {
return bloco()
}
// Com inline: o corpo da lambda e copiado no call site
inline fun <T> executarInline(bloco: () -> T): T {
return bloco()
}
// Inline e especialmente importante para funcoes de alta ordem chamadas frequentemente
inline fun <T> List<T>.filterFast(predicate: (T) -> Boolean): List<T> {
val resultado = ArrayList<T>(size / 2)
for (item in this) {
if (predicate(item)) {
resultado.add(item)
}
}
return resultado
}
As funcoes da stdlib como let, run, apply, also, map e filter ja sao inline, entao nao ha overhead ao usa-las.
Gerenciamento de Memoria
Evitando Alocacoes Desnecessarias
// Ruim: cria novo objeto a cada chamada
fun formatarPreco(valor: Double): String {
val formatter = DecimalFormat("#,##0.00")
return "R$ ${formatter.format(valor)}"
}
// Bom: reutiliza o formatter
private val precoFormatter = DecimalFormat("#,##0.00")
fun formatarPreco(valor: Double): String {
return "R$ ${precoFormatter.format(valor)}"
}
// Ruim: boxing de primitivos
val numeros: List<Int> = listOf(1, 2, 3) // Boxing Int -> Integer
// Bom: usa IntArray para evitar boxing
val numeros = intArrayOf(1, 2, 3) // Primitivos puros
Value Classes para Zero-Cost Abstractions
// Value class: zero overhead em runtime
@JvmInline
value class ClienteId(val valor: Long)
@JvmInline
value class Email(val valor: String) {
init {
require(valor.contains("@")) { "Email invalido" }
}
}
@JvmInline
value class Reais(val centavos: Long) {
operator fun plus(outro: Reais) = Reais(centavos + outro.centavos)
fun toDouble() = centavos / 100.0
}
// Uso: type safety sem custo de alocacao
fun buscarCliente(id: ClienteId): Cliente? {
return repository.findById(id.valor)
}
Value classes sao “desembrulhadas” em compilacao, mantendo o tipo primitivo subjacente.
Coroutines Eficientes
// Ruim: cria coroutine para cada item sequencialmente
suspend fun processarItens(itens: List<Item>) {
for (item in itens) {
launch {
processar(item)
}.join() // Espera cada um individualmente
}
}
// Bom: paralelismo controlado
suspend fun processarItens(itens: List<Item>) = coroutineScope {
itens.map { item ->
async(Dispatchers.Default) {
processar(item)
}
}.awaitAll()
}
// Melhor: paralelismo limitado para nao sobrecarregar
suspend fun processarItens(itens: List<Item>) {
val semaforo = Semaphore(permits = 10)
coroutineScope {
itens.map { item ->
async(Dispatchers.IO) {
semaforo.withPermit {
processar(item)
}
}
}.awaitAll()
}
}
Flow Otimizado
// Ruim: buffer padrao pode causar backpressure
val dados = flowDeDados()
.collect { processar(it) }
// Bom: buffer para desacoplar produtor e consumidor
val dados = flowDeDados()
.buffer(capacity = 64)
.collect { processar(it) }
// Conflated: descarta valores intermediarios se o consumidor e lento
val sensorData = sensorFlow()
.conflate()
.collect { atualizarUI(it) }
// Batch processing com chunked
flowDeEventos()
.chunked(100)
.collect { lote ->
repository.salvarEmLote(lote)
}
Otimizacao de String
Operacoes com strings sao surpreendentemente custosas:
// Ruim: concatenacao em loop cria muitos objetos String
fun construirRelatorio(itens: List<Item>): String {
var resultado = ""
for (item in itens) {
resultado += "${item.nome}: ${item.valor}\n" // Nova String a cada iteracao
}
return resultado
}
// Bom: StringBuilder
fun construirRelatorio(itens: List<Item>): String {
return buildString(itens.size * 50) { // Capacidade estimada
for (item in itens) {
append(item.nome)
append(": ")
append(item.valor)
appendLine()
}
}
}
// Bom: joinToString para casos simples
fun construirRelatorio(itens: List<Item>): String {
return itens.joinToString("\n") { "${it.nome}: ${it.valor}" }
}
Configuracao da JVM
Parametros da JVM impactam diretamente a performance:
// Parametros recomendados para servidores
// -XX:+UseG1GC -> GC equilibrado para latencia e throughput
// -XX:MaxRAMPercentage=75.0 -> Usa 75% da RAM disponivel
// -XX:+UseContainerSupport -> Respeita limites de container
// -XX:+ExitOnOutOfMemoryError -> Reinicia em OOM
// -XX:+UseStringDeduplication -> Deduplica strings identicas no heap
// -XX:+OptimizeStringConcat -> Otimiza concatenacao de strings
// Para Android, use o Baseline Profile
// Compila os caminhos mais comuns em AOT na instalacao
@ExperimentalBaselineProfilesApi
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun startup() {
rule.collectBaselineProfile(
packageName = "com.exemplo.app"
) {
startActivityAndWait()
}
}
}
Cache Estrategico
// Cache em memoria com LRU
class LruCache<K, V>(private val maxSize: Int) {
private val cache = LinkedHashMap<K, V>(maxSize, 0.75f, true)
@Synchronized
fun get(key: K): V? = cache[key]
@Synchronized
fun put(key: K, value: V) {
cache[key] = value
if (cache.size > maxSize) {
val primeiraChave = cache.keys.first()
cache.remove(primeiraChave)
}
}
}
// Cache com coroutines e expiracao
class AsyncCache<K, V>(
private val ttlMs: Long = 60_000L
) {
private data class Entry<V>(val valor: V, val timestamp: Long)
private val cache = ConcurrentHashMap<K, Entry<V>>()
suspend fun getOrLoad(key: K, loader: suspend () -> V): V {
val entry = cache[key]
if (entry != null && System.currentTimeMillis() - entry.timestamp < ttlMs) {
return entry.valor
}
val valor = loader()
cache[key] = Entry(valor, System.currentTimeMillis())
return valor
}
fun invalidar(key: K) {
cache.remove(key)
}
}
Boas Praticas de Performance em Kotlin
- Meça antes de otimizar: use profiler para identificar gargalos reais. Otimizacao prematura e a raiz de muitos problemas.
- Use Sequences para cadeias longas: a partir de 3 operacoes encadeadas em colecoes grandes, Sequences fazem diferenca.
- Evite boxing desnecessario: use
IntArray,LongArrayem vez deList<Int>quando performance importa. - Prefira value classes: para wrappers de primitivos, value classes eliminam alocacao em runtime.
- Reutilize objetos caros: formatters, regex compilados e conexoes de banco devem ser reutilizados.
- Configure o GC adequadamente: G1GC para servidores, ZGC para baixissima latencia.
- Use cache com sabedoria: cache acelera leituras mas adiciona complexidade de invalidacao.
Erros Comuns e Armadilhas
- Otimizar sem medir: “acho que isso e lento” nao e base para otimizacao. Sempre use dados de profiling.
- Ignorar alocacoes em loops: criar objetos dentro de loops executados milhoes de vezes e um gargalo classico.
- Coroutines sem limite: lancar milhares de coroutines com
Dispatchers.IOpode esgotar o pool de threads. Use semaforos. - Cache sem invalidacao: dados cacheados desatualizados podem causar bugs sutis. Defina TTL ou invalidacao explicita.
- String concatenation em hotpath: em caminhos executados frequentemente, concatenacao cria pressao desnecessaria no GC.
- Regex nao compilado:
Regexcompilado uma vez e reutilizado e ordens de magnitude mais rapido que compilar a cada uso.
Conclusao e Proximos Passos
Performance em Kotlin e um equilibrio entre codigo idiomatico e otimizacoes pontuais. A maioria do codigo deve priorizar legibilidade, e otimizacoes devem ser aplicadas apenas onde profiling indica necessidade real. As tecnicas apresentadas neste guia – sequences, inline functions, value classes, cache e configuracao da JVM – cobrem os cenarios mais comuns. Para ir alem, explore ferramentas de APM como New Relic ou Datadog para monitoramento em producao e consulte nossos guias sobre Docker e microservicos para otimizar a infraestrutura como um todo.