Modern Java Explained: JVM, Records, Sealed Types, Virtual Threads, GC, and Production Basics
Java is a statically typed programming language and runtime platform built around a simple portability idea: compile source code to JVM bytecode, then run that bytecode on a compatible Java Virtual Machine.
Modern Java is very different from the language many developers remember from Java 8-era tutorials. The platform now includes records, sealed classes, pattern matching, virtual threads, improved garbage collectors, compact source-file options, stronger observability, and a six-month release cadence.
As of 13 September 2026, JDK 26 is the latest generally available Java SE release. JDK 25 is the current long-term-support release. Oracle's current download page notes that JDK 26 will be superseded by JDK 27 later in September 2026, so JDK 27 should not yet be described as the current GA release.
Java source, bytecode, and the JVM
A Java source file is compiled by javac:
javac Application.java
The compiler produces bytecode in a .class file.
The JVM then executes that bytecode:
java Application
This separation lets the same compiled program run across compatible operating systems and processor architectures as long as a suitable JVM exists.
The JVM is also responsible for major runtime services such as:
- class loading
- bytecode verification
- just-in-time compilation
- garbage collection
- thread scheduling integration
- runtime diagnostics
Java performance is therefore a combination of your code, libraries, JVM implementation, GC configuration, workload, and hardware.
JDK, JVM, and Java SE are different terms
These are often mixed together.
Java SE
The standard Java platform specification and APIs.
JVM
The runtime that executes Java bytecode.
JDK
The development kit containing tools such as:
java
javac
javadoc
jcmd
jstack
jmap
jfr
A production organization should know both the Java specification level and the exact JDK distribution/version it runs.
Start with simple typed code
A basic program:
public class Application {
public static void main(String[] args) {
int port = 8080;
String service = "orders";
System.out.println(service + " on " + port);
}
}
Java variables have known types, and the compiler rejects many incompatible operations before runtime.
Primitive types include:
byte short int long
float double
boolean char
Everything else is represented through reference types such as classes, interfaces, arrays, records, and enums.
Java objects have reference semantics
Consider:
var first = new ArrayList();
var second = first;
first and second refer to the same list object.
Assigning a reference does not clone the object automatically.
This matters when designing mutable domain objects and concurrency boundaries. Shared mutable objects can be modified through any reference that points to them.
Prefer immutable or narrowly scoped state when possible.
var is local type inference, not dynamic typing
Modern Java supports local variable type inference:
var name = "Asha";
var count = 10;
The compiler still infers a concrete static type.
This is not equivalent to JavaScript-style dynamic typing.
Use var where the type remains obvious from context. Avoid it when the inferred type makes code harder to understand.
Records are concise data carriers
Record classes became permanent in Java 16.
A record can represent a small immutable data carrier:
public record Product(String id, String name, long priceMinor) {}
The compiler provides the canonical constructor, accessors, equals, hashCode, and toString behavior based on the record components.
Records are useful for:
- API DTOs
- configuration values
- events
- query results
- immutable domain values
They are shallowly immutable: a record component that points to a mutable object can still refer to mutable state.
Use records where identity is the values
A Money(currency, amount) value is a natural record candidate.
A long-lived mutable aggregate such as an order whose state changes over time may still deserve an ordinary class with controlled behavior.
Do not replace every class with a record merely to reduce boilerplate.
The model should reflect domain semantics.
Sealed classes constrain inheritance
Sealed classes and interfaces became permanent in Java 17.
They let a type explicitly control which classes may implement/extend it.
Conceptually:
PaymentResult
Success
Declined
RetryableFailure
This is useful when the set of valid variants is known and should remain controlled.
Combined with pattern matching, sealed hierarchies can model state and result types much more clearly than string-based type fields.
Enums remain excellent for closed constant sets
For simple states:
public enum OrderStatus {
PENDING,
PAID,
CANCELLED
}
Enums give type safety and exhaustive-ish handling patterns compared with raw strings.
Do not store enum ordinals as durable public/database meaning. If enum ordering changes, ordinal-based storage can silently reinterpret old data.
Persist stable symbolic values or explicitly mapped database codes.
Pattern matching has made Java control flow cleaner
Modern Java includes pattern matching for instanceof and switch.
Instead of manual cast boilerplate, code can reason about a value and bind it in one step.
Pattern matching becomes especially useful with sealed hierarchies and records.
Use it to make variant-handling clearer—not to build giant switch statements that indicate missing object/domain boundaries.
Exceptions are for exceptional/control failure paths, not ordinary validation everywhere
Java uses checked and unchecked exceptions.
Good application design distinguishes:
- validation/domain outcomes
- transient infrastructure failures
- programmer bugs/invariant violations
For example, “customer entered an invalid coupon” may be better represented as a normal domain result than a stack trace-heavy exceptional failure.
At service boundaries, convert internal exceptions into stable API error contracts rather than leaking Java stack traces to clients.
Prefer try-with-resources for closeable resources
Resources such as streams, files, JDBC statements, and result sets need reliable cleanup.
Use try-with-resources:
try (var input = Files.newInputStream(path)) {
// consume stream
}
The runtime closes the resource automatically at the end of the block.
This is safer than relying on every return/exception path to call close() manually.
Collections are central to Java application code
Common collection interfaces include:
List
Set
Map
Queue
Deque
Program to useful interfaces rather than concrete implementations when the caller does not depend on implementation details.
For example:
List names = new ArrayList<>();
Understand the complexity/behavior of the implementation you choose. A HashMap, ConcurrentHashMap, ordered map, and tree-based map solve different problems.
Immutable collections make boundaries safer
Modern Java provides factory methods such as:
List.of(...)
Set.of(...)
Map.of(...)
Returning an immutable collection from a service can prevent callers from accidentally mutating internal state.
But immutability is still shallow if the elements themselves are mutable.
Choose ownership semantics explicitly at API boundaries.
Streams are an API for data processing, not a replacement for every loop
Java Streams support operations such as:
map
filter
flatMap
sorted
reduce
collect
They can make transformation pipelines expressive.
But a clear for loop is often better when:
- control flow is complex
- side effects dominate
- debugging needs stepwise state
- the stream pipeline becomes difficult to read
Do not optimize for the shortest code.
The JVM uses garbage collection
Java applications normally allocate objects on a managed heap.
The garbage collector reclaims objects that are no longer reachable.
This eliminates manual free/delete-style memory management for ordinary Java objects and prevents major classes of memory-corruption bugs.
GC does not prevent:
- logical memory leaks from retained references
- unbounded caches
- oversized buffers
- native memory leaks
- excessive allocation rate
- OOM from too much live data
A Java process can be memory-safe and still run out of memory.
Choose GC from workload, not folklore
Modern OpenJDK distributions provide multiple garbage collectors with different latency/throughput goals.
The default collector is a good starting point for many applications.
Tune or switch collectors only after measuring:
- heap occupancy
- allocation rate
- pause behavior
- CPU overhead
- tail latency
- object lifetime profile
Do not copy random JVM flags from an old blog. Defaults change across JDK generations.
Heap size must leave process/container headroom
A Java process consumes more than the configured heap.
Memory also includes:
- metaspace
- thread stacks
- code cache/JIT data
- direct buffers
- native libraries
- JVM runtime structures
If a container has a 2 GiB limit, setting a 2 GiB maximum Java heap leaves no room for those components.
Monitor:
Java heap
process RSS
container/cgroup memory
native memory where relevant
Capacity-test with realistic concurrency.
Virtual threads changed the Java server concurrency story
Virtual threads became permanent in Java 21 and remain part of current Java.
A virtual thread is a Thread managed primarily by the Java runtime rather than being permanently tied to one operating-system thread.
They are designed for workloads where many concurrent tasks spend much of their time blocked on I/O.
Oracle's current documentation emphasizes an important point:
virtual threads increase throughput/concurrency; they are not “faster threads” that make CPU work execute faster.
Virtual threads fit blocking I/O well
A server flow such as:
receive request
query database
call remote service
wait
return response
can use a straightforward thread-per-task style while supporting much higher numbers of waiting tasks than platform-thread pools traditionally allowed.
That can reduce the need for complex callback/reactive code in selected applications.
But downstream resources are still finite.
Virtual threads do not make the database infinite
If you allow 50,000 virtual threads to all execute JDBC queries simultaneously against a database with 100 useful concurrent connections, the database becomes the bottleneck.
Use concurrency limits around scarce resources:
- database connections
- payment providers
- rate-limited APIs
- CPU-heavy processing
Virtual threads make thread creation cheap. They do not make dependencies cheap.
CPU-heavy work still needs controlled parallelism
Virtual threads are not intended for long-running CPU-intensive work.
For CPU-bound processing, useful parallelism is still constrained roughly by available processor capacity.
An application that creates one million CPU-burning tasks only creates a scheduling problem.
Separate I/O concurrency from CPU parallelism.
Thread locals need more thought with virtual threads
Virtual threads can exist in very large numbers.
A heavy thread-local object copied or allocated per virtual thread can create large memory consumption.
Use thread-local state carefully, especially for expensive caches or large context objects.
Prefer explicit request/context passing when it produces clearer ownership.
Synchronization is still necessary
Java provides synchronization tools such as:
synchronized- locks
- atomics
- concurrent collections
- semaphores
- latches
Virtual threads do not remove shared-state races.
Whenever possible, reduce mutable shared state instead of attempting to protect one giant shared object with increasingly complicated locks.
The Java Memory Model defines visibility and ordering
Concurrency correctness is not only “two threads did not crash.”
The Java Memory Model defines when writes from one thread become visible to another and how synchronization establishes happens-before relationships.
Use standard concurrency primitives rather than inventing flag protocols around unsynchronized fields.
For advanced lock-free code, understand volatile, atomics, and memory-order semantics deeply before depending on them.
CompletableFuture is useful but should not create invisible thread pools
CompletableFuture can model asynchronous composition.
Be explicit about which executor runs expensive stages.
Default/common pools can become hidden shared bottlenecks when unrelated application features all submit work to them.
Virtual threads may simplify many blocking workflows, but asynchronous composition remains useful for parallel independent operations and APIs that naturally return futures.
Modules exist, but most backend projects should adopt them deliberately
The Java Platform Module System arrived in Java 9.
Modules can provide stronger package encapsulation and explicit dependencies.
They are valuable for some large platforms and libraries.
Many Spring/backend applications continue to use ordinary JAR/package boundaries successfully.
Do not add module complexity unless the stronger boundary solves a real packaging/encapsulation problem for your application.
Maven and Gradle are the dominant build tools
A Java build normally manages:
- dependencies
- compilation
- tests
- packaging
- plugins
- code generation
Keep dependency versions controlled through one clear model.
Avoid dynamic/unbounded version ranges in production builds.
Use lockfiles/dependency-management/BOM patterns where your build ecosystem supports them so CI and developer machines resolve predictable artifacts.
Dependency supply-chain security matters
A Java application can have hundreds of transitive dependencies.
Maintain:
- vulnerability scanning
- SBOMs where required
- dependency update policy
- license review
- repository/proxy controls
- reproducible build practices
Do not assume a dependency is safe because it is popular or downloaded from Maven Central.
Minimize unnecessary libraries in security-sensitive services.
JFR is one of Java's strongest production diagnostics tools
Java Flight Recorder can capture low-overhead runtime events for diagnosing issues such as:
- CPU hotspots
- allocations
- garbage collection
- lock contention
- thread behavior
- I/O/runtime events
It is especially useful for problems that are difficult to reproduce locally.
Use it with operational controls appropriate to your environment and avoid capturing sensitive application payloads unnecessarily.
jcmd, thread dumps, and heap analysis remain essential
Useful JDK diagnostics include:
jcmd- thread dumps
- heap dumps
- native memory tracking depending on runtime/configuration
- GC logs
Operational runbooks should explain when and how these tools are used safely.
A heap dump can contain customer data and credentials stored in memory, so treat diagnostic artifacts as sensitive data.
Observe the application, not only the JVM
JVM metrics are useful:
heap
GC
threads
CPU
class loading
but a backend also needs product/application signals:
request rate
error rate
latency
DB pool saturation
queue depth
business operation success
A healthy heap does not mean checkout works.
Tie JVM telemetry to application SLOs and traces.
LTS vs feature releases
Java uses a six-month release cadence.
As of September 2026:
- JDK 26 is the current GA feature release
- JDK 25 is the current LTS release
- JDK 21 is the previous LTS release
An organization does not need to run every six-month feature release in production.
Many teams standardize on LTS releases and move between them deliberately.
Other teams track feature releases to access improvements sooner.
The right strategy depends on vendor support, upgrade automation, dependencies, and operational maturity.
Keep patch/security releases current
Running an LTS line does not mean staying on its original .0 build forever.
Apply current supported patch/security releases according to your JDK vendor's policy.
Oracle's August 2026 release notes, for example, list current security baselines across supported Java families and recommend keeping the JDK updated with Critical Patch Updates.
Test runtime upgrades in staging/canary environments and keep rollback procedures clear.
Java is a platform, not just one framework
Java can power:
- Spring Boot services
- Quarkus/Micronaut applications
- Android tooling/components
- Kafka ecosystem software
- data platforms
- desktop software
- CLI tools
- large enterprise systems
Do not equate “Java backend” with one framework or one architecture style.
The JVM ecosystem is valuable partly because teams can choose different libraries while retaining one mature runtime/tooling platform.
Modern Java checklist
If you are learning or modernizing Java, understand:
- source -> bytecode -> JVM execution
- JDK vs JVM vs Java SE
- reference semantics and mutability
- records for value/data carriers
- sealed types and pattern matching for constrained variants
- collections and immutable boundaries
- exception/error design
- garbage collection and process memory
- virtual threads for high-concurrency blocking I/O
- scarce-resource concurrency limits
- synchronization and shared-state safety
- build/dependency management
- JFR and runtime diagnostics
- LTS vs feature-release strategy
Modern Java's strength is not one syntax feature. It is the combination of a mature JVM, strong tooling, backwards-compatible platform evolution, high-quality concurrency/runtime features, and a huge production ecosystem. Use those strengths while still designing clean application boundaries, bounded resource use, and measurable operational behavior.
Official references
BUILDING AN AI PRODUCT?
Talk to Lofingo about custom autonomous AI agents, enterprise workflow automation, and scalable software systems.
EXPLORE AI SERVICES →