Java Modern untuk Produksi
EN
Materi 6 dari 9 · 12m

Biaya-biaya kecil yang menumpuk

Konkatenasi String di dalam loop, apa yang dilakukan text block pada indentasimu, di mana var berhenti bekerja, dan apa yang sebenarnya dioptimalkan garbage collector.

Tidak ada yang eksotis di sini. Ini kumpulan fakta kecil yang membedakan kode yang lolos load test dari kode yang terbaca sama persis tapi tidak lolos.

+= pada String di dalam loop

String out = "";
for (String s : lines) {
out += s; // String baru setiap iterasi
}

String itu immutable, jadi setiap += mengalokasikan yang baru dan menyalin semua yang sudah ada. Untuk n baris itu kerja kuadratik dan n objek yang dibuang. Compiler mengoptimalkan konkatenasi di dalam satu ekspresi; dia tidak bisa mengangkat builder keluar dari loop yang bentuknya tidak dia lihat.

StringBuilder out = new StringBuilder();
for (String s : lines) out.append(s);
String result = out.toString();

StringBuilder menumbuhkan satu buffer. Output sama, kerja linear. Pakai String.join atau Collectors.joining kalau kamu cuma menempelkan satu koleksi — lebih jelas, dan di dalamnya melakukan hal yang sama.

Konsekuensinya: a + b + c dalam satu ekspresi tidak masalah. Yang menyakitkan itu loop-nya.

Text block dan indentasi yang cuma kebetulan

String query = """
SELECT id, status
FROM orders
WHERE tenant = ?
""";

Compiler membuang spasi depan yang sama-sama dimiliki semua baris — indentasi minimum di antara seluruh baris, termasuk baris penutupnya. Jadi blok di atas menghasilkan baris tanpa spasi depan, dan indentasi yang kamu pakai untuk merapikannya dengan kode sekitarnya tidak berbiaya apa pun.

Dua konsekuensi yang perlu diketahui. Penutup """ ikut dalam perhitungan itu: menaruhnya lebih ke kiri dari isinya justru menambahkan indentasi kembali ke setiap baris. Dan newline di akhir ikut disertakan, kecuali kamu mengakhiri baris terakhir dengan \.

Di mana var berhenti

var menyimpulkan dari inisialisasinya, jadi dia butuh inisialisasi, dan tipenya harus bisa dinyatakan:

var count = 0; // boleh
var list = new ArrayList<String>(); // boleh
var x; // tanpa inisialisasi — gagal compile
var n = null; // tidak ada yang bisa disimpulkan
void f(var arg) {} // bukan untuk parameter
class C { var field = 1; } // bukan untuk field
var[] arr = {1, 2}; // bukan untuk tipe array

Ini fitur untuk variabel lokal: variabel lokal, indeks for, variabel try-with-resources, dan parameter lambda (semuanya atau tidak sama sekali). Bukan field, bukan parameter, bukan tipe kembalian — justru itu yang dibaca orang lain untuk memahami kodemu, dan itu sebabnya bahasanya menolak.

Untuk apa collector itu

G1 adalah collector yang menyeimbangkan throughput dan pause: dia berusaha menjaga pause di bawah target sambil mengumpulkan secara konkuren, dan dia rela memakai lebih banyak CPU dan heap untuk itu. Ini default yang wajar untuk sebuah service.

Artinya dalam praktik: angka yang perlu diperhatikan bukan “berapa memori terpakai” tapi waktu pause dan laju alokasi. Service yang mengalokasikan cukup keras sampai G1 sibuk akan terlihat sebagai CPU yang habis di thread GC, bukan sebagai out-of-memory. Dan OutOfMemoryError di service yang berjalan lama hampir tidak pernah berarti heap-nya terlalu kecil — itu berarti ada yang menahan objek yang seharusnya sudah jadi sampah, dan perbaikannya heap dump, bukan -Xmx.

ZGC adalah alternatifnya kalau target pause-nya memang milidetik satu digit; dia menukar itu dengan overhead CPU dan memori yang lebih besar. Jangan ganti collector berdasarkan dugaan — ukur dulu distribusi pause-nya, karena jawabannya biasanya GC bukan masalahnya.

Dan satu yang sama sekali bukan soal performa

List.of("a", "b") mengembalikan list immutable, bukan ArrayList. Memanggil add padanya melempar UnsupportedOperationException saat runtime, dengan stack trace yang menunjuk kode yang kelihatan sangat wajar. Kalau sesuatu memang perlu bisa diubah, katakan: new ArrayList<>(List.of("a", "b")).


Pengukuran di balik semua ini, dan yang ternyata tidak berpengaruh: Pola Java Modern untuk Produksi.