Virtual Memory & Address Space
IntermediateVirtual memory gives each process the illusion of a large, private address space by mapping virtual pages to physical frames, allowing processes larger than physical RAM to run.
Overview
Every process runs in its own virtual address space — on a 64-bit system this is theoretically 128 TB of addressable memory regardless of how much RAM the machine has. The OS and MMU cooperate to map virtual pages to physical frames on demand. The address space layout is fixed by convention: text segment (executable code) at low addresses, then initialised data, BSS (zero-initialised globals), heap growing upward, a large gap, stack growing downward from high addresses, and kernel space at the very top. Java exposes this through -Xmx (max heap) and -Xms (initial heap) flags, which control only the heap portion; the JVM itself, code cache, metaspace, and thread stacks occupy additional virtual address space.
Address Space Layout
The virtual address space is partitioned into segments with distinct permissions. Text is read+execute only, preventing code modification. Data and heap are read+write. The kernel space mapped at the top is accessible only in kernel mode — any user-space access triggers a segfault. On Linux you can inspect a process's address space layout via /proc/<pid>/maps.
// Java: controlling heap portion of virtual address space
// java -Xms512m -Xmx2g -Xss256k -XX:MaxMetaspaceSize=256m MyApp
//
// Virtual address space breakdown for a typical JVM process:
// ┌─────────────────────────┐ high address
// │ Kernel space (1TB+) │ — inaccessible from user code
// ├─────────────────────────┤
// │ Stack(s) — one per │ — grows down; -Xss controls each thread stack
// │ thread (~256KB–2MB) │
// ├─────────────────────────┤
// │ Memory-mapped files, │
// │ JVM code cache (JIT) │
// ├─────────────────────────┤
// │ Heap (Java objects) │ — -Xms to -Xmx; GC operates here
// ├─────────────────────────┤
// │ Metaspace (class meta) │ — -XX:MaxMetaspaceSize
// ├─────────────────────────┤
// │ JVM + libc text/data │ — read+exec
// └─────────────────────────┘ low address (0x0 — unmapped, catches null deref)
// Checking runtime memory from Java
Runtime rt = Runtime.getRuntime();
long heapUsed = rt.totalMemory() - rt.freeMemory();
long heapMax = rt.maxMemory();
System.out.printf("Heap: %d MB used / %d MB max%n",
heapUsed / 1_048_576, heapMax / 1_048_576);
// OutOfMemoryError = heap portion exhausted (not all virtual memory)
// StackOverflowError = thread stack portion exhausted32-bit vs 64-bit Address Space and CompressedOops
A 32-bit address space is 4 GB — a hard ceiling per process. 64-bit gives 128 TB of virtual space (48-bit addressing on x86-64). The JVM uses CompressedOops (-XX:+UseCompressedOops, enabled by default below 32 GB heap) to encode 64-bit heap pointers in 32 bits by assuming 8-byte object alignment, saving ~30–40% heap memory for pointer-heavy object graphs.
// Checking JVM pointer compression
// Run: java -XX:+PrintFlagsFinal -version | grep UseCompressedOops
//
// CompressedOops: encode heap address / 8 into 32 bits
// Works when heap < 32GB (addresses fit in 35 bits → shift right 3 bits → 32 bits)
//
// Impact: object reference in heap = 4 bytes (compressed) vs 8 bytes (uncompressed)
// A HashMap<K,V> with 1M entries: ~32MB compressed vs ~64MB uncompressed
// Detecting compressed oops at runtime
boolean compressedOops = ManagementFactory.getPlatformMXBeans(HotSpotDiagnosticMXBean.class)
.stream()
.map(b -> b.getVMOption("UseCompressedOops"))
.map(VMOption::getValue)
.findFirst()
.map(Boolean::valueOf)
.orElse(false);
System.out.println("CompressedOops: " + compressedOops);
// Practical: keep heap < 32GB to benefit from compressed oops
// Going from 31GB to 33GB heap can INCREASE memory usage due to pointer expansionVirtual Memory and Memory-Mapped Files
Memory-mapped files use the virtual memory system to map file contents directly into the process address space. Reading a mapped region triggers a page fault the first time, loading the file page from disk via demand paging. Subsequent reads are served from RAM. This avoids explicit read() syscalls and enables efficient IPC via a shared mapping.
// Memory-mapped file — demand paging loads pages from disk on access
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "rw");
FileChannel channel = raf.getChannel()) {
// Map entire file into virtual address space
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size());
// First access triggers page fault → OS loads page from disk
buffer.putInt(0, 42); // write at offset 0
int value = buffer.getInt(0); // read back — now in RAM, fast
System.out.println("Value: " + value);
// Force dirty pages to disk
buffer.force();
}
// Analogy: the file is "lazily loaded" — only accessed pages consume RAM
// A 10GB file mapped but only 1MB read = only 1MB of physical RAM usedKey Points to Remember
- 1Each process has its own virtual address space — isolation prevents one process from corrupting another.
- 2Address space layout: text → data → BSS → heap (grows up) → gap → stack (grows down) → kernel.
- 332-bit processes are limited to 4 GB address space; 64-bit processes have 128 TB virtual space.
- 4Java -Xmx controls max heap size — only the heap portion of the virtual address space.
- 5CompressedOops encodes 64-bit heap pointers in 32 bits; disabled automatically above 32 GB heap.
- 6Memory-mapped files use demand paging to load file contents — only accessed pages consume RAM.
Interview Questions
Sign in to ask AriaWhat is virtual memory and why does each process need its own address space?
What is the difference between -Xmx and the total virtual memory used by a JVM process?
Why does increasing heap from 31 GB to 33 GB sometimes increase total memory usage in Java?
How does memory-mapped I/O leverage the virtual memory system?
Ask Aria about Virtual Memory & Address Space
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.