Introduction
Writing Java code is only half the story.
The real challenge begins after deployment.
Modern enterprise applications run:
- Inside Docker containers
- On Kubernetes clusters
- Behind API Gateways
- Across multiple cloud regions
- Under constant production load
A typical Spring Boot microservice may:
- Process millions of REST requests daily
- Consume Kafka messages
- Publish events to messaging systems
- Execute scheduled jobs
- Connect to databases
- Cache data
- Handle thousands of concurrent users
In these environments, application performance depends on much more than efficient code.
The Java Virtual Machine (JVM) plays a central role in:
- Memory management
- Garbage collection
- Thread scheduling
- Class loading
- JIT compilation
- CPU utilization
- Container resource awareness
Java 11 introduced significant improvements in these areas, making it one of the best Java releases for running cloud-native microservices.
In this article, we’ll explore the production features every enterprise Java developer should understand.
Learning Objectives
By the end of this article, you will be able to:
- Understand how the JVM manages memory.
- Learn the default garbage collector in Java 11.
- Configure JVM memory correctly.
- Understand container awareness.
- Use Java Flight Recorder.
- Analyze memory usage.
- Tune applications running in Docker and Kubernetes.
- Apply production best practices.
JVM Memory Overview
Every Java application runs inside the JVM.
The JVM manages several memory areas.
JVM
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Heap Metaspace Thread Stack
│
▼
Young Generation
│
▼
Old Generation
Each area has a different responsibility.
Heap Memory
The Heap stores Java objects.
Example:
Customer customer = new Customer();
Order order = new Order();
Both objects are allocated in the Heap.
The Heap is managed automatically by the Garbage Collector.
Young Generation
Most newly created objects are allocated here.
Example:
for(int i=0;i<100000;i++){
new Customer();
}
Many of these objects become unreachable quickly.
The Young Generation is optimized for short-lived objects.
Old Generation
Objects that survive multiple garbage collection cycles are promoted here.
Examples include:
- Cached objects
- Application configuration
- Singleton beans
- Long-lived domain objects
Garbage collection in the Old Generation is generally more expensive.
Metaspace
Before Java 8, class metadata was stored in PermGen.
Java 8 replaced PermGen with Metaspace.
Metaspace stores:
- Class definitions
- Method metadata
- Reflection information
- Annotations
Unlike PermGen, Metaspace grows dynamically as needed (subject to configured limits).
Thread Stack
Every thread has its own stack.
The stack stores:
- Local variables
- Method parameters
- Return addresses
- Method call frames
Example:
public void calculate(){
int total = 100;
}
total exists on the thread’s stack, not in the heap.
Garbage Collection
One of Java’s greatest strengths is automatic memory management.
Instead of manually freeing memory, developers simply stop referencing objects.
customer = null;
The object becomes eligible for garbage collection once no live references remain.
The exact time of reclamation is determined by the JVM.
Default Garbage Collector in Java 11
Java 11 uses G1 (Garbage-First Garbage Collector) as the default collector.
G1 was designed for:
- Large heaps
- Predictable pause times
- High-throughput applications
- Multi-core systems
It divides the heap into regions rather than using large contiguous memory areas.
G1 Regions
□□□□□□□□□□□□□□□□□□□□□□□□
Heap
↓
Small Independent Regions
The collector prioritizes regions containing the most reclaimable memory, helping reduce pause times.
Why G1?
Compared to older collectors, G1 generally provides:
- More predictable pause behavior.
- Better performance on large heaps.
- Improved scalability.
- Better suitability for server applications.
Container Awareness
One of the biggest Java 11 improvements for cloud deployments was enhanced container awareness.
Before Java 10, the JVM often detected the host machine’s resources instead of the container’s limits.
Example:
Docker Memory
↓
2 GB
↓
JVM Thinks
↓
32 GB
This frequently resulted in oversized heaps and OutOfMemoryError conditions.
Java 11 recognizes container resource limits more accurately.
Kubernetes Example
Pod Memory
↓
1024 MB
↓
JVM Uses
↓
Container Limit
This makes Java applications far more predictable in containerized environments.
Heap Configuration
Common JVM options:
-Xms
Initial Heap
-Xmx
Maximum Heap
Example:
-Xms512m
-Xmx2g
Choose values appropriate for the application’s workload and the container’s memory limit.
Flight Recorder (JFR)
Java Flight Recorder became available without commercial licensing restrictions starting with Java 11.
It provides low-overhead production diagnostics.
Capture information such as:
- CPU usage
- Memory allocation
- Garbage collection
- Thread activity
- Lock contention
- Method profiling
Unlike many profilers, JFR is designed for use in production environments.
Java Mission Control (JMC)
JFR recordings can be analyzed using Java Mission Control.
Typical analysis includes:
Application
↓
Flight Recording
↓
Java Mission Control
↓
Performance Analysis
This helps identify hotspots without significantly affecting application performance.
Monitoring Garbage Collection
Useful JVM logging options include:
-Xlog:gc*
Logs reveal:
- GC frequency
- Pause durations
- Heap occupancy
- Memory reclamation
Regularly reviewing GC logs helps detect memory pressure before it becomes a production issue.
Docker Best Practices
When running Spring Boot in Docker:
- Set realistic memory limits.
- Avoid relying solely on JVM defaults.
- Monitor heap usage.
- Leave room for native memory and thread stacks.
- Validate startup parameters in production-like environments.
Kubernetes Best Practices
For Kubernetes deployments:
- Configure resource requests and limits.
- Align JVM heap settings with container memory.
- Monitor pod restarts and OOMKilled events.
- Track GC metrics alongside CPU and memory.
Container orchestration and JVM tuning should be considered together.
Spring Boot Considerations
A typical Spring Boot application includes:
- Embedded web server
- Bean container
- HTTP clients
- Thread pools
- Database connection pools
- Caches
All of these consume memory outside the application’s domain objects.
Heap sizing should account for the complete runtime, not just business data.
Thread Pools
Avoid creating unbounded thread pools.
Example:
Executors.newFixedThreadPool(20);
The optimal size depends on whether tasks are CPU-bound or I/O-bound.
Profile the workload rather than relying on arbitrary numbers.
Common Performance Problems
Memory Leak
Objects remain reachable unexpectedly.
Example:
Static Collection
↓
Never Cleared
↓
Heap Growth
Large Object Allocation
Creating very large temporary objects repeatedly increases GC pressure.
Stream data where practical instead of loading everything into memory.
Excessive Object Creation
Example:
for(...) {
new Customer();
}
Object creation is inexpensive in Java, but unnecessary allocation in tight loops can still increase GC activity.
Production Monitoring
Track metrics such as:
- Heap usage
- GC pause time
- CPU utilization
- Thread count
- Allocation rate
- Response time
- Error rate
Use observability platforms to correlate JVM behavior with application performance.
Enterprise Deployment Checklist
✔ Configure heap limits.
✔ Enable GC logging.
✔ Monitor memory usage.
✔ Use Java Flight Recorder for production diagnostics.
✔ Validate container memory settings.
✔ Load test before production.
✔ Observe GC pause times.
✔ Review thread pool configuration.
✔ Monitor JVM metrics continuously.
Common Mistakes
Setting -Xmx Equal to Container Memory
The JVM also requires native memory, thread stacks, direct buffers, and metadata.
Leave sufficient headroom to avoid container termination.
Ignoring GC Logs
Garbage collection behavior provides valuable insight into application health.
Review logs regularly during performance testing.
Assuming More Heap Is Always Better
Larger heaps reduce GC frequency but can increase pause times and memory footprint.
Choose heap sizes based on workload and measurements.
Running Production Without Monitoring
Performance problems are much easier to prevent than diagnose after failures occur.
Instrumentation should be part of every production deployment.
Best Practices
✔ Use G1 GC unless measurements indicate another collector is more appropriate.
✔ Monitor heap and GC continuously.
✔ Take advantage of Java Flight Recorder.
✔ Configure containers with realistic resource limits.
✔ Tune the JVM using production metrics rather than assumptions.
✔ Profile before optimizing.
Interview Questions
Which Garbage Collector is the default in Java 11?
G1 (Garbage-First Garbage Collector).
What replaced PermGen?
Metaspace.
Why is container awareness important?
It enables the JVM to make memory and CPU decisions based on container limits rather than the host machine, improving behavior in Docker and Kubernetes environments.
What is Java Flight Recorder?
A low-overhead profiling and diagnostics tool designed for production environments.
Should JVM tuning be performed before profiling?
No.
Measure application behavior first, then tune based on evidence.
Summary
Java 11 strengthened the JVM for modern cloud-native deployments. Improved container awareness, the G1 Garbage Collector, Java Flight Recorder, and enhanced monitoring capabilities make it well suited for running enterprise microservices in Docker and Kubernetes.
Effective performance tuning is not about memorizing JVM flags. It is about understanding how the JVM manages memory, observing application behavior under realistic workloads, and making informed decisions based on metrics rather than assumptions.
Coming Up Next
Part 31 – Java 12: Switch Expressions – Writing Cleaner, Safer and More Expressive Decision Logic
We’ll begin exploring Java 12 by covering one of its most influential language improvements:
- Why traditional
switchstatements had limitations - Switch expressions
- Arrow (
->) syntax yield- Returning values directly
- Exhaustiveness
- Pattern for modern business rules
- Migration from legacy
switch - Enterprise use cases in validation, routing, pricing, and workflow engines