Type profiling and speculation in JVM

Ondrej Kvasnovsky
5 min readJan 18, 2025

--

Introduction

The Java Virtual Machine (JVM) constantly works to optimize your code’s performance while it runs. One of its most powerful optimization techniques is type profiling and speculation — a process where the JVM observes how your program uses different types of objects and makes educated guesses about future behavior.

Why it matters

In Java, many operations are dynamic by default:

  • Method calls can be virtual (decided at runtime)
  • Objects can be of any type that extends/implements the declared type
  • Type checks and casts happen frequently

All this flexibility comes at a performance cost. However, in real applications, code often follows predictable patterns:

  • A method might mostly receive String objects even though it accepts Object
  • An interface might primarily be implemented by one specific class
  • Some type checks might almost always succeed or fail

The JVM takes advantage of these patterns through type profiling and speculation. It:

  1. Monitors how types are used at key points in your code
  2. Makes predictions based on observed patterns
  3. Optimizes code based on these predictions
  4. Maintains fallback paths for when predictions are wrong

This optimizes the common case while preserving Java’s flexibility and type safety guarantees.

How JVM optimizes

Data Collection (Type Profiling)

The JVM collects data at key monitoring points in your code:

void process(Object obj) {
// Profiling point 1: Method entry
if (obj instanceof String) {
// Profiling point 2: Inside if block
((String)obj).length();
} else {
// Profiling point 3: Inside else block
obj.toString();
}
// Profiling point 4: Method exit
}

At each profiling point, the JVM records what types it sees. This helps it understand type flow through your program.

The JVM strategically places these profiling points at important locations:

  1. Method entries and exits
  2. Type checks (instanceof)
  3. Type casts
  4. Virtual method calls
  5. Inside exception handlers
  6. Loop entries and exits
  7. Branch points (if/else statements)

At each of these profiling points, the JVM collects information about:

  • Most common types
  • Presence of null values
  • Type flow patterns
  • Exception-causing types

Data Storage (Type Profile Tables)

The JVM maintains three types of profile tables to make smart optimization decisions. Let’s understand each with a practical example.

1. Types by Location Table

This table tracks what types appear at each profiling point in your code. Consider this method:

public void processInput(Object input) {  // Profiling point 1
if (input instanceof String) { // Profiling point 2
String str = (String)input; // Profiling point 3
str.length();
} else {
input.toString(); // Profiling point 4
}
} // Profiling point 5

The JVM builds a table like this:

Map<ProfilingPoint, Set<Class>> typesByLocation = {
methodEntry: [String.class, Integer.class], // What enters the method
instanceofCheck: [String.class], // What passes instanceof
castPoint: [String.class], // What gets cast
elseBranch: [Integer.class], // What goes to else
methodExit: [String.class, Integer.class] // What leaves the method
}

This helps JVM understand:

  • What types to expect at each point
  • Where type checks usually succeed/fail
  • Where to optimize type-specific code

2. Type Frequency Table

This table counts how often each type appears. For example:

// After processing 1000 calls to processInput():
Map<Class, Integer> typeFrequencies = {
String.class: 850, // String was passed 850 times
Integer.class: 150 // Integer was passed 150 times

The JVM uses this to:

  • Optimize for the most common cases (85% Strings)
  • Decide if specialized code paths are worth creating
  • Determine when to deoptimize based on changing patterns

3. Type Flow Table

This tracks how types “flow” through your code. For example:

public void process(Object input) {          // point1
if (validate(input)) { // point2
handleValidInput(input); // point3
}
}

The JVM builds a flow table:

Map<ProfilingPoint, Map<Class, Set<Class>>> typeFlow = {
"point1->point2": {
// When Object enters, we see String/Integer
Object.class -> [String.class, Integer.class],
// Strings stay Strings
String.class -> [String.class]
},
"point2->point3": {
// Only Strings make it to handleValidInput
String.class -> [String.class]
}
}

This helps JVM:

  • Predict type changes through program flow
  • Optimize type checks and casts
  • Identify patterns for method inlining

Optimization strategies

1. Probability-based speculation

The JVM calculates probabilities for each type it observes:

void handleData(Object data) {
// If JVM observes:
// String: 90% of calls
// Integer: 10% of calls
// It will optimize for String handling
data.toString();
}

The JVM will generate optimized machine code for the most common type(s), with fallback paths for less common types.

2. Hierarchical type speculation

The JVM considers inheritance relationships when speculating:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

void feed(Animal animal) {
// JVM tracks that it sees:
// Dog: 60%
// Cat: 40%
// It can optimize knowing both are Animals
animal.eat();
}

This allows for optimizations that work with parent classes even when child classes vary.

3. Guards for type safety

Guards are safety checks the JVM inserts:

// Original Java code
void process(Object obj) {
obj.toString();
}

// Conceptual JVM-generated code
void process(Object obj) {
if (obj.getClass() == String.class) { // Guard
// Fast path: direct call to String.toString()
} else {
// Slow path: regular virtual call
}
}

If a guard fails, the code deoptimizes to a safer version.

Putting it all together

Here’s how the system works as a whole:

  1. Data collection — JVM monitors code execution at profiling points
  2. Analysis — Records type frequencies. Tracks type flow patterns. Identifies common paths.
  3. Optimization — Uses probability-based speculation for common cases. Applies hierarchical optimization where possible. Adds guards to protect optimizations.
  4. Safety — Maintains fallback paths. Deoptimizes when guards fail. Preserves Java’s type safety guarantees.

Example

public void processData(Object data) {
// JVM knows from profiling:
// 1. 85% String (frequency table)
// 2. Strings pass instanceof (type flow)
// 3. No exceptions at cast (location table)

if (data instanceof String) {
String str = (String)data;
// Heavily optimized String path
str.length();
} else {
// Fallback path
data.toString();
}
}

This system allows the JVM to:

  1. Generate optimized code for common cases
  2. Maintain type safety with guards
  3. Fallback gracefully when needed
  4. Adapt to changing patterns

Summary

The JVM’s type profiling and speculation system combines:

  • Strategic monitoring points
  • Comprehensive data collection
  • Multiple optimization strategies
  • Safety mechanisms

This creates a flexible, efficient, and safe execution environment for Java code.

--

--

No responses yet