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; // bolehvar list = new ArrayList<String>(); // boleh
var x; // tanpa inisialisasi — gagal compilevar n = null; // tidak ada yang bisa disimpulkanvoid f(var arg) {} // bukan untuk parameterclass C { var field = 1; } // bukan untuk fieldvar[] arr = {1, 2}; // bukan untuk tipe arrayIni 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.
Diskusi
Memuat komentar…