Jvm

6 posts

datadog3 min readCurated summary

How we improved APM Java startup by encoding a prefix trie as a JVM constant

Startup performance is critical for users, developers, and cloud costs, but Java APM instrumentation must balance observability against the overhead of transforming classes. Datadog reduced class-matching overhead by 30% over four years by optimizing the first filtering stage: matching class-name prefixes. Its main innovation was encoding a prefix trie as a single JVM string constant, avoiding the startup cost of constructing a conventional trie. ## Java Instrumentation and Class Matching - Java APM uses the Java Instrumentation API to intercept and transform classes as they load. - Instrumentation adds method advice that records method execution and propagates tracing context. - Instrumenting every method would be too expensive, so APM first identifies valuable classes. - Applications may load tens or hundreds of thousands of classes, making efficient filtering important. - Class-name and package-prefix checks are cheaper than structural or hierarchy-based checks because they avoid parsing class files. - Datadog therefore begins with a curated ignore list of class and package prefixes. ## Startup Constraints in `premain` - Agents register transformers in the JVM’s `premain` phase, before the application’s `main` method. - At this point: - Few classes have been loaded. - The JIT compiler is cold or unavailable, especially on Java 8. - Code runs interpreted and unoptimized. - Loading or calling certain JDK classes can have irreversible side effects. - For example, touching `java.util.logging` initializes `LogManager`, potentially preventing an application from configuring its own logging manager later. - These constraints make ordinary data loading, parsing, and object construction undesirable during startup. ## Replacing a Hand-Written Matcher with a Trie - Datadog’s earlier matcher used a complex nested code structure to represent prefixes. - Although flexible, it was difficult to maintain and required special optimizations for Java 8 startup. - A trie was a natural replacement because it shares common characters among prefixes and supports efficient lookup. - A conventional trie would require: - Locating and reading a resource. - Parsing its contents. - Constructing trie nodes. - Loading additional code or dependencies. - Those operations would impose unacceptable costs during `premain`. ## Encoding the Trie as a JVM Constant - Datadog created `ClassNameTrie`, which stores the entire prefix trie in a Java string constant. - The JVM loads the encoded data with a single `ldc` bytecode instruction. - This approach avoids resource I/O and runtime trie construction. - Embedding the data in the class also makes it resilient to repackaging. - The compact representation improves cache locality and reduces startup work. ## Compact Node Representation - Java strings contain 16-bit `char` values, allowing each character to encode one of 65,536 possible values. - Each trie node stores: - A character indicating the number of branches. - Sorted branch characters, enabling binary search. - One value character per branch. - Value characters encode different outcomes: - **Leaf:** returns a definitive result and ends the search. - **Bud:** records a possible result but permits further matching. - **Inline segment length:** indicates that additional prefix characters are stored directly. - Buds and leaves can include a **glob bit**, allowing a match to apply even when extra characters remain in the class name. - The encoding reserves the remaining value range for match results, with a maximum stored value of 8,191. The broader lesson is that startup-sensitive JVM code may benefit from moving computation into class-loading time and representing lookup structures in compact constants. For Java agents, precomputed, dependency-free data structures can deliver trie-like performance without the initialization and JIT costs of building them at runtime.

Read original(opens in new tab)
datadog3 min readCurated summary

Unbiased Java CPU profiling with JFR in JDK 25

Java Flight Recorder (JFR) provides low-overhead, production-safe diagnostics, but its `ExecutionSample` event can produce biased CPU profiles because it samples JVM-observed runnable threads rather than strictly measuring CPU time. For CPU-bound workloads—particularly reactive applications—this may obscure the real hotspots. Modern profilers therefore combine JFR with JVMTI, `SIGPROF`, `AsyncGetCallTrace`, and JVM-internal techniques, while the Java ecosystem works toward a supported CPU-sampling mechanism. ## How Sampling Profilers Work - Continuous profilers repeatedly capture stack traces and aggregate them to reveal recurring behavior. - CPU profilers commonly sample at fixed intervals, such as every 20 milliseconds. - **CPU-time sampling** highlights code actively consuming processor cycles. - **Wall-clock sampling** reveals latency sources, including I/O waits, lock contention, and blocked threads. - Other profilers trigger on events such as allocations, garbage collection, thread parking, or lock contention. - Regardless of the trigger, profilers capture a stack, associate it with an event, and aggregate the results. ## Limitations of JFR’s `ExecutionSample` - JFR is integrated into the JVM and designed for low-overhead, always-on production use. - Its `ExecutionSample` event captures stacks from a rotating subset of runnable threads. - CPU-heavy threads tend to appear more often, but samples are not strictly proportional to actual CPU consumption. - This can lead to incomplete or biased results on CPU-saturated systems. - Reactive applications are a notable example: their scheduling behavior can cause thread CPU usage and hotspots to be underrepresented. ## CPU Sampling with `AsyncGetCallTrace` - JVMTI agents can use operating-system signals such as `SIGPROF` to sample threads according to CPU time. - The signal handler invokes HotSpot’s `AsyncGetCallTrace` to walk Java stacks asynchronously. - This approach avoids safepoint bias and can capture stacks during arbitrary execution states. - Tools such as `async-profiler` use this technique to produce profiles that more closely match actual CPU usage. - The drawback is that `AsyncGetCallTrace` is an unsupported internal JVM API. - Under heavy load, it can occasionally fault, requiring profilers to add extensive safeguards. - Datadog also uses **vmstructs walking**, which reads internal JVM metadata to recover stack and runtime information unavailable through standard APIs. ## The Safety–Accuracy Tradeoff - JFR offers stability, structured runtime telemetry, and low overhead. - `AsyncGetCallTrace` and vmstructs walking offer more accurate CPU sampling. - Relying on JVM internals creates maintenance and reliability risks because those interfaces are not officially stable. - Consequently, modern profilers combine JFR with unsupported sampling mechanisms rather than choosing only one approach. ## Toward a Supported CPU Profiling Event - Datadog, SAP, Amazon, and OpenJDK contributors recognized that this limitation affected the broader profiling ecosystem. - JFR was already the natural foundation for safe, continuous profiling. - The missing capability was a first-class CPU sampling event that could provide accurate CPU-based results without depending on unsupported JVM internals. - Datadog participated in OpenJDK discussions to explain why existing sampling was insufficient and to help improve the platform’s profiling foundation. Ultimately, accurate production CPU profiling requires both JFR’s safety and CPU-time-based sampling. A supported JFR CPU profiling event would remove the ecosystem’s dependence on fragile JVM internals while preserving the low-overhead behavior needed for continuous use.

Read original(opens in new tab)
netflix4 min readCurated summary

Scaling ArchUnit with Nebula ArchRules

Netflix’s Nebula ArchRules extends ArchUnit so architectural and API-lifecycle rules can be shared across thousands of Gradle repositories. Unlike AST-based tools, ArchUnit analyzes compiled JVM bytecode, supports multiple JVM languages, and offers a type-safe Java API for authoring and testing rules. The approach helps identify unsafe API usage, technical debt, and deviations from Netflix’s preferred development practices at fleet scale. ## The API Lifecycle Problem - Netflix operates tens of thousands of Java repositories in a polyrepo environment. - A library incident involving a backwards-incompatible change highlighted the difficulty of deciding when deprecated APIs can safely be removed. - Netflix introduced lifecycle annotations: - `@Deprecated` for APIs scheduled for removal - `@Public` for APIs intended for downstream use - `@Experimental` for APIs that may change - Unannotated APIs are treated as internal - The remaining challenge was identifying downstream projects that use internal, experimental, or deprecated APIs incorrectly. - The same tooling could support large migrations, such as major Spring Boot upgrades. ## Why ArchUnit - ArchUnit is an open-source library commonly used within JUnit suites to enforce architectural rules. - It is built on ASM and analyzes compiled JVM bytecode rather than source syntax. - Its main strengths are: - Cross-language JVM support for Java, Kotlin, Scala, and other JVM languages - A fluent builder API for readable rule definitions - A lower-level API for complex custom analysis - Access to class relationships, dependencies, and call sites through its class graph - Standard ArchUnit is primarily designed for one repository, so Netflix created Nebula ArchRules to distribute rules across many Gradle projects. ## Bytecode Analysis vs. AST Analysis - AST-based tools such as PMD inspect source-code structure and can be sensitive to language-specific syntax and syntactic sugar. - Supporting multiple JVM languages may require separate rules for each language. - ASM analyzes the bytecode that will actually execute, regardless of how the source was written. - This makes rules more consistent across Java, Kotlin, Scala, and other JVM languages. ## Rule Authoring - Tools such as PMD and SpotBugs are generally optimized for built-in rules or third-party plugins rather than custom rule development. - PMD custom rules may require difficult-to-maintain XPath expressions and separate tooling for testing. - ArchUnit rules are written as type-safe, fluent Java code. - Rules can be unit tested directly by passing them class references, without running a separate analysis process. - ArchUnit’s class graph provides contextual information about dependencies and call relationships, enabling more sophisticated checks. ## ArchRules Libraries - The Nebula ArchRules Library Plugin adds an `archRules` source set to a Gradle project. - A class implementing `ArchRulesService` exposes a `Map<String, ArchRule>`: - The map key names the rule. - The `ArchRule` defines the constraint using ArchUnit’s API. - Rule code and its dependencies are kept separate from the application’s main code. - Gradle publishes the rules in a separate JAR using the `arch-rules` classifier and an `arch-rules` usage attribute. - Downstream projects must use Gradle Module Metadata to resolve the rules variant. ## Standalone and Bundled Rule Libraries - Standalone rule libraries contain only `archRules` code. - They are useful for: - Enforcing rules around APIs the organization does not own - Checking usage of Java or open-source libraries - Applying generic rules, such as prohibiting use of deprecated APIs - Bundled rule libraries contain both normal library code and rules specific to how that library should be used. - Netflix maintains open-source standalone rule libraries as examples and reusable building blocks. Nebula ArchRules turns ArchUnit from a repository-local testing library into a reusable organization-wide policy mechanism. Teams can publish rules as Gradle artifacts and apply them consistently across JVM projects, making API governance, dependency policies, and architectural standards easier to enforce at scale.

Read original(opens in new tab)
tossOriginal article

Managing Thousands of API/ (opens in new tab)

Toss Payments manages thousands of API and batch server configurations that handle trillions of won in transactions, where a single typo in a JVM setting can lead to massive financial infrastructure failure. To solve the risks associated with manual "copy-paste" workflows and configuration duplication, the team developed a sophisticated system that treats configuration as code. By implementing layered architectures and dynamic templates, they created a testable, unified environment capable of managing complex hybrid cloud setups with minimal human error. ## Overlay Architecture for Hierarchical Control * The team implemented a layered configuration system consisting of `global`, `cluster`, `phase`, and `application` levels. * Settings are resolved by priority, where lower-level layers override higher-level defaults, allowing servers to inherit common settings while maintaining specific overrides. * This structure allows the team to control environment-specific behaviors, such as disabling canary deployments in development environments, from a single centralized directory. * The directory structure maps files 1:1 to their respective layers, ensuring that naming conventions drive the CI/CD application process. ## Solving Duplication with Template Patterns * Standard YAML overlays often fail when dealing with long strings or arrays, such as `JVM_OPTION`, because changing a single value usually requires redefining the entire block. * To prevent the proliferation of nearly identical environment variables, the team introduced a template pattern using placeholders like `{{MAX_HEAP}}`. * Developers can modify specific parameters at the application layer while the core string remains defined at the global layer, significantly reducing the risk of typos. * This approach ensures that critical settings, like G1GC parameters or heap region sizes, remain consistent across the infrastructure unless explicitly changed. ## Dynamic and Conditional Configuration Logic * The system allows for "evolutionary" configurations where Python scripts can be injected to generate dynamic values, such as random JMX ports or data fetched from remote APIs. * Advanced conditional logic was added to handle complex deployment scenarios, enabling environment variables to change their values automatically based on the target cluster name (e.g., different profiles for AWS vs. IDC). * By treating configuration as a living codebase, the team can adapt to new infrastructure requirements without abandoning their core architectural principles. ## Reliable Batch Processing through Simplicity * For batch operations handling massive settlement volumes, the team prioritized "appropriate technology" and simplicity to minimize failure points. * They chose Jenkins for its low learning curve and reliability, despite its lack of native GitOps support. * To address inconsistencies in manual UI entries and varying Java versions across machines, they standardized the batch infrastructure to ensure that high-stakes financial calculations are executed in a controlled, predictable environment. The most effective way to manage large-scale infrastructure is to transition from static, duplicated configuration files to a dynamic, code-centric system. By combining an overlay architecture for hierarchy and a template pattern for granular changes, organizations can achieve the flexibility needed for hybrid clouds while maintaining the strict safety standards required for financial systems.

naverOriginal article

Naver TV (opens in new tab)

JVM applications often suffer from initial latency spikes because the Just-In-Time (JIT) compiler requires a "warm-up" period to optimize frequently executed code into machine language. While traditional strategies rely on simulated API calls to trigger this optimization, these methods often introduce side effects like data pollution, log noise, and increased maintenance overhead. This new approach advocates for a library-centric warm-up that targets core execution paths and dependencies directly, ensuring high performance from the first real request without the risks of full-scale API simulation. ### Limitations of Traditional API-Based Warm-up * **Data and State Pollution:** Simulated API calls can inadvertently trigger database writes, send notifications, or pollute analytics data, requiring complex logic to bypass these side effects. * **Maintenance Burden:** As business logic and API signatures change, developers must constantly update the warm-up scripts or "dummy" requests to match the current application state. * **Operational Risk:** Relying on external dependencies or complex internal services during the warm-up phase can lead to deployment failures if the mock environment is not perfectly aligned with production. ### The Library-Centric Warm-up Strategy * **Targeted Optimization:** Instead of hitting the entry-point controllers, the focus shifts to warming up heavy third-party libraries and internal utility classes (e.g., JSON parsers, encryption modules, and DB drivers). * **Internal Execution Path:** By directly invoking methods within the application's service or infrastructure layer during the startup phase, the JIT compiler can reach "Tier 4" (C2) optimization for critical code blocks. * **Decoupled Logic:** Because the warm-up targets underlying libraries rather than specific business endpoints, the logic remains stable even when the high-level API changes. ### Implementation and Performance Verification * **Reflection and Hooks:** The implementation uses application startup hooks to execute intensive code paths, ensuring the JVM is "hot" before the load balancer begins directing traffic to the instance. * **JIT Compilation Monitoring:** Success is measured by tracking the number of JIT-compiled methods and the time taken to reach a stable state, specifically targeting the reduction of "cold" execution time. * **Latency Improvements:** Empirical data shows a significant reduction in P99 latency during the first few minutes of deployment, as the most CPU-intensive library functions are already pre-optimized. ### Advantages and Practical Constraints * **Safer Deployments:** Removing the need for simulated network requests makes the deployment process more robust and prevents accidental side effects in downstream systems. * **Granular Control:** Developers can selectively warm up only the most performance-sensitive parts of the application, saving startup time compared to a full-system simulation. * **Incomplete Path Coverage:** A primary limitation is that library-only warming may miss specific branch optimizations that occur only during full end-to-end request processing. To achieve the best balance between safety and performance, engineering teams should prioritize warming up shared infrastructure libraries and high-overhead utilities. While it may not cover 100% of the application's execution paths, a library-based approach provides a more maintainable and lower-risk foundation for JVM performance tuning than traditional request-based methods.

airbnb4 min readCurated summary

Migrating Airbnb’s JVM Monorepo to Bazel

Airbnb migrated its tens-of-millions-of-lines JVM monorepo from Gradle to Bazel over 4.5 years, achieving faster builds, testing, IntelliJ syncs, and development deployments. The move was driven by Bazel’s scalable remote execution, hermetic builds, and ability to provide shared infrastructure across Airbnb’s language-specific repositories. A gradual rollout, extensive automation, and close collaboration with service teams were central to making the migration successful. ## Results of the Migration - Build CSAT increased from 38% to 68%. - Local build and test times became 3–5 times faster. - IntelliJ syncs became 2–3 times faster. - Development-environment deployments became 2–3 times faster. ## Why Airbnb Chose Bazel ### Faster Builds Through Remote Execution - Large Gradle builds frequently took more than 20 minutes locally, while pre-merge CI builds had a p90 of 35 minutes. - Gradle had already been optimized with powerful machines and build sharding, but sharding caused underutilization and duplicated shared work. - Bazel’s cacheable actions and remote build execution enabled thousands of actions to run in parallel on short-lived workers. - “Build without the Bytes” reduced the amount of build output developers needed to download. - Bazel analysis runs in parallel, unlike the often single-threaded configuration phase of large Gradle projects. - Remote execution also improved local build performance, not just CI performance. ### More Reliable and Reproducible Builds - Gradle tasks could access the entire filesystem, creating accidental dependencies and race conditions. - Bazel sandboxes expose only declared inputs to each action, preventing undeclared files from affecting builds. - Bazel’s remote execution runs actions in identical containers with strict resource limits. - Using remote execution for both local and CI builds reduced differences between developer and CI environments. ### A Shared Build Infrastructure Layer Because Airbnb’s web, iOS, Python, Go, and JVM repositories all use Bazel, the company could standardize infrastructure for: - Remote caching - Remote build execution - Affected-target calculation - Build Event Protocol instrumentation and logging ## Starting with a Proof of Concept - Airbnb first migrated Viaduct, a large GraphQL monolith platform. - Viaduct was selected because it was complex, had slow builds, affected roughly 300 product engineers monthly, and had an infrastructure team willing to collaborate. - Bazel and Gradle initially coexisted, allowing developers to choose either system. - The team ported Viaduct’s build logic and created an automated Bazel build-file generator because the Gradle dependency graph continued to change. - Although Bazel was initially 2–4 times faster locally, developers did not adopt it immediately. - The team spent several additional months fixing missing integrations and bugs before Viaduct engineers voluntarily switched. ## Scaling Across the JVM Monorepo - Airbnb expanded breadth-first, aiming to make the entire repository compile and test under Bazel. - Gradle and Bazel continued to coexist during the migration. - This allowed developers to use Bazel locally while deployments still relied on Gradle. - Gradle provided a fallback when Bazel infrastructure, such as remote caching or execution, experienced incidents. - Maintaining two build graphs was costly, so Airbnb invested heavily in automation rather than requiring developers to maintain Bazel files manually. ## Automated Build-File Generation - The generator was inspired by Gazelle but was built internally to meet stricter performance requirements and handle dependency cycles. - It parses Java, Kotlin, and Scala source files to identify packages, imports, and symbol declarations. - These relationships are used to construct a file-level dependency graph. - Since generation ran on every commit before merging, Airbnb added external caching to keep it fast. - CI publishes a cached repository index for each mainline commit, allowing the generator to rescan only directories changed since that commit. Airbnb’s experience suggests that a large build-system migration is most effective when introduced incrementally: prove the benefits on a representative service, automate maintenance, preserve a fallback during rollout, and address developer workflow issues before expanding across the organization.

Read original(opens in new tab)