Runtime Startup, Loading and Lifecycle

Before your first line of code runs, the runtime must start itself, find and load your code and its libraries, check them, connect their references, run initialisation code, and finally call your entry point.

Learning Objectives

Describe runtime installation, launchers, discovery and selection; loading of modules, assemblies, class files and scripts; parsing, validation, linking and dependency resolution.

Explain JVM class loading and initialisation, .NET assembly loading and metadata, Python's import system and module cache, JavaScript module loading and WebAssembly instantiation.

Trace static initialisation, bootstrapping, standard-library initialisation, entry points, arguments and environment, configuration, shutdown hooks, finalisation caveats, exit codes, crash handling, version selection, packaging, containers, serverless cold starts and AOT deployment.

Plain-Language Explanation

Before your first line of code runs, the runtime must start itself, find and load your code and its libraries, check them, connect their references, run initialisation code, and finally call your entry point. When the program ends, the runtime runs shutdown hooks and reports an exit code. All of that takes time — the "cold start".

Precise Technical Explanation

Runtime installation and deployment Shared runtimes (a system JDK,.NET runtime, Python), bundled runtimes (self-contained.NET apps, jlink images, PyInstaller), or none (native binaries, Native AOT).

java, dotnet, python, node executables parse arguments, locate the runtime library (libjvm, hostfxr/coreclr) and start it.

Runtime discovery and selection JAVA_HOME and PATH, .NET's global.json/runtimeconfig.json roll-forward policies, Python virtual environments and py launcher, nvm/.nvmrc for Node.

Loading modules, assemblies, class files, or scripts Reading code units from disk, archives (JARs), or the network; mapping them into runtime data structures.

Parsing and validation during loading Class-file format checks and bytecode verification (JVM), metadata validation (CLR), WebAssembly validation, JavaScript parsing.

Symbol resolution and linking Turning symbolic references (java/lang/System.out) into direct ones — lazily on first use (JVM resolution) or eagerly.

Class paths and module paths, NuGet/deps.json probing, sys.path, Node's node_modules resolution and package exports.

Class loading and initialization (JVM concepts) Loading (find bytes), linking (verify, prepare statics with defaults, resolve), initialisation (run <clinit> — static initialisers — on first active use, exactly once, thread-safely)..NET assembly loading and metadata

Assemblies carry metadata tables (types, methods, references); the runtime loads them on first use through AssemblyLoadContext; type loading builds method tables; static constructors run before first access.

Python module import and module cache import checks sys.modules, then finders and loaders locate the module, compile (or load cached .pyc), create a module object, register it in sys.modules *before* executing its code (which is why cycles see partial modules), then run its top level.

JavaScript module loading ES modules: fetch and parse the module graph, link imports to exports (live bindings), then evaluate in dependency order; CommonJS require loads and evaluates synchronously with a cache.

WebAssembly module instantiation Compile (validate + compile) a module, then instantiate it with imports, creating memories, tables and globals and running the start function.

Global/static initialization Static initialisers and module top-level code; order across units is often lazy (JVM, CLR) or import-ordered (Python, ES modules); C++ has the "static initialisation order fiasco" across translation units.

The runtime initialises itself: heap, core classes or built-in modules, the interpreter, JIT, threads.

Standard library initialization Core libraries load lazily; CPython imports a small set of modules at start-up (site, codecs); -S skips site.

Main entry point and launch semantics public static void main(String[]), static void Main()/top-level statements, if __name__ == "__main__":, Node's entry script, WebAssembly _start (WASI) or exported functions.

Command-line arguments and environment args, sys.argv, process.argv, environment variables; runtime-specific options are separated from program arguments.

Runtime configuration and environment variables JAVA_TOOL_OPTIONS, DOTNET_* variables, PYTHONPATH/PYTHONHASHSEED, NODE_OPTIONS.

Shutdown hooks and exit handlers Java Runtime.addShutdownHook, Python atexit, Node process.on("exit"),.NET AppDomain.ProcessExit; they run on normal exit and on termination signals the runtime handles, not on hard kills.

Resource finalization caveats Finalizers may not run at exit; flush and close explicitly.

0 for success, non-zero for failure; uncaught exceptions typically exit with 1; the meaning beyond that is convention.

Crash handling and dumps Fatal-error logs (hs_err_pid*.log), core dumps, createdump (.NET), Node's --report-on-fatalerror.

Runtime version selection Multiple runtimes side by side; applications pin or roll forward; version mismatches surface as UnsupportedClassVersionError or missing framework errors.

Compatibility and deployment packaging Fat JARs, self-contained apps, wheels with native extensions per platform, bundlers for JavaScript.

Containers and runtime images overview Images bundle the runtime and app; slim images and jlink/trimmed runtimes reduce size; respect container memory limits (modern JVMs and.NET read cgroup limits).

Serverless/cold-start considerations Every cold start pays launcher, runtime boot, loading, initialisation and JIT warm-up; mitigations include snapshots (AWS SnapStart, V8 snapshots), AOT compilation, smaller dependency graphs and lazy initialisation.

Native image and AOT deployment overview GraalVM native-image and.NET Native AOT compile the app and needed runtime parts ahead of time: fast start-up and low memory, with restrictions on reflection and dynamic code loading (closed-world assumption).

Why It Matters in Real Systems

Start-up dominates CLI tools, serverless functions and autoscaling; initialisation order bugs cause intermittent failures; shutdown handling decides whether data is flushed and connections are drained.

Key Terms

Executable that starts a runtime Resolving symbolic references JVM class initialiser method

Registry of loaded modules (sys.modules) Code run as the runtime exits

Diagram, Execution Trace or State Transition

Text equivalent: the launcher starts the runtime, which loads, validates and links the entry code, runs static initialisation, calls the entry point, and on exit runs shutdown hooks before returning an exit code.

flowchart LR
  L[Launcher] --> B[Runtime bootstrap] --> LD[Load entry module/class] --> V[Validate / verify] --> LK[Link / resolve] --> I[Static init] --> M[Entry point] --> R[Run] --> S[Shutdown hooks] --> X[Exit code]

Syntax or Instruction Format

No dedicated source syntax; this is a runtime or architecture concept. Lifecycle APIs: static { } (Java), Runtime.addShutdownHook, atexit.register, process.on("exit"), if __name__ == "__main__":.

Complete Example

JVM class initialisation is lazy and runs once, on first active use (Temurin JDK 21):

CPython start-up and import costs measured with -X importtime (CPython 3.13.1; microseconds from one real run — self is the module's own time, cumulative includes its imports; the program runs a child interpreter and prints the slowest entries):

Exit codes and exit handlers (Node.js 24.18):

public class InitOrder {
    static class Config {
        static { System.out.println("  Config.<clinit> runs"); }
        static final String NAME = computeName();
        static String computeName() { System.out.println("  computing NAME"); return "prod"; }
    }

    static class Unused {
        static { System.out.println("  Unused.<clinit> runs"); }
    }

    public static void main(String[] args) {
        System.out.println("main starts");
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("shutdown hook runs")));
        System.out.println("first use of Config: " + Config.NAME);
        System.out.println("second use of Config: " + Config.NAME);
        System.out.println("main ends (Unused was never initialised)");
    }
}
main starts
  Config.<clinit> runs
  computing NAME
first use of Config: prod
second use of Config: prod
main ends (Unused was never initialised)
shutdown hook runs
import subprocess, sys

result = subprocess.run([sys.executable, "-X", "importtime", "-c", "import json, decimal"],
                        capture_output=True, text=True)
rows = []
for line in result.stderr.splitlines()[1:]:            # "import time:  self |  cumulative | name"
    self_part, cumulative_us, name = line.removeprefix("import time:").split("|")
    rows.append((int(cumulative_us), int(self_part), name.strip()))
for cumulative_us, self_us, name in sorted(rows, reverse=True)[:6]:
    print(f"{cumulative_us:>8} µs cumulative {self_us:>7} µs self  {name}")
print("modules imported:", len(rows))
79172 µs cumulative    2419 µs self  json
   73382 µs cumulative    3022 µs self  json.decoder
   68119 µs cumulative    2889 µs self  re
   46211 µs cumulative   15867 µs self  enum
   27355 µs cumulative    7798 µs self  functools
   19498 µs cumulative   12540 µs self  collections
modules imported: 54
const { spawnSync } = require("node:child_process");

const child = (code) => spawnSync(process.execPath, ["-e", code], { encoding: "utf8" });
let r = child(`process.on("exit", (c) => console.log("exit handler, code", c)); console.log("normal end");`);
console.log(r.stdout.trim(), "| status", r.status);
r = child(`process.on("exit", (c) => console.log("exit handler, code", c)); throw new Error("boom");`);
console.log(r.stdout.trim(), "| status", r.status, "| stderr starts:", r.stderr.split("\n").find((l) => l.startsWith("Error")));
r = child(`process.exitCode = 3; console.log("set exitCode 3")`);
console.log(r.stdout.trim(), "| status", r.status);
normal end
exit handler, code 0 | status 0
exit handler, code 1 | status 1 | stderr starts: Error: boom
set exitCode 3 | status 3

Line-by-Line Explanation

Java: Config's static initialiser and NAME's initialiser run on the first access to Config.NAME, once; Unused is never initialised because it is never actively used; the shutdown hook runs after main returns.

Python: -X importtime writes one line per imported module to stderr; the parent sorts by cumulative time. Numbers depend on the machine, disk cache and version.

Node: normal exit runs the exit handler with code 0; an uncaught exception prints the error, runs the handler with code 1 and exits with status 1; process.exitCode sets the status without throwing.

Expected Output

Java and Node outputs are deterministic; Python timings vary.

Step-by-Step Runtime Behavior

JVM: main runs → getstatic Config.NAME → class not initialised → lock class → run <clinit> (static block, then NAME initialiser) → mark initialised → return value.

Common Misconceptions

"All static initialisers run at start-up." JVM and CLR initialise lazily on first use.

"Exit handlers always run." Not on hard kills or crashes.

"Start-up time is the JIT's fault." Loading, linking, initialisation and imports often dominate.

Common Bugs and Failure Modes

Circular static initialisation (JVM can observe default values), slow imports at module top level, configuration read before it is set, missing runtime versions, relying on finalisers at exit, containers killing processes before shutdown hooks finish.

Corrected or Safer Patterns

Keep static initialisers simple and acyclic, import lazily inside functions when start-up matters, pin runtime versions, implement graceful shutdown with timeouts.

Debugging and Inspection Techniques

-Xlog:class+load, -verbose:class (JVM), DOTNET_ tracing and dotnet-trace, python -X importtime, node --cpu-prof at start-up.

Performance Considerations

Class-data sharing (CDS/AppCDS), ReadyToRun, .pyc caching, V8 snapshots and code caches cut start-up; AOT removes JIT warm-up.

Security Considerations

Loading code from writable directories, PYTHONPATH/NODE_OPTIONS injection, DLL search-order hijacking and untrusted plugins are loader-level risks; restrict search paths and verify packages.

Portability and Version Caveats

Class initialisation rules are specified (JLS §12.4); import timing, CDS and loader details are implementation-specific.

Real-World Use Cases

Serverless cold-start optimisation, CLI tool responsiveness, plugin hosts, graceful shutdown in Kubernetes.

Best Practices

Measure start-up, keep initialisation lazy and idempotent, handle shutdown signals, pin and document runtime versions.

Exercises

E1 · Beginner. Why is "Unused.<clinit> runs" never printed?

Answer: The JVM initialises a class only on first active use (instantiation, static method call, non-constant static field access); Unused is never used.

E2 · Intermediate. Why does Python register a module in sys.modules before executing it?

Answer: So recursive and circular imports find the (partially initialised) module instead of importing it again forever.

E3 · Advanced challenge. List three changes that reduce a Python serverless function's cold start.

Answer: Import heavy libraries lazily inside handlers that need them; trim dependencies and avoid work at module top level (no network calls); ship precompiled .pyc files for the exact runtime version (and use provider snapshotting if available).

Quiz

? When does the JVM run a class's static initialiser?
+ On first active use of the class, exactly once. :: Lazy and thread-safe.
- When the JVM starts. :: Only classes needed for start-up are initialised then.
- Every time a static field is read. :: Once only.
- When the class is garbage collected. :: No.

? What exit status does Node.js use after an uncaught exception?
+ 1 :: As the real run shows.
- 0 :: That signals success.
- The exception's line number. :: No.
- It depends on the exit handler's return value. :: Handlers cannot change it by returning.

? Which reduce start-up time? (select all that apply)
+ Caching compiled code (.pyc, CDS, code caches) :: Skips front-end work.
+ AOT compilation :: Removes JIT warm-up.
- Doing network calls at module top level :: Adds latency.
+ Lazy imports and initialisation :: Defers unneeded work.

Summary and Key Takeaways

A runtime launches, bootstraps, loads, validates, links and initialises code before the entry point runs, and runs hooks and returns an exit code at the end. Initialisation is often lazy; start-up cost is dominated by loading and initialisation; shutdown handlers are best-effort.

Further Reading

*The Java Virtual Machine Specification*, ch. 5 "Loading, Linking, and Initializing".

Python Language Reference, §5 "The import system"..NET documentation, "Assembly loading"; Node.js documentation, "Modules: ECMAScript modules".