// Transmissions — What We Shipped
You already know how to make a computer do a thing. What you need from Java is the set of ideas that make its programs shaped the way they are — because those ideas, not the syntax, are what your agent cannot supply and what a reviewer will judge you on.
Containers, proxies, mapping, transactions — the parts every Java framework is made of
The ten technologies worth knowing, and what each one is really for
How to learn this when an agent writes the syntax for you
PHP and Python each have an interpreter that exists to run one language. The JVM is a general-purpose virtual machine with a published instruction set, and Java is merely its best-known source language — Kotlin, Scala, Clojure and Groovy all compile to the same bytecode and interoperate at the object level. That is why "a Java shop" often means "a JVM shop".
Compilation happens twice. javac turns source into bytecode, which is portable and not especially fast. Then the JIT compiles hot bytecode into machine code while the program runs, using facts it can only know at runtime: which branch actually gets taken, which type actually shows up at a call site, whether a lock is ever contended. A long-running JVM is therefore faster after an hour than after a second.
The JVM is a profiling optimiser wearing a runtime as a disguise. It speculates — inlines a method assuming the type it has always seen, elides a lock nobody contends — and holds a deoptimisation escape hatch for when the speculation turns out wrong. Almost everything strange about Java performance follows from that: warmup, why benchmarks lie, why a microservice that restarts every ten minutes never gets fast.
Serverless and aggressive autoscaling are hostile to this design: you pay the warmup and never collect the winnings. That single fact is why GraalVM native images and Quarkus exist — they trade peak throughput for starting fast. Choosing between them is an architecture decision, not a preference.
This is the idea to internalise first, because everything else inherits from it. PHP is shared-nothing: a request arrives, an interpreter builds a world, the response goes out, the world is destroyed. Global state is a per-request illusion and leaks are bounded by the request.
A Java server is one long-lived process holding many requests at once. Objects outlive requests. Caches are real caches. A static field is genuinely global, shared by every concurrent request, for the life of the process.
| Concern | PHP / FPM | Java / JVM |
|---|---|---|
| Lifetime | One request | Days or months |
| Shared state | Only in Redis/DB | In the heap, by default |
| A leak | Freed at request end | Grows until OOM |
| Warm caches | External only (opcache aside) | Free, in-process |
| Concurrency | Processes, isolated | Threads, sharing memory |
| Startup cost | Paid per request, small | Paid once, large |
Java trades the safety of amnesia for the power of memory. Everything Java gives you — real in-process caches, connection pools, background schedulers, hot JIT code — comes from the process remembering. Everything Java asks of you — thread safety, leak discipline, careful statics — is the bill for that same memory.
You never free anything. The garbage collector traces from a set of roots (stack frames, statics, live threads) and anything unreachable is reclaimed. The consequence is not "memory is free"; it is that memory pressure becomes latency, because collection costs CPU and sometimes pauses.
Modern collectors lean on the generational hypothesis: most objects die young. So the heap is split, new objects land in a small nursery that is collected cheaply and often, and the few survivors are promoted to an old region collected rarely and expensively. Which collector you run is a latency/throughput choice: G1 is the balanced default, ZGC and Shenandoah are built for sub-millisecond pauses on large heaps, Parallel maximises raw throughput and does not care about pauses.
Two failure shapes worth recognising on sight. Allocation churn: code that allocates furiously in a hot loop keeps young collections running constantly — throughput dies without any single slow method. The unintentional cache: a static map that is only ever written to. It is not a leak in the C sense; every entry is reachable, which is exactly why it is never collected. Java's characteristic memory bug is accidental reachability, not forgetting to free.
Java's type system is nominal and checked ahead of time. Coming from PHP and Python, the temptation is to treat that as ceremony. It is better understood as machine-checked documentation: a contract the compiler enforces and, more importantly, that tooling and an agent can read. The richer your types, the more of your intent survives contact with other people — and the better your AI's output, because the compiler rejects its wrong guesses.
In a dynamic language your types live in your head and in tests. In Java they live in the signature, and the compiler is a reviewer that reads every line, never gets tired, and never approves out of politeness. Design the types first and the code becomes mostly obvious.
PHP gives you no threads; Python has a GIL that mostly saves you from yourself. Java gives you real threads over shared mutable memory, which is genuine power and the source of its nastiest bugs — the ones that pass every test and fail in production under load.
The Java Memory Model is the rulebook. Its core idea is happens-before: without an explicit synchronisation edge, one thread is under no obligation to ever see another thread's writes. Not "sees them late" — never, legally, because the compiler and CPU may reorder and cache freely. Locks, volatile, and the concurrent collections exist to create those edges.
The ladder of tools, roughly in the order you should reach for them:
Virtual threads (Project Loom) are the biggest change to this in twenty years. Platform threads are OS threads: expensive, so you pool a few hundred, so blocking on I/O is disastrous, so the industry spent a decade writing reactive code to avoid blocking. Virtual threads are cheap enough to have millions, and blocking one costs almost nothing. The architectural consequence is that plain blocking code becomes viable again at scale, and much of the reactive complexity becomes optional.
Concurrency bugs are not reproducible on your laptop, and "it worked in testing" is the normal presentation. Treat any shared mutable field reachable from two requests as a defect until you can name the mechanism making it safe.
Java splits failures in two. Checked exceptions are part of a method's signature and callers must acknowledge them; unchecked ones propagate silently up the stack. The distinction is contested, but the intent is useful: checked means "a caller can plausibly do something about this" (the file is missing, the remote said no), unchecked means "this is a bug or an environment failure" (null where there should not be one, database down).
The most common sin in Java codebases is catching an exception, logging it, and continuing — converting a loud failure into a silent wrong answer. The second is wrapping every exception in a generic one and losing the cause. Both are worth spotting in your agent's output.
A jar is a zip of classes. The classpath is an ordered list of them, and classes are loaded lazily, by name, first-match-wins. Two jars containing different versions of the same class is legal, and the winner is whichever the loader reaches first — the classic "it works locally" failure. Dependency tools resolve version conflicts for you; shading rewrites a library's package names so two versions can coexist; the module system (JPMS) lets a jar declare what it exports and what it requires, so the mess is at least declared.
Your dependency list is not a list. It is a graph, flattened into one namespace at runtime by rules you did not write. Knowing how to print that graph and read a conflict is a genuine Java skill, and it is the first thing to learn about your build tool.
Part II — what every Java framework is made of →
Written for someone who has an agent for the syntax and needs the ideas instead. Nothing here is a code sample on purpose — if you can name the mechanism, you can ask for the code and judge what comes back.