google-site-verification: google61fe8ba583a51912.html
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