Substrate VM Internals: Complete Guide

Substrate VM (now part of GraalVM Native Image) is a framework that allows ahead-of-time (AOT) compilation of Java applications to native executables. This guide covers its internals, architecture, and practical usage.

1. Substrate VM Architecture Overview

Core Components

// Conceptual architecture of Substrate VM
public class SubstrateVMArchitecture {
// 1. Pointers Analysis
public static class PointsToAnalysis {
private final ReachabilityGraph reachabilityGraph;
private final HeapModel heapModel;
public void analyze(Set<Class<?>> rootClasses) {
// Build call graph from entry points
// Analyze object flows and references
// Determine which classes, methods, and fields are reachable
}
}
// 2. Heap Snapshotting
public static class HeapSnapshotter {
public ImageHeap createImageHeap(PointsToAnalysis analysis) {
// Create immutable heap snapshot
// Initialize objects at build time
// Handle object constants and static fields
}
}
// 3. Code Generation
public static class NativeImageGenerator {
public void generateExecutable(PointsToAnalysis analysis, 
ImageHeap imageHeap,
CompilationConfiguration config) {
// Generate machine code for reachable methods
// Layout memory segments
// Create executable with runtime components
}
}
}

2. Native Image Build Process

Build Process Pipeline

public class NativeImageBuildProcess {
public static void main(String[] args) {
// 1. Initialization Phase
initializeBuildEnvironment();
// 2. Analysis Phase
PointsToAnalysis analysis = performPointsToAnalysis();
// 3. Heap Snapshot Phase  
ImageHeap imageHeap = createImageHeap(analysis);
// 4. Code Generation Phase
generateNativeExecutable(analysis, imageHeap);
// 5. Finalization Phase
createFinalExecutable();
}
private static void initializeBuildEnvironment() {
// Set up classpath and module path
// Initialize Graal compiler
// Load configuration files
}
private static PointsToAnalysis performPointsToAnalysis() {
PointsToAnalysis analysis = new PointsToAnalysis();
// Start from entry points
analysis.addRootClasses(findMainClass());
analysis.addRootClasses(findReflectionConfiguration());
analysis.addRootClasses(findJNIConfiguration());
// Perform static analysis
analysis.buildCallGraph();
analysis.computeReachableMethods();
analysis.computeReachableFields();
return analysis;
}
private static ImageHeap createImageHeap(PointsToAnalysis analysis) {
ImageHeapBuilder builder = new ImageHeapBuilder();
// Create objects for static fields
builder.initializeStaticFields();
// Handle object constants
builder.processConstantObjects();
// Build immutable heap
return builder.build();
}
private static void generateNativeExecutable(PointsToAnalysis analysis, 
ImageHeap imageHeap) {
NativeImageGenerator generator = new NativeImageGenerator();
// Generate code for reachable methods
generator.compileMethods(analysis.getReachableMethods());
// Layout memory segments
generator.layoutImageHeap(imageHeap);
generator.layoutCodeCache();
generator.layoutMetadata();
// Create executable
generator.assembleExecutable();
}
}

3. Points-to Analysis Implementation

Reachability Analysis Core

public class ReachabilityAnalyzer {
private final Set<AnalysisMethod> reachableMethods = new HashSet<>();
private final Set<AnalysisField> reachableFields = new HashSet<>();
private final Set<AnalysisClass> reachableClasses = new HashSet<>();
private final CallGraph callGraph = new CallGraph();
public void analyzeFromRoots(Set<AnalysisMethod> roots) {
Queue<AnalysisMethod> worklist = new LinkedList<>(roots);
while (!worklist.isEmpty()) {
AnalysisMethod current = worklist.poll();
if (reachableMethods.add(current)) {
// Analyze method body for new reachable elements
analyzeMethodBody(current, worklist);
// Add class to reachable set
reachableClasses.add(current.getDeclaringClass());
// Analyze method's exception handlers
analyzeExceptionHandlers(current, worklist);
}
}
}
private void analyzeMethodBody(AnalysisMethod method, Queue<AnalysisMethod> worklist) {
BytecodeParser parser = new BytecodeParser(method);
Instruction[] instructions = parser.parse();
for (Instruction instruction : instructions) {
switch (instruction.getOpcode()) {
case INVOKEVIRTUAL:
case INVOKESPECIAL:
case INVOKESTATIC:
case INVOKEINTERFACE:
AnalysisMethod callee = resolveMethod(instruction);
if (callee != null && !reachableMethods.contains(callee)) {
worklist.add(callee);
callGraph.addEdge(method, callee);
}
break;
case GETFIELD:
case PUTFIELD:
case GETSTATIC:
case PUTSTATIC:
AnalysisField field = resolveField(instruction);
if (field != null) {
reachableFields.add(field);
reachableClasses.add(field.getDeclaringClass());
}
break;
case NEW:
case ANEWARRAY:
case MULTIANEWARRAY:
AnalysisClass clazz = resolveClass(instruction);
if (clazz != null) {
reachableClasses.add(clazz);
// Add class initializer if exists
AnalysisMethod clinit = clazz.getClassInitializer();
if (clinit != null && !reachableMethods.contains(clinit)) {
worklist.add(clinit);
}
}
break;
case LDC:
// Handle constant pool entries
handleConstantPool(instruction, worklist);
break;
}
}
}
private void analyzeExceptionHandlers(AnalysisMethod method, Queue<AnalysisMethod> worklist) {
for (ExceptionHandler handler : method.getExceptionHandlers()) {
AnalysisClass exceptionClass = handler.getExceptionClass();
if (exceptionClass != null) {
reachableClasses.add(exceptionClass);
// Check if exception has a static initializer
AnalysisMethod clinit = exceptionClass.getClassInitializer();
if (clinit != null && !reachableMethods.contains(clinit)) {
worklist.add(clinit);
}
}
}
}
}

Call Graph Construction

public class CallGraph {
private final Map<AnalysisMethod, Set<AnalysisMethod>> edges = new HashMap<>();
private final Map<AnalysisMethod, Set<AnalysisMethod>> callers = new HashMap<>();
public void addEdge(AnalysisMethod caller, AnalysisMethod callee) {
edges.computeIfAbsent(caller, k -> new HashSet<>()).add(callee);
callers.computeIfAbsent(callee, k -> new HashSet<>()).add(caller);
}
public Set<AnalysisMethod> getCallees(AnalysisMethod method) {
return edges.getOrDefault(method, Collections.emptySet());
}
public Set<AnalysisMethod> getCallers(AnalysisMethod method) {
return callers.getOrDefault(method, Collections.emptySet());
}
public boolean isReachable(AnalysisMethod method) {
return !getCallers(method).isEmpty() || method.isEntryPoint();
}
}
public class AnalysisMethod {
private final String className;
private final String methodName;
private final String descriptor;
private final int modifiers;
private final byte[] bytecode;
private final AnalysisClass declaringClass;
public boolean isEntryPoint() {
return "main".equals(methodName) && 
"([Ljava/lang/String;)V".equals(descriptor) &&
Modifier.isStatic(modifiers) &&
Modifier.isPublic(modifiers);
}
public AnalysisClass getDeclaringClass() {
return declaringClass;
}
// Getters for other properties...
}

4. Heap Snapshotting Implementation

Image Heap Builder

public class ImageHeapBuilder {
private final Map<Object, Integer> objectToID = new HashMap<>();
private final List<Object> objects = new ArrayList<>();
private final Map<AnalysisField, Object> staticFieldValues = new HashMap<>();
public ImageHeap build() {
// Create immutable snapshot of the heap
Object[] heapObjects = objects.toArray();
Map<AnalysisField, Object> frozenStaticFields = 
Collections.unmodifiableMap(new HashMap<>(staticFieldValues));
return new ImageHeap(heapObjects, frozenStaticFields);
}
public void initializeStaticFields() {
for (AnalysisClass clazz : getReachableClasses()) {
initializeClassStaticFields(clazz);
}
}
private void initializeClassStaticFields(AnalysisClass clazz) {
try {
// Force class initialization at build time
Class<?> runtimeClass = Class.forName(clazz.getName());
for (AnalysisField field : clazz.getStaticFields()) {
if (field.isReachable()) {
initializeStaticField(field, runtimeClass);
}
}
} catch (ClassNotFoundException e) {
// Handle class not found during build
initializeStaticFieldsStatically(clazz);
}
}
private void initializeStaticField(AnalysisField field, Class<?> runtimeClass) {
try {
java.lang.reflect.Field reflectiveField = 
runtimeClass.getDeclaredField(field.getName());
reflectiveField.setAccessible(true);
Object value = reflectiveField.get(null);
if (value != null) {
staticFieldValues.put(field, value);
registerObject(value);
}
} catch (Exception e) {
// Handle initialization failure
System.err.println("Failed to initialize field: " + field);
}
}
private void registerObject(Object obj) {
if (!objectToID.containsKey(obj)) {
int id = objects.size();
objectToID.put(obj, id);
objects.add(obj);
// Recursively register referenced objects
registerReferencedObjects(obj);
}
}
private void registerReferencedObjects(Object obj) {
if (obj instanceof Object[]) {
for (Object element : (Object[]) obj) {
if (element != null) {
registerObject(element);
}
}
} else if (obj instanceof String) {
// Strings are handled specially
registerStringReferences((String) obj);
}
// Handle other object types as needed
}
}

5. Runtime Components

Substrate VM Runtime

public class SubstrateVMRuntime {
// Memory management
public static class ImageHeapMemory {
private final long heapStart;
private final long heapSize;
public ImageHeapMemory(long start, long size) {
this.heapStart = start;
this.heapSize = size;
}
public native long allocateObject(int size);
public native void freeObject(long address);
}
// Thread management
public static class VMThreads {
private static final ThreadLocal<VMThread> currentThread = new ThreadLocal<>();
public static VMThread currentThread() {
VMThread thread = currentThread.get();
if (thread == null) {
thread = new VMThread(Thread.currentThread());
currentThread.set(thread);
}
return thread;
}
}
// Garbage Collection (simplified)
public static class ImageHeapGC {
public static void collectGarbage() {
// Since most objects are in image heap and immutable,
// GC mainly handles runtime-allocated objects
markRuntimeObjects();
sweepRuntimeObjects();
}
private static native void markRuntimeObjects();
private static native void sweepRuntimeObjects();
}
// Reflection support
public static class ReflectionSupport {
private static final Map<String, Class<?>> knownClasses = new HashMap<>();
private static final Map<String, Method> knownMethods = new HashMap<>();
static {
initializeReflectionData();
}
private static void initializeReflectionData() {
// Load reflection configuration generated during build
loadReflectionConfiguration();
}
public static Class<?> forName(String className) {
Class<?> clazz = knownClasses.get(className);
if (clazz == null) {
throw new NoClassDefFoundError("Class not included in native image: " + className);
}
return clazz;
}
public static Method getMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
String key = clazz.getName() + "#" + methodName;
Method method = knownMethods.get(key);
if (method == null) {
throw new NoSuchMethodError("Method not included in native image: " + key);
}
return method;
}
}
}

6. Configuration Files

Reflection Configuration

{
"name": "com.example.MyClass",
"methods": [
{
"name": "myMethod",
"parameterTypes": ["java.lang.String", "int"]
},
{
"name": "<init>",
"parameterTypes": []
}
],
"fields": [
{
"name": "myField"
}
]
}

Native Image Configuration

public class NativeImageConfiguration {
@AutomaticallyFeature
public static class RuntimeInitializedClassesFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Register classes that need runtime initialization
RuntimeClassInitialization.initializeAtRunTime(
"com.example.SomeClass",
"com.example.AnotherClass"
);
}
}
@AutomaticallyFeature  
public static class ReflectionConfigurationFeature implements Feature {
@Override
public void duringSetup(DuringSetupAccess access) {
// Register classes for reflection
ReflectionRegistry registry = ImageSingletons.lookup(ReflectionRegistry.class);
registry.register(MyClass.class);
registry.register(MyClass.class.getDeclaredMethod("myMethod", String.class, int.class));
}
}
@AutomaticallyFeature
public static class ResourceConfigurationFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Include resources in native image
ResourceConfiguration resourceConfig = ImageSingletons.lookup(ResourceConfiguration.class);
resourceConfig.addResourceBundle("com.example.messages");
resourceConfig.addResources("META-INF/services/.*");
}
}
}

7. Custom Features and Hooks

Building Custom Features

public class CustomNativeImageFeatures {
// Feature for custom initialization
@AutomaticallyFeature
public static class CustomInitializationFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
System.out.println("Starting analysis phase...");
// Register custom reachability handlers
access.registerReachabilityHandler(this::handleClassReachable, 
access.getMetaAccess().lookupJavaType(MyCriticalClass.class));
}
private void handleClassReachable(DuringAnalysisAccess access, Class<?> clazz) {
System.out.println("Class became reachable: " + clazz.getName());
// Force initialization of related classes
access.requireAnalysisIteration();
}
@Override
public void duringAnalysis(DuringAnalysisAccess access) {
// Custom analysis logic
analyzeDynamicProxyUsage(access);
analyzeResourceUsage(access);
}
@Override
public void afterAnalysis(AfterAnalysisAccess access) {
System.out.println("Analysis completed");
// Generate additional configuration
generateAdditionalReflectionConfig();
}
private void analyzeDynamicProxyUsage(DuringAnalysisAccess access) {
// Analyze dynamic proxy creation
access.getBigBang().getUniverse().getMethods().stream()
.filter(method -> method.getName().contains("$Proxy"))
.forEach(proxyMethod -> {
System.out.println("Found dynamic proxy: " + proxyMethod.getName());
});
}
private void generateAdditionalReflectionConfig() {
// Generate reflection configuration for dynamically discovered classes
try (FileWriter writer = new FileWriter("generated-reflection.json")) {
writer.write(generateReflectionJson());
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Feature for JNI support
@AutomaticallyFeature
public static class JNISupportFeature implements Feature {
@Override
public void duringSetup(DuringSetupAccess access) {
// Register JNI libraries
JNIRegistration jniReg = ImageSingletons.lookup(JNIRegistration.class);
jniReg.addLibrary("mylibrary");
// Register JNI methods
jniReg.registerMethods(MyJNIClass.class);
}
}
// Feature for resource management
@AutomaticallyFeature
public static class ResourceManagementFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Configure resource inclusion patterns
ResourceConfiguration resources = ImageSingletons.lookup(ResourceConfiguration.class);
// Include all properties files
resources.addResourceRegex(".*\\.properties");
// Include specific directories
resources.addResourceRegex("META-INF/.*");
resources.addResourceRegex("WEB-INF/.*");
}
}
}

8. Build-Time Initialization

Build-Time Code Execution

public class BuildTimeInitialization {
@Feature
public static class BuildTimeInitFeature implements Feature {
private static final Set<String> INITIALIZED_CLASSES = new HashSet<>();
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Initialize classes at build time for better performance
initializeClassesAtBuildTime(access);
}
private void initializeClassesAtBuildTime(BeforeAnalysisAccess access) {
String[] buildTimeClasses = {
"java.util.Locale",
"java.time.ZoneId",
"java.text.DecimalFormatSymbols",
"com.example.config.AppConfig"
};
for (String className : buildTimeClasses) {
try {
Class<?> clazz = Class.forName(className);
forceInitialization(clazz);
INITIALIZED_CLASSES.add(className);
} catch (Exception e) {
System.err.println("Failed to initialize " + className + " at build time: " + e.getMessage());
}
}
}
private void forceInitialization(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to initialize class: " + clazz.getName(), e);
}
}
}
}
// Example of a class designed for build-time initialization
public class AppConfig {
private static final Properties CONFIG = loadConfig();
static {
// This static initializer runs at build time
System.out.println("AppConfig initialized at build time");
}
private static Properties loadConfig() {
Properties props = new Properties();
try (InputStream is = AppConfig.class.getResourceAsStream("/application.properties")) {
if (is != null) {
props.load(is);
}
} catch (IOException e) {
System.err.println("Failed to load configuration: " + e.getMessage());
}
return props;
}
public static String getProperty(String key) {
return CONFIG.getProperty(key);
}
public static String getProperty(String key, String defaultValue) {
return CONFIG.getProperty(key, defaultValue);
}
}

9. Debugging and Monitoring

Build Process Monitoring

public class BuildProcessMonitor {
@Feature
public static class BuildMonitoringFeature implements Feature {
private long analysisStartTime;
private long codeGenStartTime;
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
analysisStartTime = System.currentTimeMillis();
System.out.println("Starting points-to analysis...");
}
@Override
public void afterAnalysis(AfterAnalysisAccess access) {
long analysisTime = System.currentTimeMillis() - analysisStartTime;
System.out.printf("Points-to analysis completed in %d ms%n", analysisTime);
// Print analysis statistics
printAnalysisStatistics(access);
}
@Override
public void beforeCompilation(BeforeCompilationAccess access) {
codeGenStartTime = System.currentTimeMillis();
System.out.println("Starting code generation...");
}
@Override
public void afterCompilation(AfterCompilationAccess access) {
long codeGenTime = System.currentTimeMillis() - codeGenStartTime;
System.out.printf("Code generation completed in %d ms%n", codeGenTime);
}
private void printAnalysisStatistics(DuringAnalysisAccess access) {
AnalysisUniverse universe = access.getUniverse();
System.out.printf("Reachable classes: %d%n", universe.getTypes().size());
System.out.printf("Reachable methods: %d%n", universe.getMethods().size());
System.out.printf("Reachable fields: %d%n", universe.getFields().size());
// Print memory usage
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
System.out.printf("Memory usage: %d MB%n", usedMemory / (1024 * 1024));
}
}
// Feature for generating build reports
@Feature
public static class BuildReportFeature implements Feature {
@Override
public void afterAnalysis(AfterAnalysisAccess access) {
generateReachabilityReport(access);
generateDependencyReport(access);
}
private void generateReachabilityReport(DuringAnalysisAccess access) {
try (PrintWriter writer = new PrintWriter("reachability-report.txt")) {
writer.println("=== Reachability Report ===");
writer.println();
AnalysisUniverse universe = access.getUniverse();
writer.println("Reachable Classes:");
universe.getTypes().stream()
.sorted(Comparator.comparing(AnalysisType::getName))
.forEach(type -> writer.println("  " + type.getName()));
writer.println();
writer.println("Reachable Methods:");
universe.getMethods().stream()
.sorted(Comparator.comparing(AnalysisMethod::getQualifiedName))
.forEach(method -> writer.println("  " + method.getQualifiedName()));
} catch (IOException e) {
e.printStackTrace();
}
}
private void generateDependencyReport(DuringAnalysisAccess access) {
try (PrintWriter writer = new PrintWriter("dependency-report.txt")) {
writer.println("=== Dependency Report ===");
writer.println();
AnalysisUniverse universe = access.getUniverse();
CallGraph callGraph = universe.getCallGraph();
// Print method dependencies
for (AnalysisMethod method : universe.getMethods()) {
Set<AnalysisMethod> callees = callGraph.getCallees(method);
if (!callees.isEmpty()) {
writer.println(method.getQualifiedName() + " calls:");
for (AnalysisMethod callee : callees) {
writer.println("  -> " + callee.getQualifiedName());
}
writer.println();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

10. Advanced Usage Patterns

Dynamic Features with Conditional Logic

public class ConditionalNativeImageFeatures {
@AutomaticallyFeature
public static class ConditionalInitializationFeature implements Feature {
@Override
public void duringSetup(DuringSetupAccess access) {
// Check system properties to conditionally enable features
String profile = System.getProperty("native.image.profile", "default");
switch (profile) {
case "minimal":
configureMinimalProfile(access);
break;
case "full":
configureFullProfile(access);
break;
case "server":
configureServerProfile(access);
break;
default:
configureDefaultProfile(access);
}
}
private void configureMinimalProfile(DuringSetupAccess access) {
// Minimal configuration for small footprint
System.out.println("Configuring minimal profile");
// Only include essential classes
RuntimeClassInitialization.initializeAtBuildTime(
"java.util.Collections",
"java.util.Arrays"
);
}
private void configureFullProfile(DuringSetupAccess access) {
// Full configuration with all features
System.out.println("Configuring full profile");
// Include additional libraries and features
includeAdditionalLibraries(access);
enableAdvancedFeatures(access);
}
private void includeAdditionalLibraries(DuringSetupAccess access) {
// Conditionally include libraries based on classpath
if (isClassPresent("com.fasterxml.jackson.databind.ObjectMapper")) {
System.out.println("Jackson detected - configuring JSON support");
configureJacksonSupport(access);
}
if (isClassPresent("org.slf4j.Logger")) {
System.out.println("SLF4J detected - configuring logging");
configureLoggingSupport(access);
}
}
private boolean isClassPresent(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
// Feature for plugin system support
@AutomaticallyFeature
public static class PluginSystemFeature implements Feature {
@Override
public void duringAnalysis(DuringAnalysisAccess access) {
// Discover plugins using service loader pattern
discoverPlugins(access);
// Generate plugin configuration
generatePluginConfiguration(access);
}
private void discoverPlugins(DuringAnalysisAccess access) {
// Use ServiceLoader to discover plugins at build time
ServiceLoader<Plugin> plugins = ServiceLoader.load(Plugin.class);
for (Plugin plugin : plugins) {
System.out.println("Discovered plugin: " + plugin.getClass().getName());
// Register plugin classes for reflection
registerPluginForReflection(plugin.getClass());
// Initialize plugin at build time if possible
initializePlugin(plugin);
}
}
private void generatePluginConfiguration(DuringAnalysisAccess access) {
// Generate configuration file for discovered plugins
Properties pluginConfig = new Properties();
ServiceLoader<Plugin> plugins = ServiceLoader.load(Plugin.class);
for (Plugin plugin : plugins) {
pluginConfig.setProperty(plugin.getName(), plugin.getClass().getName());
}
try (FileOutputStream fos = new FileOutputStream("plugins.properties")) {
pluginConfig.store(fos, "Auto-generated plugin configuration");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// Plugin interface for dynamic discovery
public interface Plugin {
String getName();
void initialize();
}
// Example plugin implementation
public class DatabasePlugin implements Plugin {
@Override
public String getName() {
return "database";
}
@Override
public void initialize() {
System.out.println("Database plugin initialized at build time");
}
// This method will be called by reflection
public static void registerDrivers() {
try {
Class.forName("org.h2.Driver");
Class.forName("org.postgresql.Driver");
} catch (ClassNotFoundException e) {
System.err.println("Database drivers not found: " + e.getMessage());
}
}
}

This comprehensive guide covers Substrate VM internals, from the build process and points-to analysis to runtime components and advanced features. Understanding these internals helps in creating optimized native images and troubleshooting build issues.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper