Introduction
Java Mission Control (JMC) provides a powerful rule engine that enables automated detection of performance anti-patterns and problematic behaviors in Java applications. This system allows for custom rule creation, real-time monitoring, and proactive alerting.
JMC Rule Engine Architecture
Rule Engine Components
// Core rule interfaces and classes public interface JMCRule { String getName(); String getDescription(); RuleSeverity getSeverity(); boolean evaluate(RecordingContext context); Map<String, String> getResults(); } public enum RuleSeverity { INFO, WARNING, ERROR, CRITICAL } public class RecordingContext { private IRecording recording; private Map<String, Object> attributes; public <T> T getAttribute(String key, Class<T> type) { return type.cast(attributes.get(key)); } public IRecording getRecording() { return recording; } } Built-in JMC Rules
Memory-Related Rules
<!-- Example of built-in memory rule configuration --> <Rule id="HeapContent"> <Name>Heap Content</Name> <Description>Analyzes heap content for potential issues</Description> <Severity>WARNING</Severity> <Category>Memory</Category> <Impl>com.oracle.jmc.common.heap.HeapContentRule</Impl> <Parameters> <Parameter name="threshold">0.8</Parameter> <Parameter name="minCollectionCount">3</Parameter> </Parameters> </Rule>
GC and Performance Rules
<Rule id="GCLongPause"> <Name>Long GC Pauses</Name> <Description>Detects GC pauses longer than threshold</Description> <Severity>ERROR</Severity> <Category>GarbageCollection</Category> <Impl>com.oracle.jmc.flightrecorder.rules.jdk.gc.LongGCPauseRule</Impl> <Parameters> <Parameter name="pauseThresholdMs">1000</Parameter> </Parameters> </Rule>
Custom Rule Development
Creating Custom Rules
package com.company.jmc.rules; import org.openjdk.jmc.common.item.IItem; import org.openjdk.jmc.common.item.IItemCollection; import org.openjdk.jmc.common.item.ItemFilters; import org.openjdk.jmc.common.unit.IQuantity; import org.openjdk.jmc.flightrecorder.JfrAttributes; import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs; import org.openjdk.jmc.flightrecorder.rules.IRule; import org.openjdk.jmc.flightrecorder.rules.Result; import org.openjdk.jmc.flightrecorder.rules.Severity; import org.openjdk.jmc.flightrecorder.rules.TypedResult; import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit; import java.util.Collection; import java.util.Collections; import java.util.Map; public class CustomMemoryLeakRule implements IRule { private static final String RESULT_ID = "CustomMemoryLeak"; private static final double LEAK_THRESHOLD_PERCENT = 20.0; @Override public Result evaluate(IItemCollection items, IPreferenceValueProvider valueProvider) { // Analyze GC events for memory leak patterns IItemCollection gcEvents = items.apply( ItemFilters.type(JdkTypeIDs.GC_HEAP_SUMMARY)); if (gcEvents.isEmpty()) { return new Result(this, 0, "No GC heap summary events found"); } // Calculate memory growth trend double memoryGrowth = calculateMemoryGrowth(gcEvents); double score = calculateScore(memoryGrowth); if (memoryGrowth > LEAK_THRESHOLD_PERCENT) { String message = String.format( "Potential memory leak detected. Heap growth: %.2f%%", memoryGrowth); return new Result(this, score, message, Collections.singletonMap( TypedResult.SCORE.getIdentifier(), score )); } return new Result(this, score, "No memory leak detected"); } private double calculateMemoryGrowth(IItemCollection gcEvents) { // Implementation to calculate memory growth between GC cycles List<IQuantity> heapUsed = new ArrayList<>(); for (IItem event : gcEvents) { IQuantity used = event.getType().getAccessor("used"); if (used != null) { heapUsed.add(used); } } if (heapUsed.size() < 2) { return 0.0; } // Simple linear regression for trend analysis return calculateGrowthTrend(heapUsed); } private double calculateScore(double growthPercentage) { return Math.min(100.0, growthPercentage * 5.0); } @Override public Collection<TypedResult<?>> getResults() { return Collections.singletonList(TypedResult.SCORE); } @Override public String getId() { return RESULT_ID; } @Override public String getName() { return "Custom Memory Leak Detection"; } @Override public String getTopic() { return "Memory"; } } Threading Issue Detection Rule
public class ThreadDeadlockRule implements IRule { private static final String RESULT_ID = "ThreadDeadlockDetection"; @Override public Result evaluate(IItemCollection items, IPreferenceValueProvider valueProvider) { IItemCollection threadDumpEvents = items.apply( ItemFilters.type(JdkTypeIDs.THREAD_DUMP)); int deadlockCount = 0; StringBuilder deadlockInfo = new StringBuilder(); for (IItem event : threadDumpEvents) { String threadDump = event.getType().getAccessor("threadDump"); if (threadDump != null) { int deadlocks = analyzeThreadDumpForDeadlocks(threadDump); if (deadlocks > 0) { deadlockCount += deadlocks; deadlockInfo.append("Deadlocks found in thread dump\n"); } } } if (deadlockCount > 0) { double score = Math.min(100.0, deadlockCount * 25.0); String message = String.format( "Found %d potential deadlock(s). %s", deadlockCount, deadlockInfo.toString()); return new Result(this, score, message, Collections.singletonMap( TypedResult.SCORE.getIdentifier(), score )); } return new Result(this, 0, "No deadlocks detected"); } private int analyzeThreadDumpForDeadlocks(String threadDump) { // Parse thread dump for deadlock patterns Pattern deadlockPattern = Pattern.compile( "Found (\\d+) deadlock", Pattern.CASE_INSENSITIVE); Matcher matcher = deadlockPattern.matcher(threadDump); if (matcher.find()) { return Integer.parseInt(matcher.group(1)); } return 0; } @Override public Collection<TypedResult<?>> getResults() { return Collections.singletonList(TypedResult.SCORE); } @Override public String getId() { return RESULT_ID; } @Override public String getName() { return "Thread Deadlock Detection"; } @Override public String getTopic() { return "Threading"; } } Rule Configuration and Management
XML Rule Configuration
<!-- custom-rules.xml --> <Rules version="2.0"> <Rule id="CustomMemoryLeak"> <Name>Custom Memory Leak Detection</Name> <Description>Detects potential memory leaks based on heap growth patterns</Description> <Severity>WARNING</Severity> <Category>Memory</Category> <Impl>com.company.jmc.rules.CustomMemoryLeakRule</Impl> <Parameters> <Parameter name="growthThreshold">20.0</Parameter> <Parameter name="minSampleCount">10</Parameter> </Parameters> </Rule> <Rule id="ThreadDeadlockDetection"> <Name>Thread Deadlock Detection</Name> <Description>Detects thread deadlocks from thread dump events</Description> <Severity>ERROR</Severity> <Category>Threading</Category> <Impl>com.company.jmc.rules.ThreadDeadlockRule</Impl> </Rule> <Rule id="HighCPULoad"> <Name>High CPU Load</Name> <Description>Detects periods of high CPU utilization</Description> <Severity>WARNING</Severity> <Category>Performance</Category> <Impl>com.company.jmc.rules.HighCPULoadRule</Impl> <Parameters> <Parameter name="cpuThreshold">80.0</Parameter> <Parameter name="durationThreshold">30000</Parameter> </Parameters> </Rule> </Rules>
Rule Preference Management
public class RuleConfigurationManager { private Map<String, RulePreferences> rulePreferences; public RuleConfigurationManager() { this.rulePreferences = new HashMap<>(); loadDefaultPreferences(); } private void loadDefaultPreferences() { // Memory leak rule preferences RulePreferences memoryLeakPrefs = new RulePreferences(); memoryLeakPrefs.setThreshold("growthThreshold", 20.0); memoryLeakPrefs.setThreshold("minSampleCount", 10); rulePreferences.put("CustomMemoryLeak", memoryLeakPrefs); // CPU load rule preferences RulePreferences cpuLoadPrefs = new RulePreferences(); cpuLoadPrefs.setThreshold("cpuThreshold", 80.0); cpuLoadPrefs.setThreshold("durationThreshold", 30000L); rulePreferences.put("HighCPULoad", cpuLoadPrefs); } public void updateRulePreference(String ruleId, String parameter, Object value) { RulePreferences prefs = rulePreferences.get(ruleId); if (prefs != null) { prefs.setThreshold(parameter, value); } } public Object getRulePreference(String ruleId, String parameter) { RulePreferences prefs = rulePreferences.get(ruleId); return prefs != null ? prefs.getThreshold(parameter) : null; } } class RulePreferences { private Map<String, Object> thresholds = new HashMap<>(); public void setThreshold(String key, Object value) { thresholds.put(key, value); } public Object getThreshold(String key) { return thresholds.get(key); } } Advanced Rule Implementations
Method Performance Rule
public class MethodPerformanceRule implements IRule { private static final String RESULT_ID = "MethodPerformance"; private static final long SLOW_METHOD_THRESHOLD_MS = 1000; @Override public Result evaluate(IItemCollection items, IPreferenceValueProvider valueProvider) { IItemCollection methodProfilingEvents = items.apply( ItemFilters.type(JdkTypeIDs.METHOD_PROFILING_SAMPLE)); Map<String, MethodStats> methodStats = new HashMap<>(); // Aggregate method execution statistics for (IItem event : methodProfilingEvents) { String methodName = getMethodName(event); long duration = getMethodDuration(event); methodStats.computeIfAbsent(methodName, k -> new MethodStats()) .addSample(duration); } // Identify slow methods List<MethodResult> slowMethods = new ArrayList<>(); for (Map.Entry<String, MethodStats> entry : methodStats.entrySet()) { MethodStats stats = entry.getValue(); if (stats.getAverageDuration() > SLOW_METHOD_THRESHOLD_MS) { slowMethods.add(new MethodResult( entry.getKey(), stats.getAverageDuration(), stats.getSampleCount() )); } } if (!slowMethods.isEmpty()) { double score = calculatePerformanceScore(slowMethods); String message = buildSlowMethodsMessage(slowMethods); return new Result(this, score, message, Collections.singletonMap( TypedResult.SCORE.getIdentifier(), score )); } return new Result(this, 0, "No slow methods detected"); } private String getMethodName(IItem event) { // Extract method name from event return event.getType().getAccessor("method").toString(); } private long getMethodDuration(IItem event) { // Extract method duration from event IQuantity duration = event.getType().getAccessor("duration"); return duration != null ? duration.longValue() : 0; } private double calculatePerformanceScore(List<MethodResult> slowMethods) { return Math.min(100.0, slowMethods.size() * 10.0); } private String buildSlowMethodsMessage(List<MethodResult> slowMethods) { StringBuilder message = new StringBuilder( "Slow methods detected:\n"); for (MethodResult method : slowMethods) { message.append(String.format( "- %s: avg %.2fms (%d samples)\n", method.getMethodName(), method.getAverageDuration(), method.getSampleCount() )); } return message.toString(); } @Override public Collection<TypedResult<?>> getResults() { return Collections.singletonList(TypedResult.SCORE); } @Override public String getId() { return RESULT_ID; } @Override public String getName() { return "Method Performance Analysis"; } @Override public String getTopic() { return "Performance"; } } class MethodStats { private long totalDuration; private int sampleCount; public void addSample(long duration) { totalDuration += duration; sampleCount++; } public double getAverageDuration() { return sampleCount > 0 ? (double) totalDuration / sampleCount : 0.0; } public int getSampleCount() { return sampleCount; } } class MethodResult { private final String methodName; private final double averageDuration; private final int sampleCount; public MethodResult(String methodName, double averageDuration, int sampleCount) { this.methodName = methodName; this.averageDuration = averageDuration; this.sampleCount = sampleCount; } // Getters public String getMethodName() { return methodName; } public double getAverageDuration() { return averageDuration; } public int getSampleCount() { return sampleCount; } } Database Connection Rule
public class DatabaseConnectionRule implements IRule { private static final String RESULT_ID = "DatabaseConnectionAnalysis"; private static final int CONNECTION_LEAK_THRESHOLD = 10; @Override public Result evaluate(IItemCollection items, IPreferenceValueProvider valueProvider) { IItemCollection jdbcEvents = items.apply( ItemFilters.or( ItemFilters.type("jdbc.Connection"), ItemFilters.type("jdbc.Statement"), ItemFilters.type("jdbc.ResultSet") )); Map<String, ConnectionStats> connectionStats = new HashMap<>(); Set<String> leakedConnections = new HashSet<>(); // Analyze JDBC events for connection leaks for (IItem event : jdbcEvents) { String connectionId = getConnectionId(event); String eventType = event.getType().getIdentifier(); ConnectionStats stats = connectionStats.computeIfAbsent( connectionId, k -> new ConnectionStats()); if ("jdbc.Connection".equals(eventType)) { stats.recordConnectionEvent(event); } else if ("jdbc.Statement".equals(eventType)) { stats.recordStatementEvent(event); } else if ("jdbc.ResultSet".equals(eventType)) { stats.recordResultSetEvent(event); } // Check for potential leaks if (stats.isPotentialLeak()) { leakedConnections.add(connectionId); } } if (!leakedConnections.isEmpty()) { double score = calculateLeakScore(leakedConnections.size()); String message = buildLeakMessage(leakedConnections, connectionStats); return new Result(this, score, message, Collections.singletonMap( TypedResult.SCORE.getIdentifier(), score )); } return new Result(this, 0, "No database connection leaks detected"); } private String getConnectionId(IItem event) { // Extract connection identifier from event return event.getType().getAccessor("connectionId").toString(); } private double calculateLeakScore(int leakCount) { return Math.min(100.0, leakCount * 20.0); } private String buildLeakMessage( Set<String> leakedConnections, Map<String, ConnectionStats> stats) { StringBuilder message = new StringBuilder( "Potential database connection leaks detected:\n"); for (String connectionId : leakedConnections) { ConnectionStats connectionStats = stats.get(connectionId); message.append(String.format( "- Connection %s: %s\n", connectionId, connectionStats.getLeakReason() )); } return message.toString(); } @Override public Collection<TypedResult<?>> getResults() { return Collections.singletonList(TypedResult.SCORE); } @Override public String getId() { return RESULT_ID; } @Override public String getName() { return "Database Connection Analysis"; } @Override public String getTopic() { return "Database"; } } class ConnectionStats { private long connectionOpenTime = -1; private int statementCount = 0; private int resultSetCount = 0; private boolean closed = false; public void recordConnectionEvent(IItem event) { String operation = event.getType().getAccessor("operation").toString(); if ("open".equals(operation)) { connectionOpenTime = System.currentTimeMillis(); } else if ("close".equals(operation)) { closed = true; } } public void recordStatementEvent(IItem event) { statementCount++; } public void recordResultSetEvent(IItem event) { resultSetCount++; } public boolean isPotentialLeak() { return !closed && connectionOpenTime > 0 && (System.currentTimeMillis() - connectionOpenTime) > 300000; // 5 minutes } public String getLeakReason() { if (!closed) { return "Connection open for extended period without closure"; } return "Unknown"; } } Rule Engine Integration
JMC Rule Engine Setup
public class JMCRuleEngine { private List<IRule> rules; private RuleConfigurationManager configManager; private RuleResultProcessor resultProcessor; public JMCRuleEngine() { this.rules = new ArrayList<>(); this.configManager = new RuleConfigurationManager(); this.resultProcessor = new RuleResultProcessor(); loadRules(); } private void loadRules() { // Load built-in rules rules.add(new CustomMemoryLeakRule()); rules.add(new ThreadDeadlockRule()); rules.add(new MethodPerformanceRule()); rules.add(new DatabaseConnectionRule()); // Load rules from XML configuration loadRulesFromXML("custom-rules.xml"); } private void loadRulesFromXML(String xmlFile) { try { // Parse XML and instantiate rules RuleParser parser = new RuleParser(); List<IRule> xmlRules = parser.parseRules(xmlFile); rules.addAll(xmlRules); } catch (Exception e) { System.err.println("Failed to load rules from XML: " + e.getMessage()); } } public List<RuleResult> analyzeRecording(IRecording recording) { List<RuleResult> results = new ArrayList<>(); IItemCollection items = recording.getItems(); for (IRule rule : rules) { try { Result result = rule.evaluate(items, configManager); results.add(new RuleResult(rule, result)); // Process results for alerts resultProcessor.processResult(rule, result); } catch (Exception e) { System.err.println("Error evaluating rule " + rule.getName() + ": " + e.getMessage()); } } return results; } public void registerRule(IRule rule) { rules.add(rule); } public void setRulePreference(String ruleId, String parameter, Object value) { configManager.updateRulePreference(ruleId, parameter, value); } } class RuleResult { private final IRule rule; private final Result result; private final Date timestamp; public RuleResult(IRule rule, Result result) { this.rule = rule; this.result = result; this.timestamp = new Date(); } // Getters public IRule getRule() { return rule; } public Result getResult() { return result; } public Date getTimestamp() { return timestamp; } } Alert Processing and Notification
public class RuleResultProcessor { private List<AlertHandler> alertHandlers; private Map<String, Long> lastAlertTime; private long alertCooldownMs = 300000; // 5 minutes public RuleResultProcessor() { this.alertHandlers = new ArrayList<>(); this.lastAlertTime = new HashMap<>(); setupDefaultHandlers(); } private void setupDefaultHandlers() { alertHandlers.add(new LoggingAlertHandler()); alertHandlers.add(new EmailAlertHandler()); alertHandlers.add(new JMXAlertHandler()); } public void processResult(IRule rule, Result result) { double score = result.getScore(); // Only process results with significant scores if (score < 25.0) { return; } // Check alert cooldown String ruleId = rule.getId(); long currentTime = System.currentTimeMillis(); Long lastAlert = lastAlertTime.get(ruleId); if (lastAlert != null && (currentTime - lastAlert) < alertCooldownMs) { return; // Still in cooldown period } // Create alert Alert alert = createAlert(rule, result, score); // Send to all handlers for (AlertHandler handler : alertHandlers) { try { handler.handleAlert(alert); } catch (Exception e) { System.err.println("Error in alert handler " + handler.getClass().getSimpleName() + ": " + e.getMessage()); } } // Update last alert time lastAlertTime.put(ruleId, currentTime); } private Alert createAlert(IRule rule, Result result, double score) { return new Alert( rule.getId(), rule.getName(), result.getShortDescription(), score, new Date(), determineSeverity(score) ); } private AlertSeverity determineSeverity(double score) { if (score >= 80.0) return AlertSeverity.CRITICAL; if (score >= 60.0) return AlertSeverity.ERROR; if (score >= 40.0) return AlertSeverity.WARNING; return AlertSeverity.INFO; } public void addAlertHandler(AlertHandler handler) { alertHandlers.add(handler); } public void setAlertCooldown(long cooldownMs) { this.alertCooldownMs = cooldownMs; } } interface AlertHandler { void handleAlert(Alert alert); } class LoggingAlertHandler implements AlertHandler { @Override public void handleAlert(Alert alert) { String logMessage = String.format( "[%s] %s: %s (Score: %.1f)", alert.getSeverity(), alert.getRuleName(), alert.getMessage(), alert.getScore() ); if (alert.getSeverity() == AlertSeverity.CRITICAL) { System.err.println("CRITICAL ALERT: " + logMessage); } else { System.out.println("ALERT: " + logMessage); } } } class EmailAlertHandler implements AlertHandler { private String recipient; private EmailService emailService; public EmailAlertHandler() { this.recipient = "[email protected]"; this.emailService = new EmailService(); } @Override public void handleAlert(Alert alert) { if (alert.getSeverity().ordinal() >= AlertSeverity.ERROR.ordinal()) { String subject = String.format("JMC Alert: %s - %s", alert.getSeverity(), alert.getRuleName()); String body = String.format( "Rule: %s\nSeverity: %s\nScore: %.1f\nMessage: %s\nTime: %s", alert.getRuleName(), alert.getSeverity(), alert.getScore(), alert.getMessage(), alert.getTimestamp() ); emailService.sendEmail(recipient, subject, body); } } } enum AlertSeverity { INFO, WARNING, ERROR, CRITICAL } class Alert { private final String ruleId; private final String ruleName; private final String message; private final double score; private final Date timestamp; private final AlertSeverity severity; public Alert(String ruleId, String ruleName, String message, double score, Date timestamp, AlertSeverity severity) { this.ruleId = ruleId; this.ruleName = ruleName; this.message = message; this.score = score; this.timestamp = timestamp; this.severity = severity; } // Getters public String getRuleId() { return ruleId; } public String getRuleName() { return ruleName; } public String getMessage() { return message; } public double getScore() { return score; } public Date getTimestamp() { return timestamp; } public AlertSeverity getSeverity() { return severity; } } Rule Testing and Validation
Unit Testing Framework
public class RuleTestFramework { public static TestRecording createTestRecording() { return new TestRecording(); } public static void assertRuleFires(IRule rule, IItemCollection items, double minScore, String expectedMessage) { Result result = rule.evaluate(items, new TestPreferenceProvider()); assertTrue("Rule should fire with score >= " + minScore, result.getScore() >= minScore); if (expectedMessage != null) { assertTrue("Rule message should contain: " + expectedMessage, result.getShortDescription().contains(expectedMessage)); } } public static void assertRuleDoesNotFire(IRule rule, IItemCollection items) { Result result = rule.evaluate(items, new TestPreferenceProvider()); assertTrue("Rule should not fire", result.getScore() < 25.0); } } class TestRecording implements IRecording { private List<IItem> items = new ArrayList<>(); public void addEvent(IItem event) { items.add(event); } @Override public IItemCollection getItems() { return new TestItemCollection(items); } // Other IRecording methods... } class TestPreferenceProvider implements IPreferenceValueProvider { @Override public <T> T getPreferenceValue(ITypedPreference<T> preference) { return preference.getDefault(); } } Production Deployment
Integration with Monitoring Systems
public class JMCAlertService { private JMCRuleEngine ruleEngine; private ScheduledExecutorService scheduler; private IRecording continuousRecording; public JMCAlertService() { this.ruleEngine = new JMCRuleEngine(); this.scheduler = Executors.newScheduledThreadPool(1); setupContinuousRecording(); } private void setupContinuousRecording() { try { continuousRecording = JfrFactory.getFlightRecorder().takeSnapshot(); startContinuousMonitoring(); } catch (Exception e) { System.err.println("Failed to setup continuous recording: " + e.getMessage()); } } private void startContinuousMonitoring() { scheduler.scheduleAtFixedRate(() -> { try { IRecording snapshot = JfrFactory.getFlightRecorder().takeSnapshot(); List<RuleResult> results = ruleEngine.analyzeRecording(snapshot); logResults(results); generateReports(results); } catch (Exception e) { System.err.println("Error during monitoring cycle: " + e.getMessage()); } }, 0, 5, TimeUnit.MINUTES); // Run every 5 minutes } private void logResults(List<RuleResult> results) { for (RuleResult result : results) { if (result.getResult().getScore() > 50.0) { System.out.printf("High priority alert: %s - Score: %.1f\n", result.getRule().getName(), result.getResult().getScore()); } } } private void generateReports(List<RuleResult> results) { // Generate HTML/PDF reports ReportGenerator generator = new ReportGenerator(); generator.generateDailyReport(results); } public void shutdown() { scheduler.shutdown(); if (continuousRecording != null) { continuousRecording.close(); } } } Best Practices
Rule Development Guidelines
public class RuleBestPractices { /* * 1. Keep rules focused on specific issues * 2. Use appropriate severity levels * 3. Provide clear, actionable messages * 4. Test rules with various data sets * 5. Consider performance impact of rule evaluation * 6. Use configuration parameters for thresholds * 7. Handle missing data gracefully * 8. Document rule purpose and interpretation */ // Example of well-designed rule public class WellDesignedRule implements IRule { private static final String PARAM_THRESHOLD = "threshold"; private static final double DEFAULT_THRESHOLD = 75.0; @Override public Result evaluate(IItemCollection items, IPreferenceValueProvider valueProvider) { double threshold = valueProvider.getPreferenceValue( PreferenceToolkit.createDoublePreference(PARAM_THRESHOLD, DEFAULT_THRESHOLD)); // Rule implementation... return new Result(this, 0, "Implementation details"); } // Other required methods... } } Conclusion
The JMC Rule Engine provides a powerful framework for automated performance monitoring and alerting in Java applications. Key benefits include:
- Proactive issue detection before they impact users
- Customizable rule sets for application-specific requirements
- Integration with existing monitoring systems
- Comprehensive alerting mechanisms
- Extensible architecture for custom rule development
By implementing and customizing JMC rules, organizations can significantly improve their application monitoring capabilities and reduce mean time to resolution for performance issues.