JITtypical

What a JIT Costs

Compilation on the user's critical path, a warmup period where the program is measurably slower than itself, memory for code and profiles, benchmark numbers that will not sit still, and a writable-then-executable memory region that some platforms refuse to allow at all.

The question

What am I actually paying for a JIT, and when is the bill larger than the benefit?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The program has no single representation any more, and that is precisely the cost being accounted. At any moment it exists as bytecode, as profile data, as compiled code at one or more tiers, and as the metadata linking them — each occupying memory, each potentially stale, and the set of them changing as execution proceeds. This lesson works in the representation of the *whole system over time*: what exists, when, at what price. It exists to answer a question no snapshot of the generated code can — what does having a JIT cost when it is not paying off?

What this phase may assume or do

Nothing here is a transformation, so the precondition is what a runtime is entitled to assume from the platform beneath it. A JIT requires the ability to write bytes and then execute them, which means an operating system that permits a mapping to be made executable, a hardware instruction-cache maintenance path so that newly written instructions are actually the ones fetched, and a policy environment that allows it. Where any of those is withheld — a platform that forbids executable anonymous mappings, a hardened process with W^X enforced, an environment where code signing is mandatory — a JIT is not slow, it is impossible, and the runtime must fall back to interpretation or to ahead-of-time compiled code.

Key points

  • Warmup is distinct from startup: the program does useful work at less than its steady-state speed for a period that can be minutes and is re-entered rather than paid once.
  • Compilation competes with the application for CPU on the same machine, and the compile queue is a latency variable that shows up in tails rather than means.
  • A JIT holds several representations at once — bytecode, profiles, compiled code per tier, deoptimization metadata — and memory grows with what has ever been hot.
  • A full code cache stops compilation silently: performance regresses with no error to attribute it to.
  • Measurement becomes genuinely hard: results depend on warmup, iteration count, input order and what else ran in the process.
  • A JIT needs an exception to W^X, which some platforms refuse to grant, making an interpreted or ahead-of-time path a hard requirement rather than a fallback.
  • JIT-region memory is a known exploitation target, defended by write-then-remap, dual mappings, and randomization of constants and layout.
  • The technique wins for long-running, type-stable, memory-comfortable workloads on permissive platforms, and loses outside that envelope.

Warmup, and a compiler on the critical path

typicalWarmup duration varies by orders of magnitude across runtimes and workloads: a small script may be fully warm in milliseconds, a large JVM service in tens of seconds to minutes. It also depends on tier configuration — forcing the optimizing tier immediately shortens warmup and lengthens startup. Any specific number is a fact about one application on one runtime version, and the only useful figure is one measured on your own workload.

The most visible cost is a period during which the program runs slower than it will later, and it is worth separating two things people merge. Startup is the time before the program does anything useful — loading, parsing, initializing. Warmup is the time after that during which the program is doing useful work at less than its steady-state speed, because the profile is not yet representative and the compilations have not yet happened.

Warmup can be long. For a large server application it is routinely tens of seconds and can be minutes, and it is re-entered rather than paid once: a traffic shift makes new code hot, a deployment resets everything, a deoptimization storm can effectively restart it for one function. This is why load balancers ramp traffic to new instances, why readiness checks sometimes gate on synthetic warmup traffic, and why the first requests after a deploy are systematically worse than the rest — an engine internal that has become a deployment concern.

And warmup is a *latency* cost, not a throughput cost, which is why it hides from averages. A service whose mean latency is fine can have a warmup period that dominates its 99th percentile after every rollout. Looking at the latency percentiles rather than the mean is what makes it visible at all.

An ahead-of-time compiler's cost is charged to a build. A JIT's cost is charged to the running process, competing with the application for CPU, and in the worst case competing on the same core as the request that triggered the compilation. Background compilation threads mitigate this and do not remove it: the work is still being done by the machine that is serving traffic.

The queue is the part that surprises people. Crossing a threshold enqueues a compilation; it does not perform one. Under a burst — startup, a deployment, a traffic shift — many functions cross thresholds at once, the queue grows, and functions run in the wrong tier for far longer than the threshold implied. That produces a tail-latency effect with no single slow request to blame, which is exactly the shape of problem that is hardest to diagnose from application-level metrics.

There is also a subtler competition. The compiler thread's working set displaces the application's in the caches, and compiled code being installed invalidates instruction-cache lines. Neither is large, and both are the kind of second-order cost that makes a JIT's benefit on a benchmark larger than its benefit in a busy production process.

  • Compilation consumes CPU that the application would otherwise have, on the same machine, during the period when the application is least optimized.
  • The compile queue is a latency variable: threshold crossing enqueues, and depth under burst decides how long code runs in the wrong tier.
  • A deoptimization storm turns this cost from bounded into unbounded, because the same function is compiled repeatedly and forever.
  • Compiler threads and installed code perturb the caches the application is using, which is a small, real and rarely measured cost.
  • On a machine with few cores, background compilation is not free at all — it is directly contending with the request being served.

Memory: three copies and the metadata

A JIT holds more of the program in memory than any other execution strategy, because it holds several forms at once. The bytecode remains, because it is the deoptimization target and the source for future compilations. The compiled code exists, potentially at more than one tier for the same function. The profile data — counters, feedback slots, inline caches — is proportional to the number of sites in the loaded program rather than to the hot subset. And the deoptimization metadata described in [[deoptimization]] is emitted per deoptimization point and can rival the generated code in size.

None of these is reclaimed automatically in a useful way. Code caches are finite and, when they fill, engines stop compiling — silently, in the sense that the application sees a performance change with no error. Profiles for functions that were hot once persist unless something ages them. In a long-running process the memory attributable to the compilation machinery grows with how much of the program has *ever* been hot, which is a different and larger quantity than what is hot now.

This is why a JIT is a poor fit for constrained environments and why the alternatives in those settings are ahead-of-time compilation or plain interpretation. It is also why serverless and container platforms, where memory is directly priced, have driven so much interest in ahead-of-time compilation for languages that historically relied on JITs.

What a JIT keeps, and what it coststypical
What is heldScales withReclaimed whenWhat happens if it fills
BytecodeTotal code loadedThe code is unloaded entirelyNormal memory pressure
Compiled code (per tier)Code that has ever been hotInvalidated and swept, if the runtime does thatCompilation stops; performance silently regresses
Profile and feedback slotsNumber of sites in loaded code, not hot onesAged out, if the runtime ages themFeedback quality degrades or allocation fails
Deoptimization metadataimplementationDeoptimization points in compiled codeWith the code it belongs toCompilation of large methods is refused
Inline cachesCacheable sites, plus chain lengthWith the code, or on invalidationSites fall back to shared lookups

Numbers that will not sit still

A JIT makes performance measurement genuinely harder, and not by a little. The same code, run twice in the same process, can perform differently because the second run is warm. Two benchmarks in one process interfere: the first pollutes the profile of shared code, and the second is measured on caches specialized for the first. Iteration count changes which tier is measured. Input order changes which shapes a site saw. Even the order of the benchmark methods in the file can matter.

The standard defences all exist because of this: warm up before measuring, run each benchmark in a fresh process, measure many iterations and report a distribution rather than a mean, and use a harness that understands the runtime — JMH for the JVM exists largely to handle exactly these hazards. Benchmarking practice has a long catalogue of the ways this goes wrong; the point here is that a JIT is what makes so many of them possible.

The consequence for engineering practice is a real one. Performance regressions are harder to attribute, A/B comparisons need more samples to reach significance, and a change that looks like a large improvement may be a change in which tier the benchmark reached. On a JIT runtime, "we measured it and it was faster" needs more supporting detail than it does elsewhere.

The security surface: W^X and what it forbids

implementationConcrete mechanisms are platform-specific and change: macOS and iOS use MAP_JIT with per-thread write protection toggled by pthread_jit_write_protect_np; Linux JITs typically allocate with mmap and use mprotect to flip permissions; hardened kernels and some container policies can forbid making anonymous mappings executable at all. Whether a given runtime can JIT in a given deployment is an environment question that must be checked rather than assumed.

A JIT writes bytes into memory and then executes them. That is precisely the capability that memory-corruption exploits need, and the defence that has been standard for two decades — W^X, "write xor execute", where no page is simultaneously writable and executable — is a defence a JIT must be granted an exception to.

The exception is not total. Engines narrow it: allocate the region as writable, write the code, flush the instruction cache, then remap the region read-execute before any of it runs, so no page is ever both at once. Some go further with dual mappings — one writable view and one executable view of the same physical pages, with the writable view held only by the compiler thread — and hardware features such as Apple's per-thread MAP_JIT permission switching and ARM's memory-tagging extensions narrow the window further still. The residual risk is real: JIT-region memory is a well-known target, and JIT spraying — arranging for attacker-chosen bytes to appear inside generated code — is a documented technique that engines defend against by randomizing constants and code layout.

The practical consequence is that some platforms simply refuse. iOS forbids JIT compilation for ordinary applications, which is why JavaScript engines there run in interpreter or ahead-of-time modes for embedded content; some game consoles, many embedded targets, and hardened server configurations do the same. In those environments a JIT is not a slow option, it is not an option, and the language implementation must have an ahead-of-time or interpreted path — see [[aot-compilation]] and [[compiler-security]].

  • A JIT requires an exception to W^X, the standard mitigation against executing attacker-supplied data.
  • The standard narrowing is write-then-remap: pages are writable while being filled and read-execute before anything runs.
  • Dual mappings and per-thread permission switching narrow the window further, at the cost of platform-specific machinery.
  • JIT spraying — steering constants and code layout so attacker-chosen bytes land in executable memory — is why engines randomize both.
  • Some platforms forbid the capability outright, which makes an interpreter or ahead-of-time path a hard requirement rather than a fallback.
  • Compiled code is also unsigned code, which conflicts with code-signing and attestation policies independently of any memory-safety argument.

When the bill exceeds the benefit

Set out plainly, the accounting says a JIT wins when a process lives long enough to amortize compilation, when the hot set is small and type-stable, when memory is not the binding constraint, and when the platform permits it. Reverse any of those and the answer changes.

A command-line tool that runs for eighty milliseconds pays the whole machinery and collects nothing. A serverless function invoked once per cold container is the same case, at scale, which is exactly why ahead-of-time compilation and snapshotting have become mainstream for that deployment shape. A memory-priced container pays for code caches and profiles it would rather spend on the heap. A hard real-time path cannot tolerate a compilation or a deoptimization landing inside a deadline. And a locked-down platform will not run one at all.

The honest summary is that a JIT is an excellent answer for long-running, type-stable, memory-comfortable workloads on permissive platforms, and a poor one outside that envelope. Which is worth stating as directly as the wins, because the technique is frequently presented as strictly better than ahead-of-time compilation and is not — the two make opposite bets about process lifetime, and both bets are sometimes right.

How it works

The steps, in the order the compiler takes them.

  • Account startup and warmup separately, because they have different causes and different fixes: startup is loading and initialization, warmup is profile accumulation plus compilation latency.
  • Measure the compile queue: how many compilations are pending, how long they wait, and whether that correlates with the latency tail rather than the mean.
  • Attribute memory across the four consumers — bytecode, compiled code, profile data, deoptimization metadata — rather than treating "the runtime" as one number.
  • Watch for the silent regression: a full code cache stops compilation, so track code-cache occupancy as an operational metric rather than discovering it from a latency graph.
  • Benchmark in a fresh process per case, warm up explicitly, report distributions, and treat any single-number comparison on a JIT runtime as provisional.
  • Verify the platform permits executable mappings in the actual deployment environment, including hardened kernels and container security profiles, rather than assuming the runtime's default path is available.
  • Where the workload is short-lived or memory-priced, evaluate the ahead-of-time path deliberately rather than by default — it is a different bet, not a downgrade.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • Every deployment produces a latency spike lasting tens of seconds while instances warm up, and the spike is invisible in mean latency and obvious in the 99th percentile.
  • The code cache fills in a long-running process, compilation stops, and throughput degrades gradually with no error logged and nothing in the application to explain it.
  • A burst of tier-up requests during startup saturates the compiler threads, and request latency during that window is dominated by code running in the wrong tier.
  • A benchmark shows a large regression that turns out to be a difference in iteration count changing which tier was measured, and a week is spent optimizing code that was never the problem.
  • A short-lived process — a build tool, a serverless invocation, a test runner — pays the full JIT overhead thousands of times a day and never crosses a threshold that would repay it.
  • A deployment to a hardened environment fails to make executable mappings and the runtime silently falls back to interpretation, producing a large and initially inexplicable slowdown.
  • Memory usage in a container grows past its limit as compiled code and profiles accumulate, and the process is killed by the platform for a reason unrelated to the heap.

When it helps

  • Long-running server processes, where warmup is amortized over hours and steady-state throughput is what is being bought.
  • Workloads with a small, stable hot set, where the compile budget is spent once and repaid continuously.
  • Dynamically typed languages, where the specialization available at run time is worth far more than the machinery costs.
  • Environments with memory headroom and permissive execution policies, which is most conventional server deployment.
  • Situations where the code to be optimized is not knowable at build time — a query plan, a regex, a user-supplied template.

When it hurts

  • Short-lived processes of every kind: command-line tools, test runners, serverless invocations, build steps.
  • Memory-priced deployments, where code caches, profiles and metadata compete directly with the application's heap.
  • Hard real-time and low-latency paths that cannot tolerate a compilation or a deoptimization inside a deadline.
  • Locked-down platforms — iOS applications, some consoles, hardened kernels, code-signing regimes — where the required memory permissions are unavailable.
  • Performance engineering itself, where the non-determinism makes regression detection and attribution materially harder.
  • Reproducible or auditable execution, where "the same input produces the same instructions" is a requirement — see [[reproducible-compilation]].

What it costs

Every one of these is paid by something.

  • Runtime compilation buys profile-specialized code and pays with CPU taken from the application at exactly the moment the application is least optimized.
  • Tiering buys a shorter warmup and pays with several compiled forms of the same function held in memory, plus the implementation surface of every tier agreeing about the language.
  • Aggressive speculation buys the largest steady-state gains and pays with deoptimization risk, larger metadata, and performance that varies with input rather than with code.
  • Keeping compiled code and profiles buys instant re-entry to fast paths and pays memory proportional to what has ever been hot, which in a long-running process only grows.
  • A JIT buys adaptation to the actual workload and pays with a writable-then-executable memory region, which is a security exception that some environments will not grant at any price.
  • The whole strategy buys steady-state speed and pays with predictability: worse tails, harder measurement, and a performance profile that is a property of execution history rather than of the code.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Ahead-of-time compilation: no warmup, no runtime compile surface, no W^X exception, predictable performance, and no specialization to this execution — [[aot-compilation]].
  • Ahead-of-time compilation from a recorded profile, recovering much of the specialization at build time — [[profile-guided-optimization]], with the caveat that the profile is from a different run.
  • Interpretation only: minimal memory, instant start, no security exception, and an order of magnitude off native on hot code — [[interpreter-performance]].
  • Snapshotting a warmed process image and restoring it, so warmup is paid once at build time rather than per instance. CRaC for the JVM and V8 startup snapshots take this route, at the cost of image size and of the snapshot matching the deployment.
  • A hybrid: ahead-of-time code as the baseline with a JIT permitted to replace hot methods, which is .NET's ReadyToRun and Android's ART model, and which makes startup and steady state separately tunable.
  • Move the hot work out of the managed language into native code, which removes the question rather than answering it — the strategy behind most fast numerical Python.

See it for yourself

The flag, dump or tool that shows you this directly.

  • JVM: -XX:+PrintCodeCache and -XX:ReservedCodeCacheSize show and set the code-cache budget; a full cache logs a warning and stops compilation, which is the silent regression made visible.
  • JVM: -Xint forces interpretation and -XX:TieredStopAtLevel=1 restricts to C1, which brackets what the JIT is contributing on your workload rather than on a benchmark.
  • JVM: jcmd <pid> Compiler.queue shows pending compilations, which is the queue-depth variable behind warmup tails.
  • Node/V8: --max-old-space-size and the --trace-gc family for the memory side; --jitless disables runtime code generation entirely, which is also what a W^X-restricted environment forces.
  • .NET: DOTNET_TieredCompilation=0 and DOTNET_ReadyToRun=0 isolate the contributions of tiering and of ahead-of-time images to startup and steady state.
  • For the security posture, check the actual deployment: on Linux, whether the process may mprotect an anonymous mapping executable under the container's seccomp and SELinux policy; on Apple platforms, whether the MAP_JIT entitlement is present.
  • Use JMH on the JVM, or an equivalent harness elsewhere, for any measurement at all — it exists specifically to control the hazards this lesson describes.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "JIT is always slower because compilation happens at runtime." Compilation is paid once per hot method and amortized across every subsequent execution, and the resulting code can be better than a static compiler's because it knows the actual types and branch bias. The cost is real and it is a warmup cost, not a permanent tax — which is why long-running services use JITs and short-lived tools should not.
  • "Warmup is a benchmark artifact." It is a production cost paid after every deployment, every scale-out event and every traffic shift, and it is the reason load balancers ramp traffic to new instances.
  • "The JIT compiles in the background, so it is free." The background thread runs on the same machine as the application, competing for CPU and cache, during the period the application is at its slowest.
  • "Memory overhead is just the compiled code." It is bytecode plus profiles plus compiled code at several tiers plus deoptimization metadata, and the profile component scales with the whole loaded program rather than the hot part.
  • "W^X is a hardening detail that does not affect language implementations." It decides whether a JIT can exist in a given environment at all, which is why iOS-restricted browsers and hardened server deployments run interpreters or ahead-of-time code.
  • "A JIT and an ahead-of-time compiler are the same technology at different times, so the JIT strictly dominates." They make opposite bets about process lifetime and memory, and each bet is right for some deployments. Choosing between them is a real decision with real losers on both sides.

Misconceptions

The claim, and what is actually true.

Adding a JIT is a strict improvement over interpreting.
It is a bet that the process lives long enough and has memory enough to repay compilation. For short-lived or memory-constrained workloads the interpreter wins outright, which is why runtimes keep one and why --jitless and -Xint are supported modes rather than debugging curiosities.
Warmup is over once the first requests are served.
Warmup lasts until the profile is representative and the compilations have completed, which for a large service is tens of seconds or more — and it restarts whenever the workload shifts, code is invalidated, or a new path becomes hot.
The security concern with JITs is that generated code might be malicious.
The generated code is the runtime's own output. The concern is the *capability*: a writable-then-executable region is exactly what a memory-corruption exploit needs, which is why the mitigation is about narrowing the window and randomizing contents rather than about validating what the compiler emitted.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

A JIT buys speed on code that runs a lot, and it is not free. The program is slower for a while at the start; the compiler uses processor time that the program wanted; several copies of the program are kept in memory at once; measurements become unreliable because the answer depends on how long you have been running; and the whole thing needs permission to write instructions into memory and then run them, which some devices do not grant.

practical

Three habits follow. Measure after warmup and measure warmup separately, in a fresh process per case, and never trust a single number from a JIT runtime. Track code-cache occupancy as an operational metric, because a full cache silently stops compilation and looks like an unrelated regression. And when choosing a runtime for a short-lived workload — a CLI, a serverless function, a test runner — evaluate the ahead-of-time option seriously rather than assuming the JIT is the better default. Every one of these is a case where the standard advice for an ahead-of-time compiled language gives the wrong answer.

advanced

The framing worth carrying is that a JIT converts a build-time cost into a run-time cost, and everything on this list is a consequence of that conversion. Compile time becomes latency. Compiler memory becomes resident memory. The compiler's determinism becomes the program's non-determinism. The compiler's output, previously a signed artifact produced on a build machine, becomes unsigned bytes written into a live process — which is why the security story is not an afterthought but a direct consequence of the architecture. Seen that way, the interesting recent developments all make sense as attempts to move some of the cost back: profile-guided ahead-of-time builds, ReadyToRun images, Android's cross-run profile persistence, JVM checkpoint-and-restore, and V8 startup snapshots are all ways of paying at build or install time for something the JIT would otherwise pay for at run time. The endpoint is not "JIT wins" or "ahead-of-time wins" but a spectrum of when compilation happens, with real engineering pressure to place each piece of the work at the point where it is cheapest.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

typicalWarmup durations, code-cache sizes and memory overheads vary by orders of magnitude across runtimes, versions and workloads. Nothing in this lesson is a number to quote — the categories transfer and the magnitudes must be measured on the workload and runtime in question, since a small script and a large service differ by more than a hundredfold on every axis here.
implementationMechanisms for handling W^X differ by platform and change: MAP_JIT with per-thread write protection on Apple platforms, mmap plus mprotect on Linux, dual mappings in some engines, and outright prohibition on iOS applications and under some hardened kernel and container policies. Whether a JIT can run in a specific deployment is an environment fact to verify rather than a property of the runtime.
specNo language specification requires or forbids just-in-time compilation; the JVM specification and ECMAScript both define semantics and leave execution strategy to the implementation. That is what makes an interpreted or ahead-of-time fallback on a restricted platform a conforming implementation rather than a degraded one, and it is why "a language is compiled or interpreted" is a category error.
simplifiedAtlasLang has no JIT, so none of these costs is measured by our simulator; the lesson is an account of what production runtimes pay, drawn from their documented behaviour and flags. Our VM does illustrate the opposite end of the spectrum honestly — no compilation, no warmup, no code cache, and an order of magnitude off native, permanently.

If you were asked this in an interview

  • Itemize what a JIT costs. Which of those costs would make you choose an ahead-of-time compiler instead?
  • Why is warmup a production concern rather than a benchmarking one?
  • What does W^X have to do with language implementation, and what happens on a platform that enforces it strictly?
  • A long-running service degrades gradually over days with no code change and no memory leak in the heap. What would you check?
  • How would you benchmark two implementations honestly on a JIT runtime?

Connections

Domains that do not exist yet
  • DevOps / Production Engineering — Warmup as a deployment concern: traffic ramping, readiness gating, cold-start capacity planning and snapshot-based restore
    Everything in this lesson turns into an operational decision — how fast to send traffic to a new instance, how much headroom to keep for compilation, whether to pay for ahead-of-time images. The mechanism is a compiler one and the consequences are entirely in how a service is deployed and scaled.
  • Programming Languages & Runtime Internals — The code cache: sizing, eviction, sweeping and what a runtime does when it cannot allocate executable memory
    The memory costs enumerated here are held and managed by the runtime, and its policy on eviction and cache exhaustion decides whether a full cache is a graceful degradation or a silent cliff. The compiler produces the code; how long it survives is not the compiler's decision.
  • Testing & Reliability Engineering — Benchmarking methodology under non-determinism: fresh processes, warmup protocols, distributions rather than means
    A JIT makes performance a property of execution history, which breaks the assumptions behind naive measurement. The statistical discipline required to compare two versions honestly is a testing and measurement subject, and harnesses like JMH exist because that discipline could not be left to individual judgement.