DevelopmentAll levels

Java

Java is a statically typed language compiled to bytecode for the JVM, a runtime that compiles hot code as it runs and frees memory with a garbage collector. For thirty years it has been the default for large back-end systems — banks, airlines, telecoms — and what changed most in the last five is how it is written and how it waits: records, sealed types and pattern matching in the language, virtual threads in the runtime. The page opens with every building block on one screen, the current way beside the one it replaced, then covers releases and licences, the JVM, memory in a container, Spring and its proxies, threads and virtual threads, and five documented cases.

Current LTS
JDK 25 · September 2025
Latest
JDK 27 · next LTS 29 in 2027
Releases
Every 6 months, LTS every 2 years

Java on one screen: every building block, then and now

Java keeps old code running. Most classes written for Java 8 in 2014 still compile and run on JDK 27, so most code bases mix three or four eras, and most material online teaches the oldest one. Since a new release arrives every six months, a lot has been replaced. Here is all of it first. The table lists each job, the way current Java does it, the way it replaced, and the JDK release in which the current way became final — not preview, not incubator. It is also the fastest way to date a code base, a tutorial or a job candidate’s answer.

JobThe current wayWhat it replacedJDK
A class that only carries datarecordFields, a constructor, getters, equals and hashCode by hand — or Lombok16
A closed set of subtypessealed with permitsAn open hierarchy, or a type field and a comment17
Branching on typeswitch with type patterns, checked for completenessAn if chain of instanceof, each followed by a cast21
Taking a record apartRecord patterns: case Point(var x, var y)Calling the accessors one by one21
A variable you must declare but never use_A name like ignored22
Multi-line text — SQL, JSONText blocks, """Concatenation with \n15
Types of local variablesvarThe type written on both sides10
Dates and timesjava.time: Instant, LocalDate, ZonedDateTimeDate and Calendar8
Calling an HTTP APIjava.net.http.HttpClient — HTTP/3 since 26HttpURLConnection, or a third-party client11
The first and last elementgetFirst(), getLast(), reversed()list.get(list.size() - 1)21
A custom step in a streamStream gatherersCollecting to a list and looping over it24
Waiting on I/O at scaleVirtual threadsA bounded thread pool, or reactive code21
synchronized around a blocking callSafe on virtual threads — no longer pins the carrierRewriting it with ReentrantLock24
Per-request contextScopedValueThreadLocal25
Several subtasks as one unitStructured concurrency — still a previewAn ExecutorService and a list of futurespreview
Calling native codeForeign Function & Memory APIJNI22
A small program’s entry pointvoid main() in a compact source filepublic static void main(String[] args) inside a class25
Faster start-upAn AOT cache recorded in a training runLoading, linking and profiling from zero on every start24 · 25
The default garbage collectorG1, in every environmentSerial, chosen silently on one CPU or under 1792 MB27
The header on every object8 bytes — compact headers, by default12 bytes25 · default 27

Releases, support, and which JDK you actually run

Since Java 10, in March 2018, a new JDK has shipped every March and September. Most releases are supported only until the next one, six months later. Every two years one release is designated — long-term support — and receives security fixes for years; since 17 the gap between LTS releases has been two years rather than three. The current LTS is 25, from September 2025; the latest release is 27, from September 2026; the next LTS will be 29, in September 2027. Production systems almost always run an LTS, and the features in the table reach most teams in LTS-sized jumps: 11, 17, 21, 25.

JDKReleasedOracle Premier SupportOracle Extended Support
8 LTSMar 2014Mar 2022Dec 2030
11 LTSSep 2018Sep 2023Jan 2032
17 LTSSep 2021Sep 2026Sep 2029
21 LTSSep 2023Sep 2028Sep 2031
25 LTSSep 2025Sep 2030Sep 2033
27Sep 2026Mar 2027—
29 LTSSep 2027, plannedSep 2032Sep 2035

Those are Oracle’s dates for paying customers, and they are not the whole story. The JDK is one open-source code base, OpenJDK, and several vendors build, test and patch it: Eclipse Adoptium’s Temurin, Amazon Corretto, Azul Zulu, Microsoft and Red Hat among them. Their builds are the same code, their updates for each LTS are free, and each publishes its own end dates, usually several years past the release. Oracle JDK is one build among them, with a licence that has changed repeatedly since 2019: in April 2019 updates of Oracle JDK 8 stopped being free for production use; in 2021 Oracle JDK 17 came under a no-fee licence that ends a year after the next LTS ships; in January 2023 the paid subscription moved to a price per employee — every employee and contractor of the company, not per server or per developer; and from the October 2026 update, Oracle JDK 21 moves back under the licence that requires a subscription in production.

The machine underneath: bytecode, the JIT and warm-up

Java does not compile to machine code. The javac compiler turns source into bytecode — instructions for an abstract machine — and the JVM for each platform turns bytecode into something the processor runs. That indirection is why one JAR file runs unchanged on a developer’s Mac and on a Linux server, and it is where Java’s performance profile comes from.

Java compiles to bytecode rather than to machine code, so the artefact is identical everywhere and the JVM is the part that differs per platform. Inside, the interpreter runs every method and the JIT recompiles whatever gets hot into native code — which is why a service is slow for its first few thousand requests and fast afterwards. Objects go on a heap the runtime manages: most die young and are collected cheaply, survivors are promoted to the old generation, and collecting that is where the pauses come from.

The JVM starts by interpreting bytecode, which is slow, while counting how often each method runs. A method that crosses a threshold is compiled quickly by the first-tier compiler, with counters left in to profile it. A method that stays hot is compiled again by the optimising compiler, which uses that profile: which branch is actually taken, which class really arrives at this call, which loop runs a million times. If an assumption later turns out wrong — a new subclass appears — the JVM throws the optimised code away and goes back a tier. This is why a Java service is noticeably slower for its first few thousand requests and then settles, and why its peak speed can beat code compiled ahead of time.

Start-up is the other cost: the JVM has to load, verify and link thousands of classes before it serves anything. That hardly matters for a service that runs for weeks and matters a great deal for a function that scales to zero. There are now four answers, each with a different price:

OptionWhat it doesWhat it costs
Plain JVMLoads and links on every start, then warms upSeconds to start; nothing to configure
AOT cache, JDK 24+A training run records loaded classes and, since 25, method profiles; later starts reuse themA training step in the build; the cache must match the JDK and the classpath
CRaCSaves a warmed-up process to disk and restores itLinux only; open files and connections must be closed and reopened around the checkpoint
GraalVM Native ImageCompiles the whole application into an executable ahead of timeStarts in milliseconds, but no runtime JIT, slow builds, and configuration for reflection

Memory: the heap, the collector, and the container around them

Objects live on the heap and are freed by a garbage collector, not by you, which removes the use-after-free and double-free bugs that dominate C and C++. Collectors rely on one strong observation: most objects die almost immediately. So the heap is split into generations. New objects go into a young generation that is collected often and cheaply; the few that survive are promoted to an old generation that is collected rarely and expensively. Long pauses come from the old generation, so "reduce garbage" usually means "stop promoting objects that should have died young".

CollectorHow it pauses the applicationRight for
SerialStops everything and collects on one threadTiny heaps and command-line tools
ParallelStops everything and collects on all coresBatch jobs, where only total throughput counts
G1 — the defaultShort pauses aimed at a target, 200 ms by defaultAlmost every service
ZGCPauses under a millisecond, whatever the heap sizeLatency-sensitive services and very large heaps; costs extra CPU and memory
ShenandoahConcurrent, with low pauses; generational since 25The same niche as ZGC; in most OpenJDK builds, not in Oracle JDK

The collector matters less, in practice, than the container around the JVM. Since JDK 10 the JVM reads the container’s memory limit rather than the host’s, and by default it takes a quarter of it for the heap: a 4 GB container gets a 1 GB heap, and most of the memory you pay for is never used. The common fix makes the opposite mistake. Setting the heap equal to the container limit — -Xmx4g in a 4 GB container — leaves no room for everything the JVM keeps outside the heap: class metadata, compiled code, a stack for every platform thread, network buffers, the collector’s own bookkeeping. The process outgrows the container, and the kernel kills it.

A JVM uses memory outside its heap: class metadata, compiled code, a stack for every platform thread, network buffers and the garbage collector’s bookkeeping. In a 4 GB container, a heap set equal to the limit leaves no room for any of it, so the whole process outgrows the container and the kernel kills it — exit code 137, no Java stack trace. Left to the default, the JVM takes a quarter of the limit for the heap, which is safe but leaves most of the memory unused. A percentage around 75 fits both. The 0.6 GB outside the heap is illustrative; Native Memory Tracking measures the real figure.

Spring: injection, auto-configuration and the proxy you do not see

Spring is close to universal on the Java server, and what it fundamentally provides is : a class declares what it needs in its constructor, and the container supplies it. That sounds like bureaucracy until you see the consequence — a class that receives its collaborators can be handed fakes in a test, so business logic is testable without a database, a network or a running server. Spring Boot adds auto-configuration, which looks at the libraries on the classpath and wires sensible defaults, and an embedded web server, so the application is one executable JAR. Boot 4.0, released in November 2025 on Spring Framework 7, requires Java 17 or later.

LayerWhat it doesWhere it bites
The containerBuilds and wires your objects, and manages how long each livesInjection into fields hides what a class needs; ask in the constructor
Auto-configurationWires defaults from what is on the classpathAdding one dependency can switch on a feature; --debug prints the condition report
Spring MVC / WebFluxMaps HTTP to methods: MVC blocks per request, WebFlux is reactiveWebFlux pays off only if the whole chain is non-blocking, the driver included
Spring Data JPAMaps objects to tables and derives queries from method namesLazy relations turn one call into N queries; the SQL log is the only honest witness
Transactions, caching, async@Transactional, @Cacheable, @Async around a methodThey work through a proxy, and a call that skips the proxy skips them
Spring implements @Transactional, @Cacheable and @Async with a proxy: a generated object that other beans hold instead of yours, which does its work and then calls your method. A call from another bean goes through it, so placeOrder runs in a transaction. A call from placeOrder to writeAudit in the same class goes to this, the real object, and never touches the proxy — so REQUIRES_NEW opens no new transaction and the audit record rolls back with the order. Nothing logs a warning. Moving writeAudit into a separate bean puts the proxy back on the path.

Spring Boot’s own support window is shorter than Java’s. Each minor line gets about a year of free fixes: 3.5 lost its free support on 30 June 2026, 4.0 keeps it until 31 December 2026, and 4.1, released in June 2026, until 31 July 2027. After that, fixes are commercial. A team that upgrades Java every two years and Spring Boot every two years is outside the free window for half of that time.

Threads, and what virtual threads changed

A Java thread has always been an operating-system thread. That gives real parallelism across cores — unlike PHP, Node.js or CPython, one Java process can keep every core busy — and it costs a stack of about a megabyte per thread, which is why servers keep threads in a pool. The classic Java web server assigns one thread per request, and its concurrency limit is the pool: Tomcat, embedded in Spring Boot, defaults to 200 threads. Two hundred requests in flight, however many of them are only waiting for a database.

Virtual threads, final in JDK 21, remove that ceiling. They are scheduled by the JVM onto a small number of operating-system threads, called carriers — by default one per core — and cost far less than a platform thread: a virtual thread’s stack lives on the heap and grows only as deep as it is used. When a virtual thread blocks on I/O, the JVM unmounts it: its stack is set aside on the heap and the carrier picks up another virtual thread. Ordinary blocking code, written in the plain style everyone knows, scales to hundreds of thousands of concurrent requests. In Spring Boot it is one property, spring.threads.virtual.enabled=true. That is why the reactive style, which bought the same scale at the price of a fully non-blocking chain and unreadable stack traces, is needed far less often than it was.

Virtual threads, final in JDK 21, run on a few operating-system threads called carriers, by default one per core. When a virtual thread blocks on I/O, the JVM unmounts it, keeps its stack on the heap and gives the carrier to another virtual thread; when the answer arrives, the thread is mounted again, possibly on a different carrier. That is how plain blocking code scales to hundreds of thousands of concurrent requests. A pinned virtual thread cannot be unmounted and keeps its carrier while it waits; with as many carriers as cores, a few pinned threads can stop all the others. On JDK 21 to 23 that happened inside synchronized; JDK 24 fixed it, and native code and class initialisers still pin.

A virtual thread that cannot be unmounted is called pinned, and it keeps its carrier while it waits. On JDK 21 to 23 that happened whenever a thread blocked inside a synchronized block — common in older libraries — and with as many carriers as cores, a few pinned threads could stop an application, which is the fifth case below. JDK 24 fixed synchronized; a thread still pins while it is inside native code or a class initialiser. Switching virtual threads on changes more than the thread count, and most of the changes move the bottleneck rather than remove it:

What you hadWhat changes with virtual threadsWhat to do
A pool of 200 threads as the limitThe limit is gone; 10,000 requests now reach the database at onceLimit what is scarce with a Semaphore, not with a thread pool
A connection pool of 10 (HikariCP’s default)It becomes the bottleneck, and threads queue for itSize it for the database, and set a short wait timeout
A ThreadLocal cache per threadA million threads hold a million copiesScopedValue for context; a shared cache for objects
Pooling threads to save their costPointless — a virtual thread is cheaper than a pool slotOne new virtual thread per task
synchronized around a blocking callPins the carrier on 21–23; fine from 24Run 24 or later, or 25 for an LTS
CPU-bound workNothing — the limit is still the core countKeep it on a bounded pool of platform threads

Five cases, and what each one was missing

Each case below is documented in a vendor’s own advisory or roadmap, a government report, or the engineering write-up of the team it happened to. Three are security failures, one is a cost, and one is an outage — and in four of the five, what was missing was knowledge of what was actually running.

CaseWhat happenedWhat was missing
Deserialisation gadgets, 2015Ordinary library classes, deserialised from the network, ran attackers’ commandsA rule never to deserialise untrusted bytes
Equifax, 2017A Struts flaw fixed in March was exploited from May; 147 million people’s data takenAn inventory of where each library runs
Log4Shell, Dec 2021A logging library executed lookups inside logged textAn SBOM, and no outbound connections from servers
Oracle’s licences, 2019–2026Routine updates moved under paid terms, priced per employee since 2023Knowing which JDK build runs in production
Netflix, Jul 2024Virtual threads pinned every carrier, and instances stopped serving trafficWatching pinning before rollout — and JDK 24

The second and third cases are the same failure four years apart. Apache Struts, a Java web framework, disclosed and fixed -2017-5638 on 7 March 2017. Equifax’s security team circulated the alert, but the system that ran Struts — the online portal where consumers disputed their credit records — was not patched, and the scan meant to catch it did not find it. Attackers entered on 13 May and stayed for 76 days; the data of about 147 million people was taken. The government reports that followed read as a list of missing inventory: the company could not reliably say which of its systems ran the library. In December 2021 Log4Shell asked every organisation the same question at once. Log4j 2, the most widely used Java logging library, evaluated ${jndi:ldap://…} lookups written inside the messages it logged, so any string an attacker could get logged — a username, a User-Agent header — made the server connect out to the attacker’s machine and, in many configurations, load and run code from it. It was rated 10 out of 10. Patching took four releases in three weeks, and the slow part everywhere was not patching but finding: Log4j usually arrived as a dependency of a dependency.

The fourth case is not a vulnerability, and it has cost some organisations more than one. Every change in Oracle’s licensing, listed in the releases section, arrived as an ordinary update to the same product. Organisations that had standardised on Oracle JDK because it was the default download could find, at an audit, that the updates they had been applying were licensed per employee across the whole company. Teams that had standardised on an OpenJDK build — the same code — noticed nothing. The control was the same one Equifax and Log4Shell were missing, applied to the runtime instead of a library: knowing what is actually running.

Where it degrades

Java’s real advantage on a long project is not speed. It is that the tooling assumes the code will outlive the people who wrote it: the build is reproducible, the dependency graph is explicit, refactoring across a million lines is mechanical and safe, and a production profiler, Flight Recorder, ships with the JDK at a cost low enough to leave on. When a Java system degrades, it is rarely the language. It is the runtime left on defaults nobody chose, an annotation trusted to do what it cannot see, or a dependency nobody knew was there:

  • A heap equal to the container limit, which ends in an OOMKilled restart with no stack trace.
  • A heap left at the default quarter, paying for memory the JVM never uses.
  • Lazy JPA relations loaded in a loop, or in the view through open-in-view, turning one endpoint into hundreds of queries.
  • A @Transactional method called from its own class, so the proxy is bypassed and nothing rolls back as designed.
  • Virtual threads switched on with a connection pool of ten, which moves the queue rather than removing it.
  • A reactive stack adopted for scale on Java 21 or later, where plain blocking code now reaches the same place.
  • Exceptions caught and logged at every layer, so one failure produces five stack traces and no decision.
  • An interface and a factory per class, for a second implementation that never arrives.
  • Built-in serialisation accepting bytes from outside the process.
  • A JDK or a Spring Boot line outside its support window — or an Oracle build nobody chose.

The honest limit is the runtime’s own shape. A JVM is built to start once and run for a long time, and every answer to fast start-up in the JVM section trades something away for it. A garbage collector, even one with sub-millisecond pauses, still means memory overhead and a process whose timing is not fully under your control. And the ceremony that pays back over a decade costs real time on a two-week prototype or a fifty-line script, where a lighter tool is simply the better choice.

The practical check is three commands against a running production instance. java -version gives the release and the vendor — the support table and the licence. jcmd <pid> VM.flags shows the collector and the heap the JVM actually chose. And the dependency tree from mvn dependency:tree or gradle dependencies, kept as an , is the answer you will need on the morning of the next Log4Shell.

When to use it

Use it when

  • Long-lived systems with complex domains, where static types and safe refactoring pay back every year.
  • Services that must use every core in one process — real threads make that possible without extra processes.
  • High-concurrency on JDK 25 — virtual threads give reactive-level scale with ordinary blocking code.
  • Regulated environments, where an LTS release with years of security fixes is a requirement, not a preference.

Avoid it when

  • Short-lived functions that scale to zero, unless you will build and maintain a native image or an AOT cache.
  • Small scripts and one-off tooling, where the ceremony costs more than the whole task is worth.
  • Reactive frameworks chosen for scale on JDK 21 or later, where virtual threads reach the same place more simply.
  • Any environment where an unpredictable pause of even a millisecond is unacceptable — that is not this runtime.

Sources and method

What this page stands on

  • 16 sources
  • 4 documented failures
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.