Container mengemas aplikasi Java beserta seluruh lingkungannya — tetapi JVM bukan proses sederhana yang bisa dilempar begitu saja ke dalam kotak berbatas memori. JVM punya heap sendiri, metaspace sendiri, dan kebiasaan lama dari era mesin fisik: membaca seluruh RAM mesin untuk menentukan ukuran heap bawaan. Di dalam container berbatas 512 MB, JVM yang tidak diberi tahu akan mencoba mengalokasikan seolah mesin punya belasan gigabyte — dan kernel mematikan prosesnya lewat OOMKill. Chapter ini membahas dua keputusan inti containerisasi Java: memilih base image yang tepat dan mengatur heap relatif terhadap batas memori container.
Memilih Base Image #
Pilihan FROM menentukan ukuran akhir image, kebiasaan pembaruan keamanan, dan alat diagnosis yang tersedia di dalamnya:
| Base image | Ukuran khas | Isi | Kapan dipakai |
|---|---|---|---|
eclipse-temurin:25-jdk | besar | JDK lengkap | build/development |
eclipse-temurin:25-jre | sedang | JRE saja | aplikasi berbasis classpath |
eclipse-temurin:25-jre-alpine | kecil | JRE di Alpine musl | image minimal |
image hasil jlink + FROM scratch | paling kecil | hanya modul terpakai | kontrol penuh |
Tidak ada pemenang mutlak: Alpine kecil tetapi memakai libc musl yang sesekali menimbulkan perbedaan perilaku, sedangkan image jlink buatan sendiri ramping namun harus dirakit ulang setiap ada pembaruan keamanan JDK. Untuk runtime jlink yang sudah kita bangun di chapter sebelumnya, FROM scratch menuntut ENTRYPOINT menunjuk launcher static-friendly — mulailah dari eclipse-temurin:25-jre, lalu rampingkan setelah pipeline produksi stabil.
JVM yang Sadar Container #
JDK modern mendeteksi batas cgroup container secara bawaan. Perilakunya bisa diperiksa dari flag JVM di mesin Linux:
java -XX:+PrintFlagsFinal -version | rg 'MaxRAMPercentage|UseContainerSupport'Output yang Dihasilkan:
double MaxRAMPercentage = 25.000000 {product} {default}
bool UseContainerSupport = true {product} {default}Artinya: JVM membaca batas memori container, lalu memakai 25% dari batas itu sebagai heap maksimum bawaan. Angka ini bisa diubah — dan begitu diubah, nilainya tercatat sebagai sumber {command line}:
java -XX:MaxRAMPercentage=75.0 -XX:+PrintFlagsFinal -version | rg 'MaxRAMPercentage'Output yang Dihasilkan:
double MaxRAMPercentage = 75.000000 {product} {command line}Menghitung Heap dari Batas Memori #
Persentase terasa abstrak sampai dihitung — dan bagian yang sering dilupakan adalah komponen non-heap yang ikut memakan anggaran container. Program berikut menghitung keduanya:
import java.util.LinkedHashMap;
import java.util.Map;
/** Menghitung heap dari batas memori container dengan MaxRAMPercentage. */
public class HitungHeap {
/** Komponen memori non-heap yang ikut memakan batas container (model tetap). */
static final int METASPACE_MB = 64;
static final int CODE_CACHE_MB = 48;
static final int STACK_PER_THREAD_MB = 1;
static final int DIRECT_BUFFER_MB = 32;
static final int JUMLAH_THREAD = 30;
static int heapMb(int limitMb, int persen) {
return limitMb * persen / 100;
}
static void tabelPersentase() {
System.out.println("== Heap maksimum dari MaxRAMPercentage ==");
System.out.printf("%-14s %8s %8s %8s%n", "batas container", "25%", "50%", "75%");
for (int limit : new int[]{256, 512, 1024}) {
System.out.printf("%-14s %7dMB %7dMB %7dMB%n", limit + " MB",
heapMb(limit, 25), heapMb(limit, 50), heapMb(limit, 75));
}
System.out.println();
}
static void anggaran(String judul, int limitMb, int persenHeap) {
Map<String, Integer> komponen = new LinkedHashMap<>();
komponen.put("heap (" + persenHeap + "%)", heapMb(limitMb, persenHeap));
komponen.put("metaspace", METASPACE_MB);
komponen.put("code cache", CODE_CACHE_MB);
komponen.put("thread stacks (" + JUMLAH_THREAD + " x " + STACK_PER_THREAD_MB + "MB)",
JUMLAH_THREAD * STACK_PER_THREAD_MB);
komponen.put("direct buffers", DIRECT_BUFFER_MB);
int total = 0;
System.out.println("== " + judul + " ==");
for (var e : komponen.entrySet()) {
total += e.getValue();
System.out.printf(" %-38s : %5d MB%n", e.getKey(), e.getValue());
}
System.out.printf(" %-38s : %5d MB%n", "TOTAL", total);
String verdict = (total > limitMb) ? "MELEBIHI batas -> risiko OOMKill" : "masih di dalam batas";
System.out.println(" batas container " + limitMb + " MB -> " + verdict);
System.out.println();
}
public static void main(String[] args) {
tabelPersentase();
anggaran("Anggaran 512 MB dengan heap 75%", 512, 75);
anggaran("Anggaran 512 MB dengan heap 60%", 512, 60);
}
}Output yang Dihasilkan:
== Heap maksimum dari MaxRAMPercentage ==
batas container 25% 50% 75%
256 MB 64MB 128MB 192MB
512 MB 128MB 256MB 384MB
1024 MB 256MB 512MB 768MB
== Anggaran 512 MB dengan heap 75% ==
heap (75%) : 384 MB
metaspace : 64 MB
code cache : 48 MB
thread stacks (30 x 1MB) : 30 MB
direct buffers : 32 MB
TOTAL : 558 MB
batas container 512 MB -> MELEBIHI batas -> risiko OOMKill
== Anggaran 512 MB dengan heap 60% ==
heap (60%) : 307 MB
metaspace : 64 MB
code cache : 48 MB
thread stacks (30 x 1MB) : 30 MB
direct buffers : 32 MB
TOTAL : 481 MB
batas container 512 MB -> masih di dalam batasMari kita bedah hitungannya:
heapMb(limitMb, persen): satu baris aritmetikalimit * persen / 100dalamintMB — angka bulat yang cukup untuk perencanaan kapasitas. Batas 512 MB dengan 75% menghasilkan heap 384 MB.- Konstanta non-heap: metaspace, code cache, stack per thread, dan direct buffer hidup di luar heap namun di dalam batas container. Nilainya di sini adalah model tetap; aplikasi nyata membacanya lewat
Native Memory Tracking(jcmd VM.native_memory). - Anggaran 75%: 384 MB heap + 174 MB non-heap = 558 MB — melebihi batas 512 MB. Kernel tidak peduli JVM masih punya ruang kosong di heap; cgroup limit terlanggar, proses dimatikan.
- Anggaran 60%: heap turun ke 307 MB, total 481 MB, dan masih menyisakan ruang 31 MB. Inilah alasan angka lazim di produksi berkisar 50-75% — bukan 100%.
- Tabel pertama: hubungan batas ↔ persentase ↔ heap untuk tiga ukuran container — pegangan cepat saat menetapkan request/limit di Kubernetes atau
docker run --memory.
Dockerfile JVM untuk Produksi #
Rangkuman praktik yang telah kita bahas dalam satu berkas:
FROM eclipse-temurin:25-jre
RUN groupadd -r kasir && useradd -r -g kasir kasir
COPY config/application.properties /app/config/application.properties
COPY target/toko-fat.jar /app/app.jar
USER kasir
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=60.0", "-jar", "/app/app.jar"]Bentuk exec form ENTRYPOINT (kurung siku) wajib: Java menjadi proses nomor satu di dalam container sehingga sinyal SIGTERM dari docker stop sampai langsung ke JVM — prasyarat graceful shutdown yang kita bahas di chapter tersendiri. Pengguna non-root membatasi dampak bila aplikasi berhasil dieksploitasi, dan konfigurasi tetap eksternal mengikuti chapter Externalized Configuration.
Image ini masih memuat satu ketidakefisienan: berkas yang dibutuhkan build ikut terbawa karena semuanya dilakukan dalam satu tahap. Chapter berikutnya memisahkan dapur dari ruang saji lewat multi-stage build.
Referensi #
- Docker: docker run — batas memori container
- Docker: Dockerfile Reference
- Oracle: Garbage First Garbage Collector Tuning — Heap Sizing
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HitungHeap.java
Unduh berkasimport java.util.LinkedHashMap;
import java.util.Map;
/** Menghitung heap dari batas memori container dengan MaxRAMPercentage. */
public class HitungHeap {
/** Komponen memori non-heap yang ikut memakan batas container (model tetap). */
static final int METASPACE_MB = 64;
static final int CODE_CACHE_MB = 48;
static final int STACK_PER_THREAD_MB = 1;
static final int DIRECT_BUFFER_MB = 32;
static final int JUMLAH_THREAD = 30;
static int heapMb(int limitMb, int persen) {
return limitMb * persen / 100;
}
static void tabelPersentase() {
System.out.println("== Heap maksimum dari MaxRAMPercentage ==");
System.out.printf("%-14s %8s %8s %8s%n", "batas container", "25%", "50%", "75%");
for (int limit : new int[]{256, 512, 1024}) {
System.out.printf("%-14s %7dMB %7dMB %7dMB%n", limit + " MB",
heapMb(limit, 25), heapMb(limit, 50), heapMb(limit, 75));
}
System.out.println();
}
static void anggaran(String judul, int limitMb, int persenHeap) {
Map<String, Integer> komponen = new LinkedHashMap<>();
komponen.put("heap (" + persenHeap + "%)", heapMb(limitMb, persenHeap));
komponen.put("metaspace", METASPACE_MB);
komponen.put("code cache", CODE_CACHE_MB);
komponen.put("thread stacks (" + JUMLAH_THREAD + " x " + STACK_PER_THREAD_MB + "MB)",
JUMLAH_THREAD * STACK_PER_THREAD_MB);
komponen.put("direct buffers", DIRECT_BUFFER_MB);
int total = 0;
System.out.println("== " + judul + " ==");
for (var e : komponen.entrySet()) {
total += e.getValue();
System.out.printf(" %-38s : %5d MB%n", e.getKey(), e.getValue());
}
System.out.printf(" %-38s : %5d MB%n", "TOTAL", total);
String verdict = (total > limitMb) ? "MELEBIHI batas -> risiko OOMKill" : "masih di dalam batas";
System.out.println(" batas container " + limitMb + " MB -> " + verdict);
System.out.println();
}
public static void main(String[] args) {
tabelPersentase();
anggaran("Anggaran 512 MB dengan heap 75%", 512, 75);
anggaran("Anggaran 512 MB dengan heap 60%", 512, 60);
}
}