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 imageUkuran khasIsiKapan dipakai
eclipse-temurin:25-jdkbesarJDK lengkapbuild/development
eclipse-temurin:25-jresedangJRE sajaaplikasi berbasis classpath
eclipse-temurin:25-jre-alpinekecilJRE di Alpine muslimage minimal
image hasil jlink + FROM scratchpaling kecilhanya modul terpakaikontrol 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:

bash
java -XX:+PrintFlagsFinal -version | rg 'MaxRAMPercentage|UseContainerSupport'

Output yang Dihasilkan:

text
   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}:

bash
java -XX:MaxRAMPercentage=75.0 -XX:+PrintFlagsFinal -version | rg 'MaxRAMPercentage'

Output yang Dihasilkan:

text
   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:

java HitungHeap.java
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:

text
== 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 batas

Mari kita bedah hitungannya:

  1. heapMb(limitMb, persen): satu baris aritmetika limit * persen / 100 dalam int MB — angka bulat yang cukup untuk perencanaan kapasitas. Batas 512 MB dengan 75% menghasilkan heap 384 MB.
  2. 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).
  3. 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.
  4. 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%.
  5. 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:

dockerfile Dockerfile aplikasi kasir siap produksi
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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

HitungHeap.java

Unduh berkas
java
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);
    }
}