Call
Backend Basics

Modern Java Explained: JVM, Records, Sealed Types, Virtual Threads, GC, and Production Basics

Learn modern Java from the JVM and bytecode model through records, sealed types, pattern matching, collections, GC, virtual threads, concurrency, build tooling, JFR diagnostics, and the current JDK 26 / JDK 25 LTS release model.

Published: February 24, 2025
Updated: September 12, 2026
13 min read
Modern Java Explained: JVM, Records, Sealed Types, Virtual Threads, GC, and Production Basics

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:

  1. source -> bytecode -> JVM execution
  2. JDK vs JVM vs Java SE
  3. reference semantics and mutability
  4. records for value/data carriers
  5. sealed types and pattern matching for constrained variants
  6. collections and immutable boundaries
  7. exception/error design
  8. garbage collection and process memory
  9. virtual threads for high-concurrency blocking I/O
  10. scarce-resource concurrency limits
  11. synchronization and shared-state safety
  12. build/dependency management
  13. JFR and runtime diagnostics
  14. 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

Tags:JavaJVMVirtual ThreadsBackend DevelopmentConcurrencyJDK
AI & AGENT ENGINEERING

BUILDING AN AI PRODUCT?

Talk to Lofingo about custom autonomous AI agents, enterprise workflow automation, and scalable software systems.

EXPLORE AI SERVICES →