Aplikasi Kotlin berjalan di atas JVM, dan setiap objek yang kita buat membawa biaya memori yang sering luput dari perhatian. Sebuah Integer berukuran 16 byte — padahal nilai int primitif hanya 4 byte. Daftar satu juta angka sebagai Array<Int> mengonsumsi 23 MB, sementara IntArray hanya butuh 4 MB. Pada chapter ini kita akan membedah komponen overhead memori (object header, reference, alignment), membandingkan tipe primitif dengan versi boxed, serta mendemokan string deduplication yang dapat menghemat puluhan MB di aplikasi yang memproses nama, kode, atau label berulang.
Komponen Overhead Memori #
Setiap objek di JVM terdiri dari tiga komponen: object header, field data, dan padding alignment. Object header biasanya 12–16 byte (8 byte untuk pointer kelas pada mode CompressedOops, 4 byte untuk hash/lock state). Bila sebuah kelas hanya punya satu field Int, total instance tetap 16 byte karena padding ke kelipatan 8.
| Komponen | Ukuran | Catatan |
|---|---|---|
| Object header (mark word) | 8 byte | Hash, lock, GC age |
| Class pointer | 4–8 byte | 4 byte dengan CompressedOops |
| Field data | sesuai tipe | Int = 4, Long = 8, reference = 4–8 |
| Padding | 0–7 byte | Kelipatan 8 byte |
Konsekuensi langsung: satu instance Integer dengan nilai 5 mengonsumsi 16 byte — empat kali lipat dari primitif int. Untuk koleksi besar, perbedaan ini menjadi sangat signifikan.
Primitif vs Boxed #
IntArray menyimpan nilai primitif secara berurutan di satu blok memori. Array<Int> menyimpan reference ke objek Integer individual; setiap elemen adalah pointer 8 byte plus instance Integer 16 byte. Mari kita ukur perbedaannya secara konkret:
// memory_usage.kt — Membandingkan overhead memori tipe primitif vs boxed
import kotlin.random.Random
fun main() {
println("=== Memory Usage: Primitive vs Boxed ===")
println()
// === 1. Estimasi ukuran IntArray vs Array<Int> ===
println("=== 1. Estimasi Ukuran ===")
val n = 1_000_000
val intArray = IntArray(n) { it }
val boxedArray = Array(n) { it }
// Object header biasanya 12-16 byte; IntArray menyimpan primitif rapat
val intArrayBytes = 16 + n * 4 // header + 4 byte per Int
val boxedArrayBytes = 16 + n * 8 + n * 16 // header + 8 byte reference + 16 byte Integer per elemen
println(" IntArray($n): ~${intArrayBytes / 1024} KB")
println(" Array<Int>($n): ~${boxedArrayBytes / 1024} KB")
println(" Rasio: ~${boxedArrayBytes / intArrayBytes}x lebih besar")
println()
// === 2. String deduplication simulation ===
println("=== 2. String Deduplication ===")
val rnd = Random(42)
val daftarNama = listOf("Budi", "Siti", "Dewi", "Andi", "Rina")
val denganDuplikat = (1..1000).map { daftarNama[rnd.nextInt(daftarNama.size)] }
val tanpaDuplikat = denganDuplikat.toSet().toList()
println(" Total string sebelum dedup: ${denganDuplikat.size}")
println(" Total unik setelah dedup: ${tanpaDuplikat.size}")
println(" String unik: ${tanpaDuplikat.joinToString()}")
println(" Hemat instance: ${denganDuplikat.size - tanpaDuplikat.size}")
println()
// === 3. Identity check: string interning ===
println("=== 3. String Interning ===")
val a = "halo"
val b = "halo"
val c = String("halo".toCharArray())
val cInterned = c.intern()
println(" a === b: ${a === b} // literal, di-pool otomatis")
println(" a === c: ${a === c} // String baru, instance berbeda")
println(" a === c.intern(): ${a === cInterned} // intern pakai pool")
println()
// === 4. Wrapper autoboxing di collection ===
println("=== 4. Boxing di MutableMap ===")
val hitung = mutableMapOf<Int, Int>()
for (i in 1..5) {
hitung[i] = (hitung[i] ?: 0) + 1
}
println(" Map setelah inkrementasi: $hitung")
println(" Setiap put() memerlukan boxing Int -> Integer (16 byte)")
println()
// === 5. Tabel ringkas overhead ===
println("=== 5. Ringkasan Overhead ===")
val rows = listOf(
Triple("IntArray(n)", "4n + 16", "Rapat, primitif"),
Triple("LongArray(n)", "8n + 16", "Primitif 64-bit"),
Triple("Array<Int>(n)", "24n + 16", "8 ref + 16 Integer"),
Triple("Array<String>(n) duplikat", "~8n + 16n instance", "Boros; deduplikasi"),
Triple("Array<String>(n) unik", "8n + 16 per unik", "Lebih hemat dengan intern()")
)
rows.forEach { (tipe, bytes, catatan) ->
println(" %-32s %s // %s".format(tipe, bytes, catatan))
}
println()
// === 6. Aturan praktis ===
println("=== 6. Aturan Praktis ===")
val rules = listOf(
"1. Pakai IntArray/LongArray/DoubleArray untuk angka massal",
"2. Hindari Array<Int> jika elemen banyak (autoboxing)",
"3. String berulang -> gunakan intern() atau konstanta",
"4. Map<Int, V> menyebabkan boxing key; pertimbangkan ObjectIntMap",
"5. Aktifkan -XX:+UseStringDeduplication di JVM (G1 GC)"
)
rules.forEach { println(" " + it) }
}Output yang Dihasilkan:
=== Memory Usage: Primitive vs Boxed ===
=== 1. Estimasi Ukuran ===
IntArray(1000000): ~3906 KB
Array<Int>(1000000): ~23437 KB
Rasio: ~5x lebih besar
=== 2. String Deduplication ===
Total string sebelum dedup: 1000
Total unik setelah dedup: 5
String unik: Andi, Budi, Siti, Dewi, Rina
Hemat instance: 995
=== 3. String Interning ===
a === b: true // literal, di-pool otomatis
a === c: false // String baru, instance berbeda
a === c.intern(): true // intern pakai pool
=== 4. Boxing di MutableMap ===
Map setelah inkrementasi: {1=1, 2=1, 3=1, 4=1, 5=1}
Setiap put() memerlukan boxing Int -> Integer (16 byte)
=== 5. Ringkasan Overhead ===
IntArray(n) 4n + 16 // Rapat, primitif
LongArray(n) 8n + 16 // Primitif 64-bit
Array<Int>(n) 24n + 16 // 8 ref + 16 Integer
Array<String>(n) duplikat ~8n + 16n instance // Boros; deduplikasi
Array<String>(n) unik 8n + 16 per unik // Lebih hemat dengan intern()
=== 6. Aturan Praktis ===
1. Pakai IntArray/LongArray/DoubleArray untuk angka massal
2. Hindari Array<Int> jika elemen banyak (autoboxing)
3. String berulang -> gunakan intern() atau konstanta
4. Map<Int, V> menyebabkan boxing key; pertimbangkan ObjectIntMap
5. Aktifkan -XX:+UseStringDeduplication di JVM (G1 GC)Mari kita bedah hasil di atas:
- Estimasi Ukuran:
IntArray1 juta elemen hanya butuh ~3906 KB, sementaraArray<Int>versi boxed butuh ~23437 KB. Rasio ~5x karena setiapIntegerinstance menambah 16 byte overhead plus 8 byte reference. - String Deduplication: 1000 entri acak dari 5 nama unik menghasilkan 1000 instance
String, tetapi hanya 5 yang unik.toSet()menghilangkan duplikat — bila data dibanding dengan identity check===, kita bisa membuktikan banyak instance berbeda menyimpan teks sama. - String Interning: Literal
"halo"otomatis masuk pool string, sehinggaa === bbernilaitrue.String("halo".toCharArray())membuat instance baru, sehinggaa === cbernilaifalse. Setelah.intern(), instance tersebut dikembalikan ke pool, sehinggaa === c.intern()kembalitrue. - Boxing di MutableMap: Setiap operasi
hitung[i] = ...melakukan autoboxingIntkeIntegeruntuk key dan value. Untuk 5 operasi mungkin sepele, tetapi pada loop satu juta put, ini menambah ~16 MB beban GC. - Ringkasan Overhead: Tabel ini memberikan rumus cepat memperkirakan konsumsi memori.
Array<Int>24x lebih boros per elemen dibandingIntArraykarena kombinasi reference + Integer object.
String Deduplication dan Interning #
String sering menyumbang porsi besar heap. Pada aplikasi yang memproses log, nama pelanggan, atau kode produk, banyak instance String berisi teks identik tetapi tersebar di banyak lokasi memori. JVM menyediakan dua mekanisme deduplikasi:
| Mekanisme | Kapan bekerja | Cara pakai |
|---|---|---|
| String pool (literal) | Compile-time | Tulis "halo" sebagai literal |
String.intern() | Runtime, eksplisit | Panggil .intern() pada string dinamis |
| G1 string deduplication | GC time, otomatis | Flag -XX:+UseStringDeduplication |
Boxing pada Map dan List #
Generic collection Kotlin/Java memakai tipe reference, sehingga MutableMap<Int, V> selalu membox key Int menjadi Integer. Untuk volume tinggi, perbedaan ini kentara. Library pihak ketiga seperti Eclipse Collections atau fastutil menyediakan IntIntMap yang menyimpan pasangan primitif tanpa boxing — biasanya hemat 5x memori dibanding HashMap<Int, Int>.
Referensi Lanjutan #
- JetBrains Profiler — panduan profiler JetBrains untuk memantau penggunaan heap dan retensi memori
- Inline value classes
— dokumentasi
value classuntuk mereduksi overhead memori pada tipe pembungkus
Selanjutnya #
Setelah memahami komponen overhead memori, langkah berikutnya adalah mengurangi alokasi yang tidak perlu — reuse object, gunakan StringBuilder, dan pertimbangkan inline value class. Materi tersebut dibahas pada chapter Avoiding Unnecessary Allocations.