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.