Visit Azul.com Support

Analyzing Class Initialization for ReadyNow Profiles

Class initialization affects application startup time and can contribute to runtime performance overhead. Many classes can be preloaded unnecessarily or by mistake, affecting overall system performance. Fortunately, Azul Zing records ReadyNow class initialization data in GC logs, which you can analyze using Azul GC Log Analyzer (GCLA) or address using scripts.

This page shows how you can analyze class initialization manually using GCLA and also in a scripted, autonomous way.

Note
The examples here are based on a demo application, FintechApp, which is referenced throughout the guide.

Preparation

The information provided in this guide is applicable to Zing versions 25.08.x.x with GCLA 25.08.0 and later.

Before we begin, ensure that you have the latest version of GC Log Analyzer, which you can download for free on the GCLA product page.

Manually Analyzing Class Initialization

Use the following steps to view class initialization data in GCLA:

  1. Open your GC Log in GCLA using File > Open File…​ or by dragging and dropping your GC log into the GCLA window.

    gcla open file
  2. Add your ReadyNow profile to GCLA using File > Add File…​. It may take some time to process the ReadyNow profile.

    gcla add file
  3. In the list of charts, select 17.1 All Class Events in the Class Events category.

    gcla class events
  4. In the toolbar at the top of the GCLA window, select "View Log" to switch to profile content.

    gcla view log
  5. Press Ctrl + F (or Command + F on Mac) to open the search bar. Put CLASS_INIT into the search field and hit "Apply filter."

    gcla search classinit
  6. The log view should now show only "CLASS_INIT" and "CLASS_INIT_END" entries. Locate the timestamp of the load start (10s in our demo application).

  7. From here, your analysis can begin. Here are some tips to help you conduct your analysis:

    • Every CLASS_INIT event happening after load start is a potential class to be addressed.

    • JDK shutdown logic may also have classes initialized after load start but those are not relevant and do not have to be addressed.

    • In real applications, such late unrelated classes may also come from background jobs, reporting, etc.

    • Generally, it makes sense to address classes only within a reasonable time window after load start, as the late class initialization might not be relevant to the first transaction’s latency.

      gcla classinit result
  8. Look for enclosures:

    • In our example, you can see FintechApp$AuditLogger "CLASS_INIT/CLASS_INIT_END" encloses the CLASS_INIT/CLASS_INIT_END of the class FintechApp$RiskEngine.

    • Checking the source code, we know the AuditLogger class static block calls RiskEngine, which causes the RiskEngine class to be initialized alongside the initialization of the AuditLogger class (as per the Java specification).

    • Such a closure tells us that only FintechApp$AuditLogger needs to be addressed with Class.forName or put to the force-init list, and RiskEngine class can be ignored.

    • From this analysis, we can conclude that only FintechApp$AuditLogger and FintechApp$EndOfDaySettlement need to be addressed in this example.

  9. As a final note, do not put or address generated classes. Classes with /0x suffix, com.sun.proxy.$Proxy, reflectors, Lambda Proxies, LambdaForms, and custom code-generated classes are typically impossible to preload.

    Such classes typically need to be addressed by a code change to avoid the use of code patterns which could generate them.

Scripting the Analysis Process

Manually checking for GC log entries to address problems is certainly useful, but it’s also time-consuming and costly. Scripting the process helps us extract the data and turn it into usable and actionable information.

We have developed a demo application, FintechApp, and some example scripts for autonomously obtaining class initialization data, analyzing it, and applying a fix for bad class initialization. You can find the example scripts at the end of this section.

Profile Event Format

In a ProfileLogOut dump, class initialization is recorded as event pairs:

 
<timestamp> | ClassInit <class_id> <had_initializer> <has_default_methods> <tid> <timestamp> | ClassInitEnd <class_id> <tid>
  • had_initializer: 1 if the class has a <clinit> method, 0 otherwise.

  • has_default_methods: 1 if the class implements interfaces with default methods, 0 otherwise.

  • tid: thread ID that performed the initialization.

Timestamps are in nanoseconds, relative from VM start:

 
delta_ms = (event_timestamp - VmStart_timestamp) / 1_000_000

FintechApp Demo

"FintechApp" is a simple demo application which we built for this guide.

You can view the source code for FintechApp in Example 3: FinTechApp.java

The demo application has two eager classes, MarketDataFeed and OrderRouter, which are initialized at startup. The demo app has two lazy classes, AuditLogger and EndOfDaySettlement, initialized after load start. The app sleeps until load start, triggers lazy class initialization and then stays alive for a time so that the shutdown ClassInit events fall outside the detection window.

Let’s break down what our scripts are doing:

Scripting the Detect, Fix, Verify Flow

To achieve autonomous analysis of class initialization, we need scripts which can detect problems, fix them, and finally verify that the problems no longer persist.

Running the example script, run_demo.sh (see Example 1: run_demo.sh), does the following:

  1. Compiles and runs our demo application in order to generate a profile.

  2. Runs the script detect_post_deadline_classinit.sh (see Example 2: detect_post_deadline_classinit.sh) to find post-load-start ClassInit events.

  3. Extracts application classes into force_init_classes.txt.

  4. Re-runs the demo app with ProfileForceClassInitializationDelaySec
    ProfileForceClassInitializationListPath to force early initialization.

  5. Verifies the fix - no application classes remain past load start.

 
export JAVA_HOME=/path/to/zing25.08.x.x ./run_demo.sh

Step-by-step

1. Build and Generate your Profile

 
JAVA_HOME=/path/to/zing25.08.x.x $JAVA_HOME/bin/javac src/<FintechApp.java> $JAVA_HOME/bin/java \ -XX:ProfileLogOut=fintech_demo \ -cp src \ FintechApp 5 10

FintechApp arguments: - load_start_seconds (default 5) — How long to sleep, in seconds, before triggering lazy classes. - run_after_seconds (default 10) — How long to stay alive after load start, to ensure shutdown ClassInit events fall outside the detection window

2. Detect post-load-start ClassInit events

 
./detect_post_deadline_classinit.sh fintech_demo 10 10

Arguments: <profile> <load_start_seconds> [window_seconds] - window_seconds (default 10) — only report ClassInit events within this many seconds of load start. Events beyond this window (e.g. shutdown) are ignored.

  • Exit 0: clean - no post-load-start ClassInit events within the window

  • Exit 1: lists post-load-start classes with timestamps

Expected output:

 
RESULT: ANALYSIS NEEDED Post-load-start classes: - FintechApp$AuditLogger (+5023ms) - FintechApp$EndOfDaySettlement (+5024ms) ... (plus JVM-internal classes triggered by them)

3. Create force-init list

Create a text file listing application classes to force-initialize, one per line:

Example output to text file:

 
FintechApp$AuditLogger FintechApp$EndOfDaySettlement

4. Verify with ProfileForceClassInitialization

Re-run with the profile loaded via ProfileLogIn and force-init flags (requires -XX:+UnlockExperimentalVMOptions):

 
$JAVA_HOME/bin/java \ -XX:+UnlockExperimentalVMOptions \ -XX:ProfileLogIn=fintech_demo \ -XX:ProfileLogOut=fintech_demo_fixed \ -XX:ProfileForceClassInitializationDelaySec=2 \ -XX:ProfileForceClassInitializationListPath=force_init_classes.txt \ -cp src \ FintechApp 5 10
  • ProfileForceClassInitializationDelaySec — seconds after VM start to force-initialize the listed classes (JVM flag only, not an app parameter). Must be less than load start.

  • ProfileForceClassInitializationListPath — path to the class list file.

Confirm the fix:

 
./detect_post_deadline_classinit.sh fintech_demo_fixed 10 10

The application classes (AuditLogger, EndOfDaySettlement) should no longer appear in the post-load-start list.

Example 1: run_demo.sh

 
#!/bin/bash # # run_demo.sh — End-to-end ClassInit closure analysis demo # # Builds the app, generates a profile, detects post-load-start ClassInit events, # creates a force-init list, re-runs with force init, and verifies the fix. # # Requires JAVA_HOME to point to a Zing JDK. # # Usage: # ./run_demo.sh # set -euo pipefail if [ -z "${JAVA_HOME:-}" ]; then echo "ERROR: JAVA_HOME is not set. Point it to a Zing 25.08 or later." echo "Example: export JAVA_HOME=/path/to/zing25.08.x-jdk21" exit 2 fi JAVAC="$JAVA_HOME/bin/javac" JAVA="$JAVA_HOME/bin/java" if [ ! -x "$JAVA" ]; then echo "ERROR: $JAVA not found or not executable." exit 2 fi LOAD_START=10 WINDOW=10 FORCE_INIT_DELAY=2 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROFILE_BEFORE="$SCRIPT_DIR/demo_before" PROFILE_AFTER="$SCRIPT_DIR/demo_after" FORCE_INIT_LIST="$SCRIPT_DIR/force_init_classes.txt" echo "============================================" echo " ClassInit Analysis — End-to-End Demo" echo "============================================" echo "JAVA_HOME: $JAVA_HOME" echo "Load start: ${LOAD_START}s" echo "Window: ${WINDOW}s after load start (ignore shutdown ClassInit)" echo "Force-init delay: ${FORCE_INIT_DELAY}s (JVM flag, must be < load start)" echo "" # ── Step 1: Build ───────────────────────────────────────────────── echo ">>> Step 1: Compiling FintechApp..." "$JAVAC" "$SCRIPT_DIR/src/FintechApp.java" echo " Done." echo "" # ── Step 2: Generate profile (ProfileLogOut) ────────────────────── echo ">>> Step 2: Running app to generate profile..." "$JAVA" \ -XX:ProfileLogOut=demo_before \ -cp "$SCRIPT_DIR/src" \ FintechApp "$LOAD_START" "$WINDOW" echo "" # ── Step 3: Detect post-load-start ClassInit events ────────────── echo ">>> Step 3: Detecting post-load-start ClassInit events..." echo "" if "$SCRIPT_DIR/detect_post_deadline_classinit.sh" "$PROFILE_BEFORE" "$LOAD_START" "$WINDOW"; then echo "" echo "No post-load-start ClassInit events. Nothing to fix." exit 0 fi echo "" # ── Step 4: Extract application classes into force-init list ────── echo ">>> Step 4: Building force-init class list..." VMSTART=$(grep '| VmStart' "$PROFILE_BEFORE" | head -1 | awk -F'|' '{print $1}' | tr -d ' ') LOAD_START_NS=$((LOAD_START * 1000000000)) LOAD_START_TS=$((VMSTART + LOAD_START_NS)) WINDOW_END_TS=$((LOAD_START_TS + WINDOW * 1000000000)) declare -A CLASS_NAMES while IFS= read -r line; do cid=$(echo "$line" | awk '{print $2}') cname=$(echo "$line" | awk '{print $4}') CLASS_NAMES[$cid]="$cname" done < <(grep '^Class ' "$PROFILE_BEFORE") > "$FORCE_INIT_LIST" while IFS= read -r line; do ts=$(echo "$line" | awk -F'|' '{print $1}' | tr -d ' ') classid=$(echo "$line" | awk -F'|' '{print $2}' | awk '{print $2}') if [ "$ts" -gt "$LOAD_START_TS" ] 2>/dev/null && [ "$ts" -le "$WINDOW_END_TS" ] 2>/dev/null; then cname="${CLASS_NAMES[$classid]:-}" if [[ -n "$cname" && "$cname" != *'$$Lambda$'* && "$cname" != *'/0x'* ]]; then echo "$cname" >> "$FORCE_INIT_LIST" fi fi done < <(grep '| ClassInit ' "$PROFILE_BEFORE") # Deduplicate sort -u -o "$FORCE_INIT_LIST" "$FORCE_INIT_LIST" echo " Force-init list ($FORCE_INIT_LIST):" while IFS= read -r cls; do echo " $cls" done < "$FORCE_INIT_LIST" echo "" # ── Step 5: Re-run with force init (ProfileLogIn + force flags) ─── echo ">>> Step 5: Re-running with ProfileForceClassInitialization..." "$JAVA" \ -XX:+UnlockExperimentalVMOptions \ -XX:ProfileLogIn=demo_before \ -XX:ProfileLogOut=demo_after \ -XX:ProfileForceClassInitializationDelaySec="$FORCE_INIT_DELAY" \ -XX:ProfileForceClassInitializationListPath="$FORCE_INIT_LIST" \ -cp "$SCRIPT_DIR/src" \ FintechApp "$LOAD_START" "$WINDOW" echo "" # ── Step 6: Verify fix ─────────────────────────────────────────── echo ">>> Step 6: Verifying fix..." echo "" VMSTART_AFTER=$(grep '| VmStart' "$PROFILE_AFTER" | head -1 | awk -F'|' '{print $1}' | tr -d ' ') LOAD_START_TS_AFTER=$((VMSTART_AFTER + LOAD_START_NS)) WINDOW_END_TS_AFTER=$((LOAD_START_TS_AFTER + WINDOW * 1000000000)) declare -A CLASS_NAMES_AFTER while IFS= read -r line; do cid=$(echo "$line" | awk '{print $2}') cname=$(echo "$line" | awk '{print $4}') CLASS_NAMES_AFTER[$cid]="$cname" done < <(grep '^Class ' "$PROFILE_AFTER") APP_CLASSES_REMAINING=0 while IFS= read -r line; do ts=$(echo "$line" | awk -F'|' '{print $1}' | tr -d ' ') classid=$(echo "$line" | awk -F'|' '{print $2}' | awk '{print $2}') if [ "$ts" -gt "$LOAD_START_TS_AFTER" ] 2>/dev/null && [ "$ts" -le "$WINDOW_END_TS_AFTER" ] 2>/dev/null; then cname="${CLASS_NAMES_AFTER[$classid]:-}" if [[ -n "$cname" && "$cname" != *'$$Lambda$'* && "$cname" != *'/0x'* ]]; then APP_CLASSES_REMAINING=$((APP_CLASSES_REMAINING + 1)) echo " STILL LATE: $cname (+$(( (ts - VMSTART_AFTER) / 1000000 ))ms)" fi fi done < <(grep '| ClassInit ' "$PROFILE_AFTER") echo "" if [ "$APP_CLASSES_REMAINING" -eq 0 ]; then echo "RESULT: FIXED" echo "All application classes are now initialized before the ${LOAD_START}s load start." echo "Remaining post-load-start events (if any) are JVM-internal classes." else echo "RESULT: $APP_CLASSES_REMAINING application class(es) still past load start." echo "Review the force-init list or increase ProfileForceClassInitializationDelaySec." fi

Example 2: detect_post_deadline_classinit.sh

 
#!/bin/bash # # detect_post_deadline_classinit.sh # # Scans a ReadyNow ProfileLogOut dump file for ClassInit/ClassInitEnd events # that occur AFTER load start. # # Any post-load-start ClassInit events indicate classes whose initialization # is deferred beyond load start, blocking ReadyNow compilation replay. # # Usage: # ./detect_post_deadline_classinit.sh <profile_output_file> [load_start_seconds] [window_seconds] # # profile_output_file - Path to the ProfileLogDumpOutputToFile output # load_start_seconds - Load-start boundary in seconds (default: 30) # window_seconds - How long after load start to consider (default: 10). # ClassInit events after load_start + window are ignored # (e.g. shutdown-only classes). # # Exit codes: # 0 - No post-load-start ClassInit events found (clean) # 1 - Post-load-start ClassInit events detected (analysis needed) # 2 - Usage error or file not found # set -euo pipefail if [ $# -lt 1 ]; then echo "Usage: $0 <profile_output_file> [load_start_seconds] [window_seconds]" exit 2 fi PROFILE="$1" if [ ! -f "$PROFILE" ]; then echo "ERROR: File not found: $PROFILE" exit 2 fi # --- Verify ProfileLogOut format version --- # This script was written against ZingProfile version 64. If the profile # format version has been bumped, the layout of Class/ClassInit/VmStart # records may have changed and this script's parsing could be silently wrong. EXPECTED_PROFILE_VERSION=64 PROFILE_VERSION=$(grep -m1 '^ZingProfile ' "$PROFILE" | awk '{print $2}') if [ -z "$PROFILE_VERSION" ]; then echo "WARNING: No 'ZingProfile <version>' header found in $PROFILE." echo " Cannot confirm profile format; results may be unreliable." echo "" elif [ "$PROFILE_VERSION" != "$EXPECTED_PROFILE_VERSION" ]; then echo "==========================================" echo " WARNING: ProfileLogOut format version mismatch" echo "==========================================" echo " Expected: ZingProfile $EXPECTED_PROFILE_VERSION" echo " Found: ZingProfile $PROFILE_VERSION" echo "" echo " MANUAL ACTION REQUIRED: verify that the profile record layout" echo " (VmStart / Class / ClassInit lines) has not changed in a way" echo " that would invalidate this script's parsing. If the format is" echo " still compatible, update EXPECTED_PROFILE_VERSION to $PROFILE_VERSION." echo "==========================================" echo "" fi # --- Extract VmStart timestamp (nanoseconds) --- VMSTART=$(grep '| VmStart' "$PROFILE" | head -1 | awk -F'|' '{print $1}' | tr -d ' ') if [ -z "$VMSTART" ]; then echo "ERROR: No VmStart event found in profile" exit 2 fi # --- Determine load start --- if [ $# -ge 2 ]; then LOAD_START_SEC="$2" else LOAD_START_SEC=30 echo "NOTE: No load start specified; using default ${LOAD_START_SEC}s" fi if [ $# -ge 3 ]; then WINDOW_SEC="$3" else WINDOW_SEC=10 fi LOAD_START_NS=$((LOAD_START_SEC * 1000000000)) LOAD_START_TS=$((VMSTART + LOAD_START_NS)) WINDOW_END_TS=$((LOAD_START_TS + WINDOW_SEC * 1000000000)) echo "==========================================" echo " ReadyNow ClassInit Load-Start Check" echo "==========================================" echo "Profile: $PROFILE" echo "VmStart: $VMSTART" echo "Load start: +${LOAD_START_SEC}s (ts=$LOAD_START_TS)" echo "Window: +${WINDOW_SEC}s after load start (ts=$WINDOW_END_TS)" echo "" # --- Build class ID -> name map --- declare -A CLASS_NAMES while IFS= read -r line; do cid=$(echo "$line" | awk '{print $2}') cname=$(echo "$line" | awk '{print $4}') CLASS_NAMES[$cid]="$cname" done < <(grep '^Class ' "$PROFILE") # --- Scan ClassInit events for post-load-start occurrences --- POST_DEADLINE_COUNT=0 POST_DEADLINE_CLASSES=() while IFS= read -r line; do ts=$(echo "$line" | awk -F'|' '{print $1}' | tr -d ' ') classid=$(echo "$line" | awk -F'|' '{print $2}' | awk '{print $2}') if [ "$ts" -gt "$LOAD_START_TS" ] 2>/dev/null && [ "$ts" -le "$WINDOW_END_TS" ] 2>/dev/null; then cname="${CLASS_NAMES[$classid]:-unknown(id=$classid)}" delta_ns=$((ts - VMSTART)) delta_ms=$((delta_ns / 1000000)) POST_DEADLINE_CLASSES+=("$cname (+${delta_ms}ms)") POST_DEADLINE_COUNT=$((POST_DEADLINE_COUNT + 1)) fi done < <(grep '| ClassInit ' "$PROFILE") # --- Report results --- TOTAL_CLASSINIT=$(grep -c '| ClassInit ' "$PROFILE") echo "Total ClassInit events: $TOTAL_CLASSINIT" echo "Post-load-start ClassInit events: $POST_DEADLINE_COUNT" echo "" if [ "$POST_DEADLINE_COUNT" -eq 0 ]; then echo "RESULT: CLEAN" echo "No ClassInit events after the ${LOAD_START_SEC}s load start." echo "ReadyNow compilation replay is not blocked by late class initialization." exit 0 else echo "RESULT: ANALYSIS NEEDED" echo "" echo "The following $POST_DEADLINE_COUNT classes were initialized AFTER the" echo "${LOAD_START_SEC}s load start. Any ReadyNow compilation that depends on these" echo "classes (via load/link/hierarchy closures) cannot be replayed until" echo "these classes are initialized, degrading warmup performance." echo "" echo "Post-load-start classes:" for entry in "${POST_DEADLINE_CLASSES[@]}"; do echo " - $entry" done exit 1 fi

Example 3: FintechApp.java

 
import java.util.Random; /** * Mock fintech app demonstrating eager vs lazy class initialization. * * Uses arrays and primitives instead of Collections/ConcurrentHashMap * to minimize JDK classes loaded at startup. * * Eager classes (MarketDataFeed, OrderRouter) initialize at startup. * Lazy classes (AuditLogger, EndOfDaySettlement) initialize after load start, * producing post-load-start ClassInit events in the ReadyNow profile. * */ public class FintechApp { static final Random random = new Random(42); static class MarketDataFeed { static final String[] symbols = {"AAPL", "GOOGL", "MSFT"}; static final double[] prices = {187.50, 141.80, 378.25}; static double getPrice(String symbol) { for (int i = 0; i < symbols.length; i++) { if (symbols[i].equals(symbol)) return prices[i]; } return 0.0; } static void tick() { for (int i = 0; i < prices.length; i++) { prices[i] *= 1 + (random.nextDouble() - 0.5) * 0.001; } } } static class OrderRouter { static final String[] venues = {"NYSE", "NASDAQ", "BATS"}; static String route(String symbol, double qty, double price) { String venue = venues[random.nextInt(venues.length)]; return "ORDER " + symbol + " " + (int) qty + "@" + price + " -> " + venue; } } static class RiskEngine { static final double MAX_POSITION = 1_000_000.0; static final double[] positionLimits = new double[MarketDataFeed.symbols.length]; static { for (int i = 0; i < positionLimits.length; i++) { positionLimits[i] = MAX_POSITION * (1.0 + random.nextDouble() * 0.5); } } static boolean riskEnabled() { return MAX_POSITION > 0; } static boolean checkLimit(String symbol, double qty, double price) { double notional = qty * price; for (int i = 0; i < MarketDataFeed.symbols.length; i++) { if (MarketDataFeed.symbols[i].equals(symbol)) { return notional <= positionLimits[i]; } } return false; } } static class AuditLogger { static int tradeCount; static int cancelCount; static final boolean riskEnabled; static { riskEnabled = RiskEngine.riskEnabled(); } static void logTrade(String detail) { tradeCount++; } } static class EndOfDaySettlement { static final double[] settledPrices = new double[MarketDataFeed.symbols.length]; static void settle(double[] prices) { System.arraycopy(prices, 0, settledPrices, 0, prices.length); } } public static void main(String[] args) throws Exception { int loadStartSeconds = 5; int runAfterSeconds = 10; if (args.length > 0) loadStartSeconds = Integer.parseInt(args[0]); if (args.length > 1) runAfterSeconds = Integer.parseInt(args[1]); System.out.println("=== FintechApp starting ==="); System.out.println("Load start: " + loadStartSeconds + "s, run after: " + runAfterSeconds + "s"); System.out.println(); System.out.println("--- Startup initialization (before load start) ---"); MarketDataFeed.tick(); OrderRouter.route("AAPL", 100, MarketDataFeed.getPrice("AAPL")); System.out.println(); System.out.println("--- Sleeping " + loadStartSeconds + "s until load start ---"); Thread.sleep(loadStartSeconds * 1000L); System.out.println(); System.out.println("--- Post-load-start lazy initialization ---"); AuditLogger.logTrade("AAPL [email protected]"); boolean allowed = RiskEngine.checkLimit("AAPL", 100, MarketDataFeed.getPrice("AAPL")); System.out.println(" >> Risk check for AAPL: " + (allowed ? "PASSED" : "BLOCKED")); EndOfDaySettlement.settle(MarketDataFeed.prices); System.out.println("--- Running " + runAfterSeconds + "s after load start ---"); Thread.sleep(runAfterSeconds * 1000L); System.out.println(); System.out.println("=== FintechApp complete ==="); } }