Setiap objek yang kita alokasi berakhir menjadi beban Garbage Collector. Pada aplikasi server dengan throughput tinggi, jutaan alokasi kecil per detik dapat memicu GC pause yang menyebabkan latensi p99 melonjak dari 5 ms ke 200 ms. Strategi dasarnya: alokasi sedikit, alokasi sekali pakai ulang, dan pilih struktur data yang hemat. Pada chapter ini kita akan membedah lima pola konkret: StringBuilder untuk konkatenasi, object reuse dengan reset buffer, inline value class untuk tipe pembungkus, pooling untuk instance mahar, dan array reuse dengan operasi clear().
StringBuilder vs Konkatenasi + #
Operator + pada String menciptakan instance String baru setiap kali dipanggil. Dalam loop 100 iterasi, ekspresi s += "x" menghasilkan 101 instance String (ditambah StringBuilder internal untuk setiap +=). StringBuilder eksplisit mengubah ini menjadi 1 instance final.
fun concatPlus(n: Int): String {
var s = ""
for (i in 0 until n) s += "x" // n+1 alokasi
return s
}
fun concatBuilder(n: Int): String {
val sb = StringBuilder(n) // pre-size
for (i in 0 until n) sb.append('x')
return sb.toString() // 1 alokasi
}Aturan praktis: bila konkatenasi melibatkan lebih dari 3 fragmen atau berada dalam loop, gunakan StringBuilder. Untuk konkatenasi 2-3 fragmen di luar loop, kompilator Kotlin secara otomatis mengoptimalkan menjadi StringBuilder internal.
Reuse Object dengan Buffer Reset #
Pola umum: sebuah kelas memerlukan buffer kerja (mis. StringBuilder, ByteArray, CharBuffer). Alih-alih membuat instance baru tiap pemanggilan, simpan buffer sebagai field dan reset dengan setLength(0) atau clear().
// avoiding_unnecessary_allocations.kt — Pola mengurangi alokasi objek
import kotlin.random.Random
// === 1. StringBuilder vs + concatenation ===
fun concatPlus(n: Int): String {
var s = ""
for (i in 0 until n) s += "x"
return s
}
fun concatBuilder(n: Int): String {
val sb = StringBuilder(n)
for (i in 0 until n) sb.append('x')
return sb.toString()
}
// === 2. Reuse object: Formatter dengan buffer ===
class Formatter {
private val sb = StringBuilder(64)
fun format(vararg parts: Any): String {
sb.setLength(0) // reset, tidak buat baru
for (p in parts) {
sb.append(p).append(' ')
}
return sb.toString().trim()
}
}
// === 3. Value class: no boxing ===
@JvmInline
value class UserId(val raw: Long)
@JvmInline
value class Email(val value: String)
data class Pengguna(val id: UserId, val email: Email, val nama: String)
// === 4. Pooling: reuse Random ===
object RngPool {
private val rnd = Random(42)
fun nextInt(bound: Int): Int = rnd.nextInt(bound)
}
// === 5. Array reuse: clear vs new ===
class Akumulator {
private val data = IntArray(16)
private var size = 0
fun add(value: Int) {
if (size >= data.size) return // sengaja tidak grow untuk demo
data[size++] = value
}
fun clear() { size = 0 } // tidak alokasi ulang
fun sum(): Int {
var s = 0
for (i in 0 until size) s += data[i]
return s
}
}
fun main() {
println("=== Avoiding Unnecessary Allocations ===")
println()
// === 1. Concatenation comparison ===
println("=== 1. StringBuilder vs + ===")
val n = 100
val s1 = concatPlus(n)
val s2 = concatBuilder(n)
println(" concatPlus($n): length=${s1.length} (alokasi ${n + 1} String)")
println(" concatBuilder($n): length=${s2.length} (alokasi 1 String)")
println()
// === 2. Formatter reuse ===
println("=== 2. Formatter Reuse ===")
val fmt = Formatter()
println(" ${fmt.format("Budi", "umur", 30, "aktif")}")
println(" ${fmt.format("Siti", "umur", 25, "tidak aktif")}")
println(" -> Formatter memakai 1 StringBuilder untuk semua panggilan")
println()
// === 3. Value class: no runtime overhead ===
println("=== 3. Value Class (inline) ===")
val u1 = Pengguna(UserId(1001L), Email("[email protected]"), "Budi")
val u2 = Pengguna(UserId(1002L), Email("[email protected]"), "Siti")
println(" $u1")
println(" $u2")
println(" -> UserId dan Email di-erase menjadi Long/String saat runtime")
println()
// === 4. Pool: Random ===
println("=== 4. Random Pool ===")
val dice = (1..5).map { RngPool.nextInt(6) + 1 }
println(" 5x lempar dadu (seeded): $dice")
println(" -> Random tunggal, tidak buat instance baru tiap panggilan")
println()
// === 5. Array reuse ===
println("=== 5. Array Reuse (Akumulator) ===")
val acc = Akumulator()
for (i in 1..10) acc.add(i)
println(" Batch 1: sum=${acc.sum()}, size=10")
acc.clear()
for (i in 100..105) acc.add(i)
println(" Batch 2 (setelah clear): sum=${acc.sum()}, size=6")
println(" -> Tidak ada alokasi IntArray baru antar batch")
println()
// === 6. Aturan alokasi ===
println("=== 6. Aturan Mengurangi Alokasi ===")
val rules = listOf(
"1. StringBuilder untuk konkatenasi > 3 fragmen",
"2. Reuse object (reset via setLength / clear) alih-alih new",
"3. value class untuk tipe pembungkus (UserId, Email)",
"4. Pool instance mahar seperti Random, Pattern, DecimalFormat",
"5. Sequence untuk pipeline koleksi besar (lazy, no intermediate list)",
"6. Hindari autoboxing: gunakan IntArray bukan Array<Int>"
)
rules.forEach { println(" " + it) }
}Output yang Dihasilkan:
=== Avoiding Unnecessary Allocations ===
=== 1. StringBuilder vs + ===
concatPlus(100): length=100 (alokasi 101 String)
concatBuilder(100): length=100 (alokasi 1 String)
=== 2. Formatter Reuse ===
Budi umur 30 aktif
Siti umur 25 tidak aktif
-> Formatter memakai 1 StringBuilder untuk semua panggilan
=== 3. Value Class (inline) ===
Pengguna(id=UserId(raw=1001), email=Email([email protected]), nama=Budi)
Pengguna(id=UserId(raw=1002), email=Email([email protected]), nama=Siti)
-> UserId dan Email di-erase menjadi Long/String saat runtime
=== 4. Random Pool ===
5x lempar dadu (seeded): [6, 1, 6, 3, 2]
-> Random tunggal, tidak buat instance baru tiap panggilan
=== 5. Array Reuse (Akumulator) ===
Batch 1: sum=55, size=10
Batch 2 (setelah clear): sum=615, size=6
-> Tidak ada alokasi IntArray baru antar batch
=== 6. Aturan Mengurangi Alokasi ===
1. StringBuilder untuk konkatenasi > 3 fragmen
2. Reuse object (reset via setLength / clear) alih-alih new
3. value class untuk tipe pembungkus (UserId, Email)
4. Pool instance mahar seperti Random, Pattern, DecimalFormat
5. Sequence untuk pipeline koleksi besar (lazy, no intermediate list)
6. Hindari autoboxing: gunakan IntArray bukan Array<Int>Mari kita bedah pola yang didemokan:
- StringBuilder vs
+:concatPlus(100)menghasilkan 101 instanceString(loop membuatsbaru tiap iterasi).concatBuilder(100)hanya menghasilkan 1 instanceStringfinal. Panjang keluaran sama (100), tetapi tekanan GC jauh berbeda. - Formatter reuse:
Formattermenyimpan satuStringBuildersebagai field. Setiap panggilanformat()memanggilsetLength(0)untuk reset — tanpa alokasi baru. Tanpa pola ini, 1000 panggilan format menghasilkan 1000StringBuildersementara. - Value class:
UserId(1001L)danEmail("[email protected]")secara semantik adalah tipe berbeda, tetapi di bytecode keduanya di-erase menjadiLongdanString. Tidak ada instanceUserIddi heap saat runtime — nol overhead. - Random pool:
object RngPoolmenyimpan satuRandomseeded. Tanpa pool, pola umumRandom()tiap function menciptakan instance baru, padahalRandomcukup mahal untuk dikonstruksi. - Array reuse:
Akumulator.clear()hanya mengatursize = 0tanpa mengalokasikanIntArraybaru. Batch 1 dan batch 2 berbagi buffer yang sama; beda dengan polaIntArray(16)di dalam function yang membuat instance baru tiap panggilan.
Inline Value Class untuk Tipe Pembungkus #
Value class (@JvmInline value class) adalah fitur Kotlin yang menghapus instance class pada saat runtime. Tipe UserId(val raw: Long) secara semantik berbeda dari Email(val value: String), tetapi keduanya di-erase menjadi Long dan String di bytecode. Manfaatnya: type safety di compile-time, nol overhead di runtime.
@JvmInline
value class UserId(val raw: Long)
fun findUser(id: UserId): Pengguna = ...
// findUser(Email("[email protected]")) // gagal compile
// findUser(99L) // gagal compileTanpa value class, banyak kode memakai Long polos untuk ID, lalu rentan tertukar dengan Long lain (mis. OrderId). Value class memberikan type safety tanpa biaya runtime.
Pooling dan Sequence #
Untuk objek mahar seperti Random, Pattern, DecimalFormat, simpan sebagai singleton object atau field. Untuk pipeline koleksi besar, gunakan asSequence() agar operasi map/filter dieksekusi secara lazy tanpa membuat list antara.
Referensi Lanjutan #
- Inline value classes
— panduan
value classKotlin untuk menghindari alokasi objek wrapper - Inline functions
— dokumentasi
inlinefunction untuk mengurangi overhead lambda dan kelas anonim
Selanjutnya #
Setelah memahami pola mengurangi alokasi, kita perlu tahu kapan inline function membantu — dan kapan justru memperbesar bytecode. Materi tersebut dibahas pada chapter inline Functions Performance.