When a Java application runs in production environment, certain JVM arguments were added explicitly to ensure predictable and reliable behavior and sometimes for diagnostic purposes. JVM configuration can influence runtime behavior just as much as the code itself. Memory allocation, garbage collection, and observability are all determined at startup, making JVM arguments a crucial part of application execution.

While the JVM provides sensible defaults, in production, especially under high load, low-latency requirements, or in containerized environments, explicit JVM arguments help maintain stability, performance, and observability.

Java command structure:

java [JVM options] -cp [classpath] <main class> [application arguments]

Memory Configuration

Heap sizing is still the most critical JVM decision. Java is container-aware, but explicit settings prevent unnecessary resizing and GC pauses. Common memory arguments include heap size, thread stack, and metaspace.

Heap Size: -Xms512m -Xmx2g

(Default: JVM allocates approximately 25% of system memory)

-Xms512m -Xmx2g are JVM arguments that control heap memory, which is where the JVM stores objects created by your program.

-Xms512m Initial Heap Size

  • Sets the starting heap size when the JVM launches
  • 512m means 512 megabytes
  • JVM allocates this memory immediately at startup

-Xmx2g Maximum Heap Size

  • Sets the maximum heap memory the JVM can use
  • g means gigabytes, m mean megabytes
  • The JVM will never grow the heap beyond this limit

Predictable heap sizing reduces GC overhead and prevents surprises in production.


Thread Stack: -Xss1m

(Default: 1 MB per thread)

Each Java thread gets its own stack memory for storing Local variables, Method call frames and Return addresses

  • Each thread consumes memory equal to its stack size
  • Too small may cause StackOverflowError for deep recursion or large method frames
  • Too large may limit the total number of threads because total memory is finite

Thread memory is separate from heap memory. Thread count depends on total system RAM, not heap size. You can create more threads than heap allows if the system has enough memory for their stacks. Be careful because too many threads can exhaust system memory or reduce performance due to CPU context switching.

Metaspace: -XX:MaxMetaspaceSize=256m

(Default: unlimited)

Metaspace stores class metadata, which includes information about loaded classes, methods, and bytecode.

  • Caps the maximum metaspace memory to 256 MB
  • Default is unlimited, so it can grow until the operating system runs out of memory
  • Unchecked growth can cause OutOfMemoryError: Metaspace in applications that dynamically load many classes, such as frameworks, plugins, or hot-reloading applications
  • Limiting metaspace ensures predictable memory usage and prevents JVM crashes

Predictable memory usage across heap, stack, and metaspace reduces GC overhead and ensures stability in production.

Garbage Collection

Garbage collection (GC) manages memory in the JVM by automatically reclaiming unused objects. Choosing the right GC strategy affects latency, throughput, and application stability. Modern JVMs come with reasonable defaults, but understanding and monitoring GC behavior is essential for production systems.

Common GC Options

G1 Garbage Collector

  • -XX:+UseG1GC is the default in modern JVMs
  • Designed for balanced throughput and low pause times
  • Suitable for most applications without additional tuning

Z Garbage Collector

  • -XX:+UseZGC is optional and ideal for applications requiring sub-10 millisecond pause times
  • Focuses on very low latency for high-performance, long-running services

GC Logging

  • -Xlog:gc* enables unified GC logging
  • Provides insight into memory allocation, GC pauses, and heap usage
  • Helps identify performance bottlenecks and tune GC settings if needed

Even with modern defaults, it is important to observe GC behavior under real workloads. Monitoring GC logs and metrics allows you to understand pause times, memory utilization, and overall application performance before attempting any tuning.

[ Pause time is the period during which the JVM pauses application threads to perform garbage collection.]

Diagnostics and Observability

In production, failures should leave evidence that helps developers quickly identify and resolve issues. The JVM provides several options to make diagnostics and monitoring easier.

Common Diagnostic Options

  • -XX:+HeapDumpOnOutOfMemoryError Generates a heap dump when the JVM encounters an OutOfMemoryError. This snapshot shows all objects in memory at the time of failure, which is crucial for troubleshooting memory leaks.
  • -XX:HeapDumpPath=/tmp/heapdump.hprof Specifies the location where heap dumps are saved. You can change this to a directory with enough space for large dumps.
  • -XX:ErrorFile=/tmp/hs_err_pid%p.log Creates a log file containing detailed information if the JVM crashes unexpectedly. This includes native thread state, stack traces, and system details.
  • -XX:+FlightRecorder Enables low-overhead profiling to capture JVM performance and behavior over time. Flight Recorder data is useful for post-mortem analysis and performance tuning.

Without these options, debugging production incidents can be slow and error-prone, and root causes may remain unclear.

Runtime Configuration

System properties, passed with the -D option, allow you to configure runtime behavior without changing JVM internals. This separation keeps application configuration clean and flexible.

Examples of Common System Properties

  • -Dspring.profiles.active=prod Sets the active profile for Spring applications, allowing different configuration settings for production, staging, or development environments.
  • -Dfile.encoding=UTF-8 Sets the default file encoding for the application, ensuring consistent handling of character data across environments.

Using system properties allows you to manage environment-specific settings, frameworks, and libraries separately from core JVM behavior. This improves maintainability and reduces the risk of misconfiguration in production.

What Happens If You Don’t Pass JVM Arguments?

Java will run without any JVM arguments, using defaults. This can work for small apps or local testing, but unchecked defaults in production can crash your program.

Scenarios where this happens:

  • High-load microservices: many threads cause OutOfMemoryError: unable to create new native thread due to default stack size.
  • Large datasets: heap auto-sizing may trigger frequent GCs, causing latency spikes or OOM errors.
  • Containerized environments: JVM may overcommit memory relative to container limits, leading to crashes.
  • Long-running applications: metaspace grows indefinitely with dynamic class loading, eventually hitting OutOfMemoryError.

In summary defaults are safe for development but unpredictable in production.

Conclusion:

In Java, JVM arguments aren’t about squeezing performance, they are about stability, predictability, and observability. Small, intentional choices in memory, GC, and diagnostics lead to reliable, production-ready systems.

Developers can see all available flags and their current values using following command:

java -XX:+PrintFlagsFinal -version

This gives a complete view of heap, thread stack, metaspace, GC, and other runtime settings, making tuning and debugging much easier.

References:

https://docs.oracle.com/en/java/javase/25/docs/specs/man/java.html https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html https://docs.oracle.com/en/java/javase/25/vm/java-virtual-machine-guide.pdf