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:
-
Open your GC Log in GCLA using
File > Open File…or by dragging and dropping your GC log into the GCLA window.
-
Add your ReadyNow profile to GCLA using
File > Add File…. It may take some time to process the ReadyNow profile.
-
In the list of charts, select 17.1 All Class Events in the Class Events category.
-
In the toolbar at the top of the GCLA window, select "View Log" to switch to profile content.
-
Press
Ctrl + F(orCommand + Fon Mac) to open the search bar. PutCLASS_INITinto the search field and hit "Apply filter."
-
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).
-
From here, your analysis can begin. Here are some tips to help you conduct your analysis:
-
Every
CLASS_INITevent 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.
-
-
Look for enclosures:
-
In our example, you can see
FintechApp$AuditLogger"CLASS_INIT/CLASS_INIT_END" encloses theCLASS_INIT/CLASS_INIT_ENDof the classFintechApp$RiskEngine. -
Checking the source code, we know the
AuditLoggerclass static block callsRiskEngine, which causes theRiskEngineclass to be initialized alongside the initialization of theAuditLoggerclass (as per the Java specification). -
Such a closure tells us that only
FintechApp$AuditLoggerneeds to be addressed withClass.forNameor put to the force-init list, andRiskEngineclass can be ignored. -
From this analysis, we can conclude that only
FintechApp$AuditLoggerandFintechApp$EndOfDaySettlementneed to be addressed in this example.
-
-
As a final note, do not put or address generated classes. Classes with
/0xsuffix,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:
1if the class has a<clinit>method,0otherwise. -
has_default_methods:
1if the class implements interfaces with default methods,0otherwise. -
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:
-
Compiles and runs our demo application in order to generate a profile.
-
Runs the script
detect_post_deadline_classinit.sh(see Example 2: detect_post_deadline_classinit.sh) to find post-load-startClassInitevents. -
Extracts application classes into
force_init_classes.txt. -
Re-runs the demo app with
ProfileForceClassInitializationDelaySec
ProfileForceClassInitializationListPathto force early initialization. -
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 - nopost-load-startClassInit events within the window -
Exit
1: listspost-load-startclasses 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 ===");
}
}