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.
| Job | The current way | What it replaced | JDK |
|---|---|---|---|
| A class that only carries data | record | Fields, a constructor, getters, equals and hashCode by hand — or Lombok | 16 |
| A closed set of subtypes | sealed with permits | An open hierarchy, or a type field and a comment | 17 |
| Branching on type | switch with type patterns, checked for completeness | An if chain of instanceof, each followed by a cast | 21 |
| Taking a record apart | Record patterns: case Point(var x, var y) | Calling the accessors one by one | 21 |
| A variable you must declare but never use | _ | A name like ignored | 22 |
| Multi-line text — SQL, JSON | Text blocks, """ | Concatenation with \n | 15 |
| Types of local variables | var | The type written on both sides | 10 |
| Dates and times | java.time: Instant, LocalDate, ZonedDateTime | Date and Calendar | 8 |
| Calling an HTTP API | java.net.http.HttpClient — HTTP/3 since 26 | HttpURLConnection, or a third-party client | 11 |
| The first and last element | getFirst(), getLast(), reversed() | list.get(list.size() - 1) | 21 |
| A custom step in a stream | Stream gatherers | Collecting to a list and looping over it | 24 |
| Waiting on I/O at scale | Virtual threads | A bounded thread pool, or reactive code | 21 |
synchronized around a blocking call | Safe on virtual threads — no longer pins the carrier | Rewriting it with ReentrantLock | 24 |
| Per-request context | ScopedValue | ThreadLocal | 25 |
| Several subtasks as one unit | Structured concurrency — still a preview | An ExecutorService and a list of futures | preview |
| Calling native code | Foreign Function & Memory API | JNI | 22 |
| A small program’s entry point | void main() in a compact source file | public static void main(String[] args) inside a class | 25 |
| Faster start-up | An AOT cache recorded in a training run | Loading, linking and profiling from zero on every start | 24 · 25 |
| The default garbage collector | G1, in every environment | Serial, chosen silently on one CPU or under 1792 MB | 27 |
| The header on every object | 8 bytes — compact headers, by default | 12 bytes | 25 · 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 LTSLong-Term Support — a release patched for yearsA release line that keeps receiving security and critical fixes for a published period after newer versions ship, but no new features. It lets a team upgrade on its own schedule — until the end date on the support table, after which the next vulnerability has no fix at all.All terms — 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.
| JDK | Released | Oracle Premier Support | Oracle Extended Support |
|---|---|---|---|
| 8 LTS | Mar 2014 | Mar 2022 | Dec 2030 |
| 11 LTS | Sep 2018 | Sep 2023 | Jan 2032 |
| 17 LTS | Sep 2021 | Sep 2026 | Sep 2029 |
| 21 LTS | Sep 2023 | Sep 2028 | Sep 2031 |
| 25 LTS | Sep 2025 | Sep 2030 | Sep 2033 |
| 27 | Sep 2026 | Mar 2027 | — |
| 29 LTS | Sep 2027, planned | Sep 2032 | Sep 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.
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 JITJust-In-Time compilation — compiling while runningCompiling code while the program is already running: the runtime interprets it at first, watches which parts are hot, and turns those into machine code on the fly. That is why the first seconds after start are slower than the minutes that follow.All terms 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:
| Option | What it does | What it costs |
|---|---|---|
| Plain JVM | Loads and links on every start, then warms up | Seconds to start; nothing to configure |
| AOT cache, JDK 24+ | A training run records loaded classes and, since 25, method profiles; later starts reuse them | A training step in the build; the cache must match the JDK and the classpath |
| CRaC | Saves a warmed-up process to disk and restores it | Linux only; open files and connections must be closed and reopened around the checkpoint |
| GraalVM Native Image | Compiles the whole application into an executable ahead of time | Starts 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".
| Collector | How it pauses the application | Right for |
|---|---|---|
| Serial | Stops everything and collects on one thread | Tiny heaps and command-line tools |
| Parallel | Stops everything and collects on all cores | Batch jobs, where only total throughput counts |
| G1 — the default | Short pauses aimed at a target, 200 ms by default | Almost every service |
| ZGC | Pauses under a millisecond, whatever the heap size | Latency-sensitive services and very large heaps; costs extra CPU and memory |
| Shenandoah | Concurrent, with low pauses; generational since 25 | The 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.
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 DIDependency Injection — dependencies handed in from outsideInstead of creating the objects it needs, a class receives them ready-made from outside. This is the thing that makes code testable: in a test you hand it a fake where production hands it the real database.All terms: 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.
| Layer | What it does | Where it bites |
|---|---|---|
| The container | Builds and wires your objects, and manages how long each lives | Injection into fields hides what a class needs; ask in the constructor |
| Auto-configuration | Wires defaults from what is on the classpath | Adding one dependency can switch on a feature; --debug prints the condition report |
| Spring MVC / WebFlux | Maps HTTP to methods: MVC blocks per request, WebFlux is reactive | WebFlux pays off only if the whole chain is non-blocking, the driver included |
| Spring Data JPA | Maps objects to tables and derives queries from method names | Lazy relations turn one call into N queries; the SQL log is the only honest witness |
| Transactions, caching, async | @Transactional, @Cacheable, @Async around a method | They work through a proxy, and a call that skips the proxy skips them |
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.
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 had | What changes with virtual threads | What to do |
|---|---|---|
| A pool of 200 threads as the limit | The limit is gone; 10,000 requests now reach the database at once | Limit 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 it | Size it for the database, and set a short wait timeout |
A ThreadLocal cache per thread | A million threads hold a million copies | ScopedValue for context; a shared cache for objects |
| Pooling threads to save their cost | Pointless — a virtual thread is cheaper than a pool slot | One new virtual thread per task |
synchronized around a blocking call | Pins the carrier on 21–23; fine from 24 | Run 24 or later, or 25 for an LTS |
| CPU-bound work | Nothing — the limit is still the core count | Keep 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.
| Case | What happened | What was missing |
|---|---|---|
| Deserialisation gadgets, 2015 | Ordinary library classes, deserialised from the network, ran attackers’ commands | A rule never to deserialise untrusted bytes |
| Equifax, 2017 | A Struts flaw fixed in March was exploited from May; 147 million people’s data taken | An inventory of where each library runs |
| Log4Shell, Dec 2021 | A logging library executed lookups inside logged text | An SBOM, and no outbound connections from servers |
| Oracle’s licences, 2019–2026 | Routine updates moved under paid terms, priced per employee since 2023 | Knowing which JDK build runs in production |
| Netflix, Jul 2024 | Virtual threads pinned every carrier, and instances stopped serving traffic | Watching 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 CVECommon Vulnerabilities and Exposures — a public ID for a known vulnerabilityA public identifier for a known security flaw, such as CVE-2021-44228 for Log4Shell, so that vendors, scanners and advisories all mean the same bug. The number says a flaw exists; how dangerous it is for you depends on whether you run the affected version and expose the affected part.All terms-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
OOMKilledrestart 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
@Transactionalmethod 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 SBOMSoftware Bill of Materials — a list of what is insideA machine-readable inventory of every component a piece of software is built from, including the dependencies pulled in by other dependencies. It matters on the day a vulnerability is announced: without one, the question "are we affected?" is answered by guessing.All terms, 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 APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms 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
Practices and where the terms come from
every building block and the JDK it became final in, and support- StandardJEP 444: Virtual Threads (opens in a new tab)OpenJDK
- StandardJEP 491: Synchronize Virtual Threads without Pinning (opens in a new tab)OpenJDK
- StandardJEP 483: Ahead-of-Time Class Loading & Linking (opens in a new tab)OpenJDK
- StandardJEP 523: Make G1 the Default Garbage Collector in All Environments (opens in a new tab)OpenJDK
- StandardJEP 519: Compact Object Headers (opens in a new tab)OpenJDK
- StandardJEP 534: Compact Object Headers by Default (opens in a new tab)OpenJDK
- DocumentationJDK 27 (opens in a new tab)OpenJDK · Sep 15, 2026
- DocumentationOracle Java SE Support Roadmap (opens in a new tab)Oracle
- DocumentationSpring Boot (opens in a new tab)spring.io
The failures this page learns from
the section “Five cases, and what each one was missing”- ArticleWhat Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability. (opens in a new tab)Foxglove Security · Nov 6, 2015
- Documentationfrohoff/ysoserial (opens in a new tab)GitHub
- StandardJEP 290: Filter Incoming Serialization Data (opens in a new tab)OpenJDK
- DocumentationS2-045 — Apache Struts 2 security bulletin (opens in a new tab)Apache Software Foundation · Mar 2017
- Regulator reportGAO — Data Protection: Actions Taken by Equifax and Federal Agencies in Response to the 2017 Breach (opens in a new tab)U.S. Government Accountability Office · Aug 2018
- DocumentationApache Log4j — Security: CVE-2021-44228 (opens in a new tab)Apache Logging Services
- PostmortemJava 21 Virtual Threads — Dude, Where’s My Lock? (opens in a new tab)Netflix Technology Blog · Jul 29, 2024
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.