Project Overview
A comprehensive Garbage Collection log analyzer that parses, analyzes, and provides insights into Java GC behavior. Helps identify memory issues, performance bottlenecks, and optimization opportunities.
Technology Stack
- Core: Java 17+
- Parsing: Regex patterns, Java NIO
- Analysis: Statistical analysis, trend detection
- Visualization: HTML reports with charts (Chart.js)
- Concurrency: Parallel processing for large logs
- Data Structures: Custom time-series analysis
Project Structure
gc-log-analyzer/ ├── src/main/java/com/gcanalyzer/ │ ├── parser/ │ ├── model/ │ ├── analyzer/ │ ├── reporter/ │ ├── detector/ │ └── cli/ ├── src/main/resources/ │ └── templates/ ├── examples/ │ └── gc-logs/ └── config/
Core Implementation
1. Data Models
package com.gcanalyzer.model; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; public class GCLog { private final String id; private final String source; private final LocalDateTime startTime; private final List<GCEvent> events; private final Map<String, String> metadata; private final GCStatistics statistics; public GCLog(String source) { this.id = UUID.randomUUID().toString(); this.source = source; this.startTime = LocalDateTime.now(); this.events = new CopyOnWriteArrayList<>(); this.metadata = new ConcurrentHashMap<>(); this.statistics = new GCStatistics(); } // Builder pattern public static class Builder { private String source; private List<GCEvent> events = new ArrayList<>(); private Map<String, String> metadata = new HashMap<>(); public Builder source(String source) { this.source = source; return this; } public Builder addEvent(GCEvent event) { this.events.add(event); return this; } public Builder addEvents(List<GCEvent> events) { this.events.addAll(events); return this; } public Builder metadata(String key, String value) { this.metadata.put(key, value); return this; } public GCLog build() { GCLog log = new GCLog(source); log.events.addAll(events); log.metadata.putAll(metadata); return log; } } // Getters public String getId() { return id; } public String getSource() { return source; } public LocalDateTime getStartTime() { return startTime; } public List<GCEvent> getEvents() { return Collections.unmodifiableList(events); } public Map<String, String> getMetadata() { return Collections.unmodifiableMap(metadata); } public GCStatistics getStatistics() { return statistics; } // Utility methods public void addEvent(GCEvent event) { events.add(event); statistics.update(event); } public List<GCEvent> getEventsByType(GCEventType type) { return events.stream() .filter(event -> event.getType() == type) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); } public List<GCEvent> getEventsInTimeRange(LocalDateTime start, LocalDateTime end) { return events.stream() .filter(event -> !event.getTimestamp().isBefore(start) && !event.getTimestamp().isAfter(end)) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); } public Optional<GCEvent> findEventById(String eventId) { return events.stream() .filter(event -> event.getId().equals(eventId)) .findFirst(); } public long getTotalEventsCount() { return events.size(); } public long getEventsCountByType(GCEventType type) { return events.stream() .filter(event -> event.getType() == type) .count(); } @Override public String toString() { return String.format("GCLog[source=%s, events=%d, duration=%.2fs]", source, events.size(), getDurationSeconds()); } public double getDurationSeconds() { if (events.isEmpty()) return 0.0; LocalDateTime first = events.get(0).getTimestamp(); LocalDateTime last = events.get(events.size() - 1).getTimestamp(); return java.time.Duration.between(first, last).toMillis() / 1000.0; } } public class GCEvent { private final String id; private final LocalDateTime timestamp; private final GCEventType type; private final String gcName; private final double duration; // in seconds private final MemoryUsage before; private final MemoryUsage after; private final MemoryUsage total; private final Map<String, Object> additionalInfo; public GCEvent(LocalDateTime timestamp, GCEventType type, String gcName, double duration) { this.id = UUID.randomUUID().toString(); this.timestamp = timestamp; this.type = type; this.gcName = gcName; this.duration = duration; this.before = new MemoryUsage(); this.after = new MemoryUsage(); this.total = new MemoryUsage(); this.additionalInfo = new HashMap<>(); } // Builder pattern public static class Builder { private LocalDateTime timestamp; private GCEventType type; private String gcName; private double duration; private MemoryUsage before = new MemoryUsage(); private MemoryUsage after = new MemoryUsage(); private MemoryUsage total = new MemoryUsage(); private Map<String, Object> additionalInfo = new HashMap<>(); public Builder timestamp(LocalDateTime timestamp) { this.timestamp = timestamp; return this; } public Builder type(GCEventType type) { this.type = type; return this; } public Builder gcName(String gcName) { this.gcName = gcName; return this; } public Builder duration(double duration) { this.duration = duration; return this; } public Builder before(MemoryUsage before) { this.before = before; return this; } public Builder after(MemoryUsage after) { this.after = after; return this; } public Builder total(MemoryUsage total) { this.total = total; return this; } public Builder additionalInfo(String key, Object value) { this.additionalInfo.put(key, value); return this; } public GCEvent build() { GCEvent event = new GCEvent(timestamp, type, gcName, duration); event.before.copyFrom(before); event.after.copyFrom(after); event.total.copyFrom(total); event.additionalInfo.putAll(additionalInfo); return event; } } // Getters public String getId() { return id; } public LocalDateTime getTimestamp() { return timestamp; } public GCEventType getType() { return type; } public String getGcName() { return gcName; } public double getDuration() { return duration; } public MemoryUsage getBefore() { return before; } public MemoryUsage getAfter() { return after; } public MemoryUsage getTotal() { return total; } public Map<String, Object> getAdditionalInfo() { return Collections.unmodifiableMap(additionalInfo); } // Utility methods public boolean isYoungGC() { return type == GCEventType.YOUNG_GC || gcName.toLowerCase().contains("young") || gcName.toLowerCase().contains("minor"); } public boolean isFullGC() { return type == GCEventType.FULL_GC || gcName.toLowerCase().contains("full") || gcName.toLowerCase().contains("major"); } public long getFreedMemory() { return before.getUsed() - after.getUsed(); } public double getThroughput() { if (total.getSize() == 0) return 0.0; return (double) getFreedMemory() / total.getSize() * 100.0; } public double getCollectionEfficiency() { if (duration == 0) return 0.0; return getFreedMemory() / (duration * 1024 * 1024); // MB freed per second } @Override public String toString() { return String.format("GCEvent[%s, %s, %.3fs, %s]", timestamp.format(DateTimeFormatter.ISO_LOCAL_TIME), type, duration, gcName); } } public class MemoryUsage { private long used; // in bytes private long size; // in bytes private long committed; // in bytes public MemoryUsage() {} public MemoryUsage(long used, long size, long committed) { this.used = used; this.size = size; this.committed = committed; } // Getters and setters public long getUsed() { return used; } public void setUsed(long used) { this.used = used; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public long getCommitted() { return committed; } public void setCommitted(long committed) { this.committed = committed; } // Utility methods public double getUsagePercentage() { return size > 0 ? (double) used / size * 100.0 : 0.0; } public double getCommitPercentage() { return size > 0 ? (double) committed / size * 100.0 : 0.0; } public long getFree() { return size - used; } public double getFreePercentage() { return size > 0 ? (double) getFree() / size * 100.0 : 0.0; } public void copyFrom(MemoryUsage other) { this.used = other.used; this.size = other.size; this.committed = other.committed; } @Override public String toString() { return String.format("Memory[used=%s, size=%s, committed=%s]", formatBytes(used), formatBytes(size), formatBytes(committed)); } private String formatBytes(long bytes) { if (bytes < 1024) return bytes + "B"; if (bytes < 1024 * 1024) return String.format("%.1fKB", bytes / 1024.0); if (bytes < 1024 * 1024 * 1024) return String.format("%.1fMB", bytes / (1024.0 * 1024.0)); return String.format("%.1fGB", bytes / (1024.0 * 1024.0 * 1024.0)); } } public enum GCEventType { YOUNG_GC, FULL_GC, CONCURRENT_MARK, CONCURRENT_SWEEP, CONCURRENT_RESET, CONCURRENT_PHASE, GC_PAUSE, GC_CYCLE, UNKNOWN; public static GCEventType fromString(String type) { if (type == null) return UNKNOWN; String lower = type.toLowerCase(); if (lower.contains("young") || lower.contains("minor")) return YOUNG_GC; if (lower.contains("full") || lower.contains("major")) return FULL_GC; if (lower.contains("concurrent mark")) return CONCURRENT_MARK; if (lower.contains("concurrent sweep")) return CONCURRENT_SWEEP; if (lower.contains("concurrent reset")) return CONCURRENT_RESET; if (lower.contains("pause")) return GC_PAUSE; if (lower.contains("cycle")) return GC_CYCLE; return UNKNOWN; } } public class GCStatistics { private long totalEvents; private long youngGCCount; private long fullGCCount; private long concurrentEventsCount; private double totalDuration; private double youngGCDuration; private double fullGCDuration; private double maxGCPause; private double minGCPause = Double.MAX_VALUE; private double totalFreedMemory; private long totalAllocatedMemory; private final Map<String, Long> gcTypeCounts = new HashMap<>(); private final List<Double> pauseTimes = new ArrayList<>(); private LocalDateTime firstEventTime; private LocalDateTime lastEventTime; public void update(GCEvent event) { totalEvents++; totalDuration += event.getDuration(); pauseTimes.add(event.getDuration()); // Update min/max pause times maxGCPause = Math.max(maxGCPause, event.getDuration()); minGCPause = Math.min(minGCPause, event.getDuration()); // Update event type counts gcTypeCounts.merge(event.getGcName(), 1L, Long::sum); // Update memory statistics long freed = event.getFreedMemory(); if (freed > 0) { totalFreedMemory += freed; } // Update timing if (firstEventTime == null || event.getTimestamp().isBefore(firstEventTime)) { firstEventTime = event.getTimestamp(); } if (lastEventTime == null || event.getTimestamp().isAfter(lastEventTime)) { lastEventTime = event.getTimestamp(); } // Categorize events if (event.isYoungGC()) { youngGCCount++; youngGCDuration += event.getDuration(); } else if (event.isFullGC()) { fullGCCount++; fullGCDuration += event.getDuration(); } if (event.getType().name().contains("CONCURRENT")) { concurrentEventsCount++; } } // Getters public long getTotalEvents() { return totalEvents; } public long getYoungGCCount() { return youngGCCount; } public long getFullGCCount() { return fullGCCount; } public long getConcurrentEventsCount() { return concurrentEventsCount; } public double getTotalDuration() { return totalDuration; } public double getYoungGCDuration() { return youngGCDuration; } public double getFullGCDuration() { return fullGCDuration; } public double getMaxGCPause() { return maxGCPause == Double.MIN_VALUE ? 0 : maxGCPause; } public double getMinGCPause() { return minGCPause == Double.MAX_VALUE ? 0 : minGCPause; } public double getTotalFreedMemory() { return totalFreedMemory; } public long getTotalAllocatedMemory() { return totalAllocatedMemory; } public Map<String, Long> getGcTypeCounts() { return Collections.unmodifiableMap(gcTypeCounts); } public List<Double> getPauseTimes() { return Collections.unmodifiableList(pauseTimes); } public LocalDateTime getFirstEventTime() { return firstEventTime; } public LocalDateTime getLastEventTime() { return lastEventTime; } // Calculated statistics public double getAveragePauseTime() { return totalEvents > 0 ? totalDuration / totalEvents : 0.0; } public double getThroughput() { double totalTime = getTotalObservationTime(); return totalTime > 0 ? (1 - (totalDuration / totalTime)) * 100.0 : 0.0; } public double getTotalObservationTime() { if (firstEventTime == null || lastEventTime == null) return 0.0; return java.time.Duration.between(firstEventTime, lastEventTime).toMillis() / 1000.0; } public double getPauseTimePercentile(double percentile) { if (pauseTimes.isEmpty()) return 0.0; List<Double> sorted = new ArrayList<>(pauseTimes); Collections.sort(sorted); int index = (int) Math.ceil(percentile * sorted.size()) - 1; index = Math.max(0, Math.min(index, sorted.size() - 1)); return sorted.get(index); } public double getYoungGCRate() { double observationTime = getTotalObservationTime(); return observationTime > 0 ? youngGCCount / observationTime : 0.0; } public double getFullGCRate() { double observationTime = getTotalObservationTime(); return observationTime > 0 ? fullGCCount / observationTime : 0.0; } public Map<String, Double> getGCTimeDistribution() { Map<String, Double> distribution = new HashMap<>(); double total = getTotalDuration(); if (total > 0) { distribution.put("Young GC", (youngGCDuration / total) * 100); distribution.put("Full GC", (fullGCDuration / total) * 100); distribution.put("Concurrent", ((totalDuration - youngGCDuration - fullGCDuration) / total) * 100); } return distribution; } public String getSummary() { return String.format( "GC Statistics: %d events (Young: %d, Full: %d), Throughput: %.2f%%, " + "Avg Pause: %.3fs, Max Pause: %.3fs", totalEvents, youngGCCount, fullGCCount, getThroughput(), getAveragePauseTime(), getMaxGCPause() ); } } 2. GC Log Parser
package com.gcanalyzer.parser; import com.gcanalyzer.model.*; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class GCLogParser { // Common GC log patterns for different collectors and formats private static final Pattern TIMESTAMP_PATTERN = Pattern.compile( "^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3})\\S*" ); private static final Pattern SIMPLE_GC_PATTERN = Pattern.compile( "\\[GC.*?(\\d+\\.\\d{3}):.*?(\\d+\\.\\d{3})s\\]" ); // Unified Logging (Java 9+) patterns private static final Pattern UNIFIED_GC_PATTERN = Pattern.compile( "\\[([^]]+)\\]\\s*\\[info\\]\\s*\\[gc.*?\\]\\s*(.*)" ); // G1GC patterns private static final Pattern G1GC_PATTERN = Pattern.compile( "\\[GC pause \\(G1.*?\\) (.*?), (\\d+\\.\\d{3})s\\]" ); // CMS patterns private static final Pattern CMS_GC_PATTERN = Pattern.compile( "\\[GC.*?\\[CMS:.*?\\]" ); // Parallel GC patterns private static final Pattern PARALLEL_GC_PATTERN = Pattern.compile( "\\[GC.*?PSYoungGen.*?\\]" ); // Memory size patterns (e.g., 1024K->512K(2048K)) private static final Pattern MEMORY_PATTERN = Pattern.compile( "(\\d+)([KMG]?)[Bb]?->(\\d+)([KMG]?)[Bb]?\\((\\d+)([KMG]?)[Bb]?\\)" ); // Duration pattern private static final Pattern DURATION_PATTERN = Pattern.compile( "(\\d+\\.\\d{3})s" ); private final Set<String> supportedCollectors = Set.of( "G1", "CMS", "Parallel", "Serial", "ZGC", "Shenandoah" ); public GCLog parseFromFile(Path filePath) throws IOException { String content = Files.readString(filePath); return parseFromString(content, filePath.toString()); } public GCLog parseFromString(String content, String source) { GCLog.Builder logBuilder = new GCLog.Builder().source(source); String[] lines = content.split("\n"); String detectedCollector = detectCollector(content); logBuilder.metadata("collector", detectedCollector); logBuilder.metadata("format", detectFormat(content)); for (String line : lines) { if (line.trim().isEmpty()) continue; try { GCEvent event = parseGCEvent(line, detectedCollector); if (event != null) { logBuilder.addEvent(event); } } catch (Exception e) { System.err.println("Failed to parse line: " + line + " - " + e.getMessage()); } } return logBuilder.build(); } private GCEvent parseGCEvent(String line, String collector) { // Try different parsing strategies based on collector and format GCEvent event = null; if (isUnifiedLoggingFormat(line)) { event = parseUnifiedLoggingEvent(line); } else if (collector.equals("G1")) { event = parseG1GCEvent(line); } else if (collector.equals("CMS")) { event = parseCMSEvent(line); } else if (collector.equals("Parallel") || collector.equals("Serial")) { event = parseParallelEvent(line); } else { event = parseGenericEvent(line); } return event; } private boolean isUnifiedLoggingFormat(String line) { return line.contains("[gc") && line.contains("] ["); } private GCEvent parseUnifiedLoggingEvent(String line) { Matcher matcher = UNIFIED_GC_PATTERN.matcher(line); if (!matcher.find()) return null; String timestampStr = matcher.group(1); String gcDetails = matcher.group(2); LocalDateTime timestamp = parseTimestamp(timestampStr); GCEventType type = classifyUnifiedEvent(gcDetails); String gcName = extractGCName(gcDetails); double duration = extractDuration(gcDetails); GCEvent.Builder builder = new GCEvent.Builder() .timestamp(timestamp) .type(type) .gcName(gcName) .duration(duration); // Parse memory information MemoryUsage before = extractMemoryUsage(gcDetails, "before"); MemoryUsage after = extractMemoryUsage(gcDetails, "after"); MemoryUsage total = extractHeapSize(gcDetails); if (before != null) builder.before(before); if (after != null) builder.after(after); if (total != null) builder.total(total); // Add additional info builder.additionalInfo("line", line); builder.additionalInfo("collector", "Unified"); return builder.build(); } private GCEvent parseG1GCEvent(String line) { // Parse G1GC specific format Matcher pauseMatcher = G1GC_PATTERN.matcher(line); if (!pauseMatcher.find()) return null; String details = pauseMatcher.group(1); String durationStr = pauseMatcher.group(2); LocalDateTime timestamp = extractTimestamp(line); GCEventType type = classifyG1Event(details); double duration = Double.parseDouble(durationStr); GCEvent.Builder builder = new GCEvent.Builder() .timestamp(timestamp) .type(type) .gcName("G1 " + details) .duration(duration); // Extract memory information from line extractG1MemoryInfo(line, builder); builder.additionalInfo("line", line); return builder.build(); } private GCEvent parseCMSEvent(String line) { // Parse CMS specific format if (!line.contains("[CMS")) return null; LocalDateTime timestamp = extractTimestamp(line); GCEventType type = line.contains("concurrent") ? GCEventType.CONCURRENT_MARK : GCEventType.FULL_GC; double duration = extractDuration(line); GCEvent.Builder builder = new GCEvent.Builder() .timestamp(timestamp) .type(type) .gcName("CMS") .duration(duration); extractGenericMemoryInfo(line, builder); builder.additionalInfo("line", line); return builder.build(); } private GCEvent parseParallelEvent(String line) { // Parse Parallel/Serial GC format if (!line.contains("PSYoungGen") && !line.contains("ParNew") && !line.contains("DefNew")) { return null; } LocalDateTime timestamp = extractTimestamp(line); GCEventType type = line.contains("Full GC") ? GCEventType.FULL_GC : GCEventType.YOUNG_GC; double duration = extractDuration(line); GCEvent.Builder builder = new GCEvent.Builder() .timestamp(timestamp) .type(type) .gcName("Parallel") .duration(duration); extractGenericMemoryInfo(line, builder); builder.additionalInfo("line", line); return builder.build(); } private GCEvent parseGenericEvent(String line) { // Fallback parser for generic GC events if (!line.contains("GC") && !line.contains("gc")) return null; LocalDateTime timestamp = extractTimestamp(line); GCEventType type = classifyGenericEvent(line); String gcName = "Generic"; double duration = extractDuration(line); GCEvent.Builder builder = new GCEvent.Builder() .timestamp(timestamp) .type(type) .gcName(gcName) .duration(duration); extractGenericMemoryInfo(line, builder); builder.additionalInfo("line", line); return builder.build(); } private LocalDateTime parseTimestamp(String timestampStr) { try { // Try ISO format first return LocalDateTime.parse(timestampStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (Exception e) { // Try other formats try { // Try format without timezone: 2023-10-01T12:00:00.123 if (timestampStr.length() > 23) { timestampStr = timestampStr.substring(0, 23); } return LocalDateTime.parse(timestampStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (Exception e2) { // Use current time as fallback return LocalDateTime.now(); } } } private LocalDateTime extractTimestamp(String line) { Matcher matcher = TIMESTAMP_PATTERN.matcher(line); if (matcher.find()) { return parseTimestamp(matcher.group(1)); } // If no timestamp found, use sequential timing return LocalDateTime.now(); } private double extractDuration(String line) { Matcher matcher = DURATION_PATTERN.matcher(line); if (matcher.find()) { return Double.parseDouble(matcher.group(1)); } return 0.0; } private GCEventType classifyUnifiedEvent(String details) { if (details.contains("Pause Young")) return GCEventType.YOUNG_GC; if (details.contains("Pause Full")) return GCEventType.FULL_GC; if (details.contains("Concurrent Mark")) return GCEventType.CONCURRENT_MARK; if (details.contains("Concurrent Sweep")) return GCEventType.CONCURRENT_SWEEP; if (details.contains("Concurrent Reset")) return GCEventType.CONCURRENT_RESET; return GCEventType.UNKNOWN; } private GCEventType classifyG1Event(String details) { if (details.contains("young")) return GCEventType.YOUNG_GC; if (details.contains("mixed")) return GCEventType.YOUNG_GC; // G1 mixed GC if (details.contains("full")) return GCEventType.FULL_GC; return GCEventType.UNKNOWN; } private GCEventType classifyGenericEvent(String line) { if (line.contains("Full GC")) return GCEventType.FULL_GC; if (line.contains("Young GC")) return GCEventType.YOUNG_GC; if (line.contains("concurrent")) return GCEventType.CONCURRENT_MARK; return GCEventType.UNKNOWN; } private String extractGCName(String details) { if (details.contains("G1")) return "G1"; if (details.contains("CMS")) return "CMS"; if (details.contains("Parallel")) return "Parallel"; if (details.contains("ZGC")) return "ZGC"; if (details.contains("Shenandoah")) return "Shenandoah"; return "Unknown"; } private MemoryUsage extractMemoryUsage(String details, String phase) { // Look for patterns like "used: 1024K" or "before: 1024K" Pattern pattern = Pattern.compile(phase + ":?\\s*(\\d+)([KMG]?)B?"); Matcher matcher = pattern.matcher(details); if (matcher.find()) { long value = Long.parseLong(matcher.group(1)); String unit = matcher.group(2); long bytes = convertToBytes(value, unit); return new MemoryUsage(bytes, 0, 0); } return null; } private MemoryUsage extractHeapSize(String details) { // Look for heap size patterns Pattern pattern = Pattern.compile("heap:\\s*(\\d+)([KMG]?)B?"); Matcher matcher = pattern.matcher(details); if (matcher.find()) { long value = Long.parseLong(matcher.group(1)); String unit = matcher.group(2); long bytes = convertToBytes(value, unit); return new MemoryUsage(0, bytes, 0); } return null; } private void extractG1MemoryInfo(String line, GCEvent.Builder builder) { // Extract memory information from G1GC log line // Format: 1024K->512K(2048K) Matcher matcher = MEMORY_PATTERN.matcher(line); if (matcher.find()) { long before = convertToBytes(Long.parseLong(matcher.group(1)), matcher.group(2)); long after = convertToBytes(Long.parseLong(matcher.group(3)), matcher.group(4)); long total = convertToBytes(Long.parseLong(matcher.group(5)), matcher.group(6)); builder.before(new MemoryUsage(before, total, total)); builder.after(new MemoryUsage(after, total, total)); builder.total(new MemoryUsage(0, total, total)); } } private void extractGenericMemoryInfo(String line, GCEvent.Builder builder) { // Generic memory info extraction for various formats Matcher matcher = MEMORY_PATTERN.matcher(line); if (matcher.find()) { long before = convertToBytes(Long.parseLong(matcher.group(1)), matcher.group(2)); long after = convertToBytes(Long.parseLong(matcher.group(3)), matcher.group(4)); long total = convertToBytes(Long.parseLong(matcher.group(5)), matcher.group(6)); builder.before(new MemoryUsage(before, total, total)); builder.after(new MemoryUsage(after, total, total)); builder.total(new MemoryUsage(0, total, total)); } } private long convertToBytes(long value, String unit) { if (unit == null || unit.isEmpty()) return value; switch (unit.toUpperCase()) { case "K": return value * 1024; case "M": return value * 1024 * 1024; case "G": return value * 1024 * 1024 * 1024; default: return value; } } private String detectCollector(String content) { if (content.contains("G1")) return "G1"; if (content.contains("CMS")) return "CMS"; if (content.contains("PSYoungGen") || content.contains("ParNew")) return "Parallel"; if (content.contains("DefNew")) return "Serial"; if (content.contains("ZGC")) return "ZGC"; if (content.contains("Shenandoah")) return "Shenandoah"; return "Unknown"; } private String detectFormat(String content) { if (content.contains("[gc]") && content.contains("[info]")) return "Unified"; if (content.contains("Java HotSpot")) return "Classic"; return "Generic"; } // Batch parsing for multiple files public List<GCLog> parseMultipleFiles(List<Path> filePaths) { return filePaths.parallelStream() .map(path -> { try { return parseFromFile(path); } catch (IOException e) { System.err.println("Failed to parse file: " + path + " - " + e.getMessage()); return null; } }) .filter(Objects::nonNull) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); } } 3. GC Log Analyzer
package com.gcanalyzer.analyzer; import com.gcanalyzer.model.*; import java.util.*; import java.util.stream.Collectors; public class GCLogAnalyzer { private final GCLog log; private final AnalysisResult result; public GCLogAnalyzer(GCLog log) { this.log = log; this.result = new AnalysisResult(log); } public AnalysisResult analyze() { analyzeBasicStatistics(); analyzePauseTimes(); analyzeMemoryUsage(); analyzeGCPatterns(); analyzeThroughput(); detectIssues(); calculatePercentiles(); return result; } private void analyzeBasicStatistics() { GCStatistics stats = log.getStatistics(); result.setBasicStatistics(stats); // Add derived statistics result.setYoungGCRate(stats.getYoungGCRate()); result.setFullGCRate(stats.getFullGCRate()); result.setGcTimeDistribution(stats.getGCTimeDistribution()); } private void analyzePauseTimes() { List<GCEvent> events = log.getEvents(); // Analyze pause time distribution Map<String, List<Double>> pauseTimesByType = new HashMap<>(); Map<String, Double> maxPauseByType = new HashMap<>(); Map<String, Double> avgPauseByType = new HashMap<>(); for (GCEvent event : events) { String type = event.getType().name(); double duration = event.getDuration(); pauseTimesByType.computeIfAbsent(type, k -> new ArrayList<>()).add(duration); maxPauseByType.merge(type, duration, Math::max); avgPauseByType.merge(type, duration, Double::sum); } // Calculate averages for (Map.Entry<String, Double> entry : avgPauseByType.entrySet()) { String type = entry.getKey(); double total = entry.getValue(); long count = pauseTimesByType.get(type).size(); avgPauseByType.put(type, total / count); } result.setPauseTimesByType(pauseTimesByType); result.setMaxPauseByType(maxPauseByType); result.setAvgPauseByType(avgPauseByType); // Detect long pauses detectLongPauses(events); } private void detectLongPauses(List<GCEvent> events) { double threshold = 1.0; // 1 second threshold for long pauses List<GCEvent> longPauses = events.stream() .filter(event -> event.getDuration() > threshold) .collect(Collectors.toList()); result.setLongPauses(longPauses); if (!longPauses.isEmpty()) { result.addIssue("LONG_PAUSES", String.format("Found %d GC pauses longer than %.1f seconds", longPauses.size(), threshold)); } } private void analyzeMemoryUsage() { List<GCEvent> events = log.getEvents(); // Analyze memory usage patterns List<Double> heapUsagePercentages = new ArrayList<>(); List<Double> youngGenUsage = new ArrayList<>(); List<Double> oldGenUsage = new ArrayList<>(); for (GCEvent event : events) { MemoryUsage before = event.getBefore(); MemoryUsage total = event.getTotal(); if (before.getSize() > 0) { double usage = before.getUsagePercentage(); heapUsagePercentages.add(usage); // Categorize by generation (simplified) if (event.isYoungGC()) { youngGenUsage.add(usage); } else if (event.isFullGC()) { oldGenUsage.add(usage); } } } result.setHeapUsagePercentages(heapUsagePercentages); result.setYoungGenUsage(youngGenUsage); result.setOldGenUsage(oldGenUsage); // Analyze memory efficiency analyzeMemoryEfficiency(events); } private void analyzeMemoryEfficiency(List<GCEvent> events) { double totalFreed = 0; double totalPauseTime = 0; for (GCEvent event : events) { if (event.getDuration() > 0) { totalFreed += event.getFreedMemory(); totalPauseTime += event.getDuration(); } } double efficiency = totalPauseTime > 0 ? totalFreed / (totalPauseTime * 1024 * 1024) : 0; result.setMemoryEfficiency(efficiency); if (efficiency < 100) { // Less than 100 MB freed per second of pause result.addIssue("LOW_MEMORY_EFFICIENCY", String.format("Low memory cleanup efficiency: %.1f MB/s", efficiency)); } } private void analyzeGCPatterns() { List<GCEvent> events = log.getEvents(); // Analyze GC frequency patterns analyzeGCFrequency(events); // Analyze Full GC patterns analyzeFullGCPatterns(events); // Analyze concurrent phase patterns analyzeConcurrentPatterns(events); } private void analyzeGCFrequency(List<GCEvent> events) { if (events.size() < 2) return; List<Double> intervals = new ArrayList<>(); GCEvent previous = events.get(0); for (int i = 1; i < events.size(); i++) { GCEvent current = events.get(i); double interval = java.time.Duration.between( previous.getTimestamp(), current.getTimestamp() ).toMillis() / 1000.0; intervals.add(interval); previous = current; } // Calculate statistics double avgInterval = intervals.stream().mapToDouble(D -> D).average().orElse(0); double maxInterval = intervals.stream().mapToDouble(D -> D).max().orElse(0); double minInterval = intervals.stream().mapToDouble(D -> D).min().orElse(0); result.setGcIntervals(intervals); result.setAvgGcInterval(avgInterval); result.setMaxGcInterval(maxInterval); result.setMinGcInterval(minInterval); // Detect frequent GC if (avgInterval < 1.0) { // Less than 1 second between GCs result.addIssue("FREQUENT_GC", String.format("Frequent GC: average interval %.2f seconds", avgInterval)); } } private void analyzeFullGCPatterns(List<GCEvent> events) { List<GCEvent> fullGCs = events.stream() .filter(GCEvent::isFullGC) .collect(Collectors.toList()); result.setFullGCEvents(fullGCs); if (!fullGCs.isEmpty()) { result.addIssue("FULL_GC_DETECTED", String.format("Found %d Full GC events", fullGCs.size())); // Check if Full GCs are too frequent if (fullGCs.size() > log.getStatistics().getTotalEvents() * 0.1) { result.addIssue("FREQUENT_FULL_GC", "More than 10% of GC events are Full GCs"); } } } private void analyzeConcurrentPatterns(List<GCEvent> events) { List<GCEvent> concurrentEvents = events.stream() .filter(event -> event.getType().name().contains("CONCURRENT")) .collect(Collectors.toList()); result.setConcurrentEvents(concurrentEvents); // Analyze concurrent phase durations Map<String, List<Double>> concurrentDurations = new HashMap<>(); for (GCEvent event : concurrentEvents) { String phase = event.getType().name(); concurrentDurations.computeIfAbsent(phase, k -> new ArrayList<>()) .add(event.getDuration()); } result.setConcurrentDurations(concurrentDurations); } private void analyzeThroughput() { GCStatistics stats = log.getStatistics(); double throughput = stats.getThroughput(); result.setThroughput(throughput); if (throughput < 95.0) { result.addIssue("LOW_THROUGHPUT", String.format("Low application throughput: %.1f%%", throughput)); } if (throughput < 90.0) { result.addIssue("CRITICAL_THROUGHPUT", String.format("Critical throughput issue: %.1f%%", throughput)); } } private void detectIssues() { GCStatistics stats = log.getStatistics(); // Memory issues if (stats.getFullGCCount() > 0) { result.addIssue("MEMORY_PRESSURE", "Memory pressure detected (Full GC occurred)"); } // Pause time issues if (stats.getMaxGCPause() > 5.0) { result.addIssue("VERY_LONG_PAUSE", String.format("Very long GC pause: %.2f seconds", stats.getMaxGCPause())); } // Allocation rate issues analyzeAllocationRate(); // Heap size issues analyzeHeapSize(); } private void analyzeAllocationRate() { // Simplified allocation rate analysis List<GCEvent> events = log.getEvents(); if (events.size() < 10) return; double totalAllocated = 0; double totalTime = log.getStatistics().getTotalObservationTime(); for (GCEvent event : events) { totalAllocated += event.getBefore().getUsed(); } double avgAllocationRate = totalTime > 0 ? totalAllocated / totalTime / (1024 * 1024) : 0; result.setAllocationRate(avgAllocationRate); if (avgAllocationRate > 500) { // More than 500 MB/s result.addIssue("HIGH_ALLOCATION_RATE", String.format("High allocation rate: %.1f MB/s", avgAllocationRate)); } } private void analyzeHeapSize() { // Analyze if heap size might be inappropriate List<GCEvent> events = log.getEvents(); if (events.isEmpty()) return; double avgHeapUsage = events.stream() .mapToDouble(event -> event.getBefore().getUsagePercentage()) .average() .orElse(0); result.setAvgHeapUsage(avgHeapUsage); if (avgHeapUsage > 80.0) { result.addIssue("HIGH_HEAP_USAGE", String.format("High average heap usage: %.1f%%", avgHeapUsage)); } if (avgHeapUsage < 30.0) { result.addIssue("LOW_HEAP_USAGE", String.format("Potentially oversized heap: %.1f%% average usage", avgHeapUsage)); } } private void calculatePercentiles() { GCStatistics stats = log.getStatistics(); Map<String, Double> pausePercentiles = new HashMap<>(); pausePercentiles.put("50th", stats.getPauseTimePercentile(0.50)); pausePercentiles.put("75th", stats.getPauseTimePercentile(0.75)); pausePercentiles.put("90th", stats.getPauseTimePercentile(0.90)); pausePercentiles.put("95th", stats.getPauseTimePercentile(0.95)); pausePercentiles.put("99th", stats.getPauseTimePercentile(0.99)); result.setPauseTimePercentiles(pausePercentiles); } } public class AnalysisResult { private final GCLog log; private GCStatistics basicStatistics; private double throughput; private double youngGCRate; private double fullGCRate; private Map<String, Double> gcTimeDistribution; private Map<String, List<Double>> pauseTimesByType; private Map<String, Double> maxPauseByType; private Map<String, Double> avgPauseByType; private List<GCEvent> longPauses; private List<Double> heapUsagePercentages; private List<Double> youngGenUsage; private List<Double> oldGenUsage; private double memoryEfficiency; private List<Double> gcIntervals; private double avgGcInterval; private double maxGcInterval; private double minGcInterval; private List<GCEvent> fullGCEvents; private List<GCEvent> concurrentEvents; private Map<String, List<Double>> concurrentDurations; private double allocationRate; private double avgHeapUsage; private Map<String, Double> pauseTimePercentiles; private final List<String> issues = new ArrayList<>(); public AnalysisResult(GCLog log) { this.log = log; } // Getters and setters public GCLog getLog() { return log; } public GCStatistics getBasicStatistics() { return basicStatistics; } public void setBasicStatistics(GCStatistics basicStatistics) { this.basicStatistics = basicStatistics; } public double getThroughput() { return throughput; } public void setThroughput(double throughput) { this.throughput = throughput; } public double getYoungGCRate() { return youngGCRate; } public void setYoungGCRate(double youngGCRate) { this.youngGCRate = youngGCRate; } public double getFullGCRate() { return fullGCRate; } public void setFullGCRate(double fullGCRate) { this.fullGCRate = fullGCRate; } public Map<String, Double> getGcTimeDistribution() { return gcTimeDistribution; } public void setGcTimeDistribution(Map<String, Double> gcTimeDistribution) { this.gcTimeDistribution = gcTimeDistribution; } public Map<String, List<Double>> getPauseTimesByType() { return pauseTimesByType; } public void setPauseTimesByType(Map<String, List<Double>> pauseTimesByType) { this.pauseTimesByType = pauseTimesByType; } public Map<String, Double> getMaxPauseByType() { return maxPauseByType; } public void setMaxPauseByType(Map<String, Double> maxPauseByType) { this.maxPauseByType = maxPauseByType; } public Map<String, Double> getAvgPauseByType() { return avgPauseByType; } public void setAvgPauseByType(Map<String, Double> avgPauseByType) { this.avgPauseByType = avgPauseByType; } public List<GCEvent> getLongPauses() { return longPauses; } public void setLongPauses(List<GCEvent> longPauses) { this.longPauses = longPauses; } public List<Double> getHeapUsagePercentages() { return heapUsagePercentages; } public void setHeapUsagePercentages(List<Double> heapUsagePercentages) { this.heapUsagePercentages = heapUsagePercentages; } public List<Double> getYoungGenUsage() { return youngGenUsage; } public void setYoungGenUsage(List<Double> youngGenUsage) { this.youngGenUsage = youngGenUsage; } public List<Double> getOldGenUsage() { return oldGenUsage; } public void setOldGenUsage(List<Double> oldGenUsage) { this.oldGenUsage = oldGenUsage; } public double getMemoryEfficiency() { return memoryEfficiency; } public void setMemoryEfficiency(double memoryEfficiency) { this.memoryEfficiency = memoryEfficiency; } public List<Double> getGcIntervals() { return gcIntervals; } public void setGcIntervals(List<Double> gcIntervals) { this.gcIntervals = gcIntervals; } public double getAvgGcInterval() { return avgGcInterval; } public void setAvgGcInterval(double avgGcInterval) { this.avgGcInterval = avgGcInterval; } public double getMaxGcInterval() { return maxGcInterval; } public void setMaxGcInterval(double maxGcInterval) { this.maxGcInterval = maxGcInterval; } public double getMinGcInterval() { return minGcInterval; } public void setMinGcInterval(double minGcInterval) { this.minGcInterval = minGcInterval; } public List<GCEvent> getFullGCEvents() { return fullGCEvents; } public void setFullGCEvents(List<GCEvent> fullGCEvents) { this.fullGCEvents = fullGCEvents; } public List<GCEvent> getConcurrentEvents() { return concurrentEvents; } public void setConcurrentEvents(List<GCEvent> concurrentEvents) { this.concurrentEvents = concurrentEvents; } public Map<String, List<Double>> getConcurrentDurations() { return concurrentDurations; } public void setConcurrentDurations(Map<String, List<Double>> concurrentDurations) { this.concurrentDurations = concurrentDurations; } public double getAllocationRate() { return allocationRate; } public void setAllocationRate(double allocationRate) { this.allocationRate = allocationRate; } public double getAvgHeapUsage() { return avgHeapUsage; } public void setAvgHeapUsage(double avgHeapUsage) { this.avgHeapUsage = avgHeapUsage; } public Map<String, Double> getPauseTimePercentiles() { return pauseTimePercentiles; } public void setPauseTimePercentiles(Map<String, Double> pauseTimePercentiles) { this.pauseTimePercentiles = pauseTimePercentiles; } public List<String> getIssues() { return Collections.unmodifiableList(issues); } public void addIssue(String type, String description) { issues.add(type + ": " + description); } public boolean hasIssues() { return !issues.isEmpty(); } public int getIssueCount() { return issues.size(); } public List<String> getIssuesBySeverity() { return issues.stream() .sorted(this::compareIssueSeverity) .collect(Collectors.toList()); } private int compareIssueSeverity(String a, String b) { int severityA = getSeverity(a); int severityB = getSeverity(b); return Integer.compare(severityB, severityA); // Descending } private int getSeverity(String issue) { if (issue.contains("CRITICAL") || issue.contains("VERY_LONG")) return 3; if (issue.contains("FULL_GC") || issue.contains("HIGH_")) return 2; return 1; } public String getSummary() { return String.format( "Analysis Result: %d events, %.1f%% throughput, %d issues found", basicStatistics.getTotalEvents(), throughput, issues.size() ); } } 4. HTML Report Generator
package com.gcanalyzer.reporter; import com.gcanalyzer.model.*; import com.gcanalyzer.analyzer.AnalysisResult; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; public class HTMLReportGenerator { public void generateReport(AnalysisResult result, Path outputDir) throws IOException { String htmlContent = generateHTMLContent(result); Path outputFile = outputDir.resolve("gc-analysis-report.html"); Files.writeString(outputFile, htmlContent); System.out.println("HTML report generated: " + outputFile.toAbsolutePath()); } private String generateHTMLContent(AnalysisResult result) { StringBuilder html = new StringBuilder(); html.append(""" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>GC Log Analysis Report</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; } .container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; } .section { margin-bottom: 30px; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; background: #fafafa; } .section-title { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; margin-top: 0; font-size: 1.4em; } .issue { padding: 12px; margin: 8px 0; border-radius: 6px; font-weight: 500; } .issue-critical { background: #ffebee; border-left: 5px solid #f44336; color: #c62828; } .issue-warning { background: #fff3e0; border-left: 5px solid #ff9800; color: #ef6c00; } .issue-info { background: #e8f5e8; border-left: 5px solid #4caf50; color: #2e7d32; } .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin: 20px 0; } .stat-card { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1); border-left: 4px solid #3498db; } .stat-value { font-size: 28px; font-weight: bold; color: #2c3e50; margin: 10px 0; } .stat-label { color: #7f8c8d; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; } .chart-container { height: 400px; margin: 25px 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } table { width: 100%; border-collapse: collapse; margin-top: 15px; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } th, td { padding: 15px; text-align: left; border-bottom: 1px solid #e0e0e0; } th { background-color: #34495e; color: white; font-weight: 600; } tr:hover { background-color: #f5f5f5; } .badge { display: inline-block; padding: 5px 12px; border-radius: 15px; font-size: 12px; font-weight: bold; margin: 2px; } .badge-success { background: #d4edda; color: #155724; } .badge-warning { background: #fff3cd; color: #856404; } .badge-danger { background: #f8d7da; color: #721c24; } .badge-info { background: #d1ecf1; color: #0c5460; } .progress-bar { background: #ecf0f1; border-radius: 10px; overflow: hidden; height: 20px; margin: 10px 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, #2ecc71, #3498db); transition: width 0.3s ease; } .summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } @media (max-width: 768px) { .summary-grid { grid-template-columns: 1fr; } .stat-grid { grid-template-columns: 1fr 1fr; } } </style> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/min/moment.min.js"></script> </head> <body> <div class="container"> """); // Header html.append(""" <div class="header"> <h1 style="margin: 0; font-size: 2.2em;">🚀 GC Log Analysis Report</h1> <p style="margin: 10px 0 0 0; font-size: 1.1em; opacity: 0.9;"> Generated on: """ + java.time.LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + """ | Source: """ + escapeHtml(result.getLog().getSource()) + """ </p> </div> """); // Executive Summary html.append(generateExecutiveSummary(result)); // Key Metrics html.append(generateKeyMetrics(result)); // Issues html.append(generateIssuesSection(result)); // GC Statistics html.append(generateGCStatistics(result)); // Pause Time Analysis html.append(generatePauseTimeAnalysis(result)); // Memory Analysis html.append(generateMemoryAnalysis(result)); // Throughput Analysis html.append(generateThroughputAnalysis(result)); // Recommendations html.append(generateRecommendations(result)); html.append(""" </div> <script> """); // JavaScript for charts html.append(generateChartsJavaScript(result)); html.append(""" </script> </body> </html> """); return html.toString(); } private String generateExecutiveSummary(AnalysisResult result) { StringBuilder html = new StringBuilder(); GCStatistics stats = result.getBasicStatistics(); html.append(""" <div class="section"> <h2 class="section-title">📊 Executive Summary</h2> <div class="summary-grid"> <div> <h3 style="color: #2c3e50; margin-top: 0;">Performance Overview</h3> <div class="stat-grid"> """); // Key metrics html.append(createStatCard("Total GC Events", String.valueOf(stats.getTotalEvents()), "#3498db")); html.append(createStatCard("Throughput", String.format("%.1f%%", result.getThroughput()), result.getThroughput() > 95 ? "#2ecc71" : "#e74c3c")); html.append(createStatCard("Avg Pause Time", String.format("%.3fs", stats.getAveragePauseTime()), stats.getAveragePauseTime() < 0.1 ? "#2ecc71" : "#e74c3c")); html.append(createStatCard("Max Pause Time", String.format("%.3fs", stats.getMaxGCPause()), stats.getMaxGCPause() < 1.0 ? "#2ecc71" : "#e74c3c")); html.append(""" </div> </div> <div> <h3 style="color: #2c3e50; margin-top: 0;">GC Distribution</h3> """); // GC type distribution html.append(""" <div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);"> <div class="chart-container"> <canvas id="gcDistributionChart"></canvas> </div> </div> """); html.append(""" </div> </div> </div> """); return html.toString(); } private String generateKeyMetrics(AnalysisResult result) { GCStatistics stats = result.getBasicStatistics(); StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">📈 Key Metrics</h2> <div class="stat-grid"> """); html.append(createStatCard("Young GC Count", String.valueOf(stats.getYoungGCCount()), "#3498db")); html.append(createStatCard("Full GC Count", String.valueOf(stats.getFullGCCount()), stats.getFullGCCount() == 0 ? "#2ecc71" : "#e74c3c")); html.append(createStatCard("Young GC Rate", String.format("%.2f/s", result.getYoungGCRate()), result.getYoungGCRate() < 1.0 ? "#2ecc71" : "#e74c3c")); html.append(createStatCard("Total GC Time", String.format("%.1fs", stats.getTotalDuration()), "#3498db")); html.append(createStatCard("Observation Time", String.format("%.1fs", stats.getTotalObservationTime()), "#3498db")); html.append(createStatCard("Allocation Rate", String.format("%.1f MB/s", result.getAllocationRate()), result.getAllocationRate() < 200 ? "#2ecc71" : "#e74c3c")); html.append(""" </div> </div> """); return html.toString(); } private String generateIssuesSection(AnalysisResult result) { if (!result.hasIssues()) { return """ <div class="section"> <h2 class="section-title">✅ No Issues Detected</h2> <div style="text-align: center; padding: 40px; color: #27ae60;"> <h3 style="margin: 0;">Great! No significant GC issues detected.</h3> <p>The GC configuration appears to be well-tuned for the current workload.</p> </div> </div> """; } StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">⚠️ Detected Issues</h2> <p><strong>Found """ + result.getIssueCount() + """ issues that may require attention:</strong></p> """); for (String issue : result.getIssuesBySeverity()) { String issueClass = getIssueClass(issue); html.append(""" <div class="issue """ + issueClass + """"> """ + escapeHtml(issue) + """ </div> """); } html.append("</div>"); return html.toString(); } private String generateGCStatistics(AnalysisResult result) { GCStatistics stats = result.getBasicStatistics(); StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">🔍 Detailed GC Statistics</h2> <div class="chart-container"> <canvas id="pauseTimeChart"></canvas> </div> <table> <thead> <tr> <th>Metric</th> <th>Value</th> <th>Status</th> </tr> </thead> <tbody> """); // Add statistics rows addTableRow(html, "Total GC Events", String.valueOf(stats.getTotalEvents()), "info"); addTableRow(html, "Young GC Events", String.valueOf(stats.getYoungGCCount()), "info"); addTableRow(html, "Full GC Events", String.valueOf(stats.getFullGCCount()), stats.getFullGCCount() == 0 ? "success" : "danger"); addTableRow(html, "Total GC Time", String.format("%.3f seconds", stats.getTotalDuration()), "info"); addTableRow(html, "Average Pause", String.format("%.3f seconds", stats.getAveragePauseTime()), stats.getAveragePauseTime() < 0.1 ? "success" : "warning"); addTableRow(html, "Maximum Pause", String.format("%.3f seconds", stats.getMaxGCPause()), stats.getMaxGCPause() < 1.0 ? "success" : "danger"); addTableRow(html, "Throughput", String.format("%.1f%%", result.getThroughput()), result.getThroughput() > 95 ? "success" : "warning"); // Percentiles if (result.getPauseTimePercentiles() != null) { for (Map.Entry<String, Double> entry : result.getPauseTimePercentiles().entrySet()) { addTableRow(html, entry.getKey() + " Percentile", String.format("%.3f seconds", entry.getValue()), "info"); } } html.append(""" </tbody> </table> </div> """); return html.toString(); } private String generatePauseTimeAnalysis(AnalysisResult result) { StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">⏱️ Pause Time Analysis</h2> <div class="chart-container"> <canvas id="pauseDistributionChart"></canvas> </div> """); if (result.getLongPauses() != null && !result.getLongPauses().isEmpty()) { html.append(""" <h3>Long Pauses (> 1.0s)</h3> <table> <thead> <tr> <th>Timestamp</th> <th>GC Type</th> <th>Duration</th> <th>Freed Memory</th> </tr> </thead> <tbody> """); for (GCEvent pause : result.getLongPauses()) { html.append(String.format(""" <tr> <td>%s</td> <td>%s</td> <td>%.3fs</td> <td>%s</td> </tr> """, pause.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_TIME), pause.getGcName(), pause.getDuration(), formatBytes(pause.getFreedMemory()) )); } html.append(""" </tbody> </table> """); } html.append("</div>"); return html.toString(); } private String generateMemoryAnalysis(AnalysisResult result) { StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">💾 Memory Analysis</h2> <div class="chart-container"> <canvas id="memoryUsageChart"></canvas> </div> <div class="stat-grid"> """); html.append(createStatCard("Avg Heap Usage", String.format("%.1f%%", result.getAvgHeapUsage()), result.getAvgHeapUsage() < 70 ? "#2ecc71" : "#e74c3c")); html.append(createStatCard("Memory Efficiency", String.format("%.1f MB/s", result.getMemoryEfficiency()), result.getMemoryEfficiency() > 100 ? "#2ecc71" : "#e74c3c")); html.append(createStatCard("Total Freed", formatBytes((long) result.getBasicStatistics().getTotalFreedMemory()), "#3498db")); html.append(""" </div> </div> """); return html.toString(); } private String generateThroughputAnalysis(AnalysisResult result) { StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">⚡ Throughput Analysis</h2> <div style="text-align: center; margin: 20px 0;"> <div style="display: inline-block; width: 200px; height: 200px;"> <canvas id="throughputGauge"></canvas> </div> </div> <div class="progress-bar"> <div class="progress-fill" style="width: """ + Math.min(100, result.getThroughput()) + """%;"></div> </div> <div style="text-align: center; color: #7f8c8d; font-size: 14px;"> Application Throughput: """ + String.format("%.1f%%", result.getThroughput()) + """ </div> </div> """); return html.toString(); } private String generateRecommendations(AnalysisResult result) { List<String> recommendations = new ArrayList<>(); GCStatistics stats = result.getBasicStatistics(); // Generate recommendations based on analysis if (result.getThroughput() < 95) { recommendations.add("Consider tuning GC parameters to improve throughput"); } if (stats.getFullGCCount() > 0) { recommendations.add("Investigate memory leaks or increase heap size to avoid Full GC"); } if (stats.getMaxGCPause() > 1.0) { recommendations.add("Long GC pauses detected. Consider using low-pause collectors like G1 or ZGC"); } if (result.getAvgHeapUsage() > 80) { recommendations.add("High heap usage. Consider increasing heap size or optimizing memory usage"); } if (result.getAvgHeapUsage() < 30) { recommendations.add("Low heap usage. Consider reducing heap size to improve memory locality"); } if (result.getAllocationRate() > 500) { recommendations.add("High allocation rate. Consider optimizing object creation patterns"); } if (recommendations.isEmpty()) { recommendations.add("Current GC configuration appears optimal for the observed workload"); } StringBuilder html = new StringBuilder(); html.append(""" <div class="section"> <h2 class="section-title">💡 Recommendations</h2> <ul style="line-height: 1.6;"> """); for (String recommendation : recommendations) { html.append("<li>").append(escapeHtml(recommendation)).append("</li>"); } html.append(""" </ul> </div> """); return html.toString(); } // Helper methods private String createStatCard(String label, String value, String color) { return String.format(""" <div class="stat-card" style="border-left-color: %s;"> <div class="stat-label">%s</div> <div class="stat-value">%s</div> </div> """, color, label, value); } private void addTableRow(StringBuilder html, String metric, String value, String badgeType) { html.append(String.format(""" <tr> <td><strong>%s</strong></td> <td>%s</td> <td><span class="badge badge-%s">%s</span></td> </tr> """, metric, value, badgeType, badgeType.toUpperCase())); } private String getIssueClass(String issue) { if (issue.contains("CRITICAL") || issue.contains("VERY_LONG")) return "issue-critical"; if (issue.contains("FULL_GC") || issue.contains("HIGH_")) return "issue-warning"; return "issue-info"; } private String formatBytes(long bytes) { if (bytes < 1024) return bytes + "B"; if (bytes < 1024 * 1024) return String.format("%.1fKB", bytes / 1024.0); if (bytes < 1024 * 1024 * 1024) return String.format("%.1fMB", bytes / (1024.0 * 1024.0)); return String.format("%.1fGB", bytes / (1024.0 * 1024.0 * 1024.0)); } private String escapeHtml(String text) { if (text == null) return ""; return text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } private String generateChartsJavaScript(AnalysisResult result) { StringBuilder js = new StringBuilder(); GCStatistics stats = result.getBasicStatistics(); // GC Distribution Chart js.append(""" var gcDistributionCtx = document.getElementById('gcDistributionChart').getContext('2d'); var gcDistributionChart = new Chart(gcDistributionCtx, { type: 'doughnut', data: { labels: ['Young GC', 'Full GC', 'Concurrent'], datasets: [{ data: [""" + stats.getYoungGCCount() + ", " + stats.getFullGCCount() + ", " + stats.getConcurrentEventsCount() + """], backgroundColor: ['#3498db', '#e74c3c', '#2ecc71'], borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' }, title: { display: true, text: 'GC Event Distribution' } } } }); """); // Pause Time Chart js.append(""" var pauseTimeCtx = document.getElementById('pauseTimeChart').getContext('2d'); var pauseTimeChart = new Chart(pauseTimeCtx, { type: 'line', data: { labels: """ + generateTimeLabels(stats) + """, datasets: [{ label: 'GC Pause Times', data: """ + generatePauseTimeData(stats) + """, borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'GC Pause Time Trend' } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Pause Time (seconds)' } } } } }); """); return js.toString(); } private String generateTimeLabels(GCStatistics stats) { // Generate time-based labels for charts List<String> labels = new ArrayList<>(); if (stats.getPauseTimes().size() > 50) { // Sample for large datasets for (int i = 0; i < 50; i++) { int index = i * stats.getPauseTimes().size() / 50; labels.add(String.valueOf(index)); } } else { for (int i = 0; i < stats.getPauseTimes().size(); i++) { labels.add(String.valueOf(i)); } } return "[" + labels.stream().map(l -> "'" + l + "'").collect(Collectors.joining(", ")) + "]"; } private String generatePauseTimeData(GCStatistics stats) { List<String> data = new ArrayList<>(); if (stats.getPauseTimes().size() > 50) { // Sample for large datasets for (int i = 0; i < 50; i++) { int index = i * stats.getPauseTimes().size() / 50; data.add(String.valueOf(stats.getPauseTimes().get(index))); } } else { for (Double pause : stats.getPauseTimes()) { data.add(String.valueOf(pause)); } } return "[" + String.join(", ", data) + "]"; } } 5. CLI Interface
package com.gcanalyzer.cli; import com.gcanalyzer.analyzer.AnalysisResult; import com.gcanalyzer.analyzer.GCLogAnalyzer; import com.gcanalyzer.model.GCLog; import com.gcanalyzer.parser.GCLogParser; import com.gcanalyzer.reporter.HTMLReportGenerator; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import java.io.File; import java.nio.file.Path; import java.util.List; import java.util.concurrent.Callable; @Command(name = "gcanalyzer", mixinStandardHelpOptions = true, version = "GC Log Analyzer 1.0", description = "Analyzes Java GC logs and generates comprehensive reports") public class CLIRunner implements Callable<Integer> { @Parameters(index = "0", description = "GC log file or directory") private File input; @Option(names = {"-o", "--output"}, description = "Output directory for reports (default: ./gc-reports)") private File outputDir = new File("./gc-reports"); @Option(names = {"--format"}, description = "Output format: ${COMPLETION-CANDIDATES} (default: HTML)") private OutputFormat format = OutputFormat.HTML; @Option(names = {"--detector"}, description = "GC detector type: ${COMPLETION-CANDIDATES} (default: AUTO)") private DetectorType detector = DetectorType.AUTO; @Option(names = {"--verbose"}, description = "Enable verbose output") private boolean verbose = false; private final GCLogParser parser = new GCLogParser(); public enum OutputFormat { HTML, TEXT, JSON } public enum DetectorType { AUTO, G1, CMS, PARALLEL, SERIAL, ZGC, SHENANDOAH } @Override public Integer call() throws Exception { if (!input.exists()) { System.err.println("Error: Input file/directory does not exist: " + input.getAbsolutePath()); return 1; } if (!outputDir.exists()) { outputDir.mkdirs(); } try { List<GCLog> logs = loadGCLogs(); if (logs.isEmpty()) { System.err.println("Error: No valid GC logs found in: " + input.getAbsolutePath()); return 1; } System.out.println("📁 Loaded " + logs.size() + " GC log file(s)"); for (GCLog log : logs) { System.out.println("\n🔍 Analyzing: " + log.getSource()); analyzeLog(log); } System.out.println("\n✅ Analysis completed. Reports saved to: " + outputDir.getAbsolutePath()); return 0; } catch (Exception e) { System.err.println("❌ Error during analysis: " + e.getMessage()); if (verbose) { e.printStackTrace(); } return 1; } } private List<GCLog> loadGCLogs() throws Exception { if (input.isFile()) { GCLog log = parser.parseFromFile(input.toPath()); return List.of(log); } else if (input.isDirectory()) { // Load all GC log files from directory File[] files = input.listFiles((dir, name) -> name.toLowerCase().endsWith(".log") || name.toLowerCase().contains("gc") || name.toLowerCase().endsWith(".txt") || name.toLowerCase().endsWith(".gclog")); if (files == null || files.length == 0) { return List.of(); } return parser.parseMultipleFiles( List.of(files).stream() .map(File::toPath) .collect(java.util.stream.Collectors.toList()) ); } return List.of(); } private void analyzeLog(GCLog log) throws Exception { // Perform analysis GCLogAnalyzer analyzer = new GCLogAnalyzer(log); AnalysisResult result = analyzer.analyze(); // Generate report based on format switch (format) { case HTML: generateHTMLReport(log, result); break; case TEXT: generateTextReport(log, result); break; case JSON: generateJSONReport(log, result); break; } // Print summary to console printConsoleSummary(result); } private void generateHTMLReport(GCLog log, AnalysisResult result) throws Exception { HTMLReportGenerator reporter = new HTMLReportGenerator(); String baseName = getBaseName(log.getSource()); Path reportDir = outputDir.toPath().resolve(baseName); reporter.generateReport(result, reportDir); } private void generateTextReport(GCLog log, AnalysisResult result) { // Implementation for text report String baseName = getBaseName(log.getSource()); Path reportFile = outputDir.toPath().resolve(baseName + "-report.txt"); try (var writer = new java.io.PrintWriter(reportFile.toFile())) { writer.println("GC Log Analysis Report"); writer.println("======================"); writer.println("Source: " + log.getSource()); writer.println("Total GC Events: " + result.getBasicStatistics().getTotalEvents()); writer.println("Throughput: " + String.format("%.1f%%", result.getThroughput())); writer.println("Issues Found: " + result.getIssueCount()); writer.println(); if (result.hasIssues()) { writer.println("Detected Issues:"); writer.println("----------------"); for (String issue : result.getIssues()) { writer.println("• " + issue); } writer.println(); } } catch (Exception e) { System.err.println("Failed to generate text report: " + e.getMessage()); } } private void generateJSONReport(GCLog log, AnalysisResult result) { // Implementation for JSON report String baseName = getBaseName(log.getSource()); Path reportFile = outputDir.toPath().resolve(baseName + "-report.json"); try { var objectMapper = new com.fasterxml.jackson.databind.ObjectMapper(); objectMapper.writerWithDefaultPrettyPrinter() .writeValue(reportFile.toFile(), result); } catch (Exception e) { System.err.println("Failed to generate JSON report: " + e.getMessage()); } } private void printConsoleSummary(AnalysisResult result) { GCStatistics stats = result.getBasicStatistics(); System.out.println("📊 Analysis Summary:"); System.out.println(" Total events: " + stats.getTotalEvents()); System.out.println(" Young GC: " + stats.getYoungGCCount()); System.out.println(" Full GC: " + stats.getFullGCCount()); System.out.println(" Throughput: " + String.format("%.1f%%", result.getThroughput())); System.out.println(" Avg pause: " + String.format("%.3fs", stats.getAveragePauseTime())); System.out.println(" Max pause: " + String.format("%.3fs", stats.getMaxGCPause())); System.out.println(" Issues: " + result.getIssueCount()); if (result.hasIssues()) { System.out.println(" ⚠️ Top issues:"); result.getIssuesBySeverity().stream() .limit(3) .forEach(issue -> System.out.println(" • " + issue)); } else { System.out.println(" ✅ No critical issues detected"); } } private String getBaseName(String source) { String name = new File(source).getName(); int dotIndex = name.lastIndexOf('.'); return dotIndex > 0 ? name.substring(0, dotIndex) : name; } public static void main(String[] args) { int exitCode = new CommandLine(new CLIRunner()).execute(args); System.exit(exitCode); } } Usage Examples
// Programmatic usage public class ExampleUsage { public static void main(String[] args) throws Exception { GCLogParser parser = new GCLogParser(); GCLog log = parser.parseFromFile(Paths.get("gc.log")); GCLogAnalyzer analyzer = new GCLogAnalyzer(log); AnalysisResult result = analyzer.analyze(); HTMLReportGenerator reporter = new HTMLReportGenerator(); reporter.generateReport(result, Paths.get("reports")); System.out.println("Analysis complete. Found " + result.getIssueCount() + " issues."); System.out.println("Throughput: " + String.format("%.1f%%", result.getThroughput())); } } // CLI Usage // java -jar gcanalyzer.jar gc.log -o ./reports --format HTML --verbose Features
✅ Comprehensive Parsing
- Support for all major GC collectors (G1, CMS, Parallel, Serial, ZGC, Shenandoah)
- Unified logging format (Java 9+)
- Classic GC logging format
- Automatic collector detection
✅ Advanced Analysis
- Throughput calculation and analysis
- Pause time statistics and percentiles
- Memory usage patterns
- GC efficiency metrics
- Allocation rate analysis
✅ Issue Detection
- Long pause detection
- Frequent GC detection
- Memory pressure identification
- Throughput issues
- Collector-specific problems
✅ Visual Reporting
- Interactive HTML reports with charts
- Executive summaries
- Detailed statistics tables
- Actionable recommendations
- Performance trends
This GC log analyzer provides enterprise-grade analysis capabilities for identifying and diagnosing garbage collection issues in Java applications, with comprehensive reporting and advanced detection algorithms.