0. 你將獲得什么
一個可嵌入任何 Spring Boot 應用的內存對象拓撲服務:訪問 /memviz.html
就能在瀏覽器看見對象圖。
支持按類/包名過濾、按對象大小高亮、點擊節點看詳情。
線上可用:默認只在你點擊“生成快照”時才工作;日常零開銷。
1. 傳統工具的痛點
jmap
+ MAT 做離線分析:強大但流程割裂、不實時,且換機/拷文件麻煩,我需要一種相對輕量的方式,適合“隨手開網頁看一眼”,能夠完成一些初步判斷。
VisualVM:不便嵌入業務,臨時接管和權限也會有顧慮。
線上需要:在服務本機直接打開網頁,快速看到對象圖,看對象引用鏈。
所以我實驗性做了這個內嵌式的內存對象拓撲圖:點按鈕 → dump → 解析 → 可視化顯示,一切在應用自己的 Web 界面里完成。
2. 架構設計:為什么選“HPROF 快照 + 在線解析”
目標
1. 全量對象、真實引用鏈
2. 無需預埋、無需重啟
3. 對線上影響可控(只在你手動觸發時才消耗)
方案
用 HotSpotDiagnosticMXBean
在線觸發堆快照(HPROF) (可選擇 live/非 live)。
采用輕量 HPROF 解析庫在應用內直接解析文件,構建nodes/links Graph JSON。
前端用 純 HTML + JS(D3 力導向圖) 渲染,支持搜索、過濾、點擊查看詳情。
解析庫:示例使用 org.gridkit.jvmtool:hprof-heap
,能直接讀 HPROF 并遍歷對象與引用,落地簡單。
3. 可運行代碼
項目結構
memviz/├─ pom.xml├─ src/main/java/com/example/memviz/│ ├─ MemvizApplication.java│ ├─ controller/MemvizController.java│ ├─ service/HeapDumpService.java│ ├─ service/HprofParseService.java│ ├─ model/GraphModel.java│ └─ util/SafeExecs.java└─ src/main/resources/static/└─ memviz.html
3.1 pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>memviz</artifactId><version>1.0.0</version><properties><java.version>17</java.version><spring-boot.version>3.3.2</spring-boot.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 輕量 HPROF 解析器(GridKit jvmtool) --><dependency><groupId>org.gridkit.jvmtool</groupId><artifactId>hprof-heap</artifactId><version>0.16</version></dependency><!-- 可選:更漂亮的 JSON(日志/調試用) --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
說明:hprof-heap
是一個開源的 HPROF 解析庫,可以實現遍歷對象 → 找到引用關系 → 生成拓撲。
3.2 入口 MemvizApplication.java
package com.example.memviz;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class MemvizApplication {public static void main(String[] args) {SpringApplication.run(MemvizApplication.class, args);}
}
3.3 模型 GraphModel.java
package com.example.memviz.model;import cn.hutool.core.util.RandomUtil;import java.util.*;public class GraphModel {public static class Node {public String id; // objectId 或 class@idpublic String label; // 類名(短)public String className; // 類名(全)public long shallowSize; // 淺表大小public String category; // JDK/第三方/業務public int instanceCount; // 該類的實例總數public String formattedSize; // 格式化的大小顯示public String packageName; // 包名public boolean isArray; // 是否為數組類型public String objectType; // 對象類型描述// private String bigString = new String(RandomUtil.randomBytes(1024 * 1024 * 10));public Node(String id, String label, String className, long shallowSize, String category) {this.id = id;this.label = label;this.className = className;this.shallowSize = shallowSize;this.category = category;}// 增強構造函數public Node(String id, String label, String className, long shallowSize, String category,int instanceCount, String formattedSize, String packageName, boolean isArray, String objectType) {this.id = id;this.label = label;this.className = className;this.shallowSize = shallowSize;this.category = category;this.instanceCount = instanceCount;this.formattedSize = formattedSize;this.packageName = packageName;this.isArray = isArray;this.objectType = objectType;}}public static class Link {public String source;public String target;public String field; // 通過哪個字段/元素引用public Link(String s, String t, String field) {this.source = s;this.target = t;this.field = field;}}// Top100類統計信息public static class TopClassStat {public String className;public String shortName;public String packageName;public String category;public int instanceCount; // 實例數量public long totalSize; // 該類所有實例的總內存(淺表大小)public String formattedTotalSize; // 格式化的總內存public long totalDeepSize; // 該類所有實例的總深度大小public String formattedTotalDeepSize; // 格式化的總深度大小public long avgSize; // 平均每個實例大小(淺表)public String formattedAvgSize; // 格式化的平均大小public long avgDeepSize; // 平均每個實例深度大小public String formattedAvgDeepSize; // 格式化的平均深度大小public int rank; // 排名public List<ClassInstance> topInstances; // 該類中內存占用最大的實例列表public TopClassStat(String className, String shortName, String packageName, String category,int instanceCount, long totalSize, String formattedTotalSize,long totalDeepSize, String formattedTotalDeepSize,long avgSize, String formattedAvgSize, long avgDeepSize, String formattedAvgDeepSize,int rank, List<ClassInstance> topInstances) {this.className = className;this.shortName = shortName;this.packageName = packageName;this.category = category;this.instanceCount = instanceCount;this.totalSize = totalSize;this.formattedTotalSize = formattedTotalSize;this.totalDeepSize = totalDeepSize;this.formattedTotalDeepSize = formattedTotalDeepSize;this.avgSize = avgSize;this.formattedAvgSize = formattedAvgSize;this.avgDeepSize = avgDeepSize;this.formattedAvgDeepSize = formattedAvgDeepSize;this.rank = rank;this.topInstances = topInstances != null ? topInstances : new ArrayList<>();}}// 類的實例信息public static class ClassInstance {public String id;public long size;public String formattedSize;public int rank; // 在該類中的排名public String packageName; // 包名public String objectType; // 對象類型public boolean isArray; // 是否數組public double sizePercentInClass; // 在該類中的內存占比public ClassInstance(String id, long size, String formattedSize, int rank, String packageName, String objectType, boolean isArray, double sizePercentInClass) {this.id = id;this.size = size;this.formattedSize = formattedSize;this.rank = rank;this.packageName = packageName;this.objectType = objectType;this.isArray = isArray;this.sizePercentInClass = sizePercentInClass;}}public List<Node> nodes = new ArrayList<>();public List<Link> links = new ArrayList<>();public List<TopClassStat> top100Classes = new ArrayList<>(); // Top100類統計列表public int totalObjects; // 總對象數public long totalMemory; // 總內存占用public String formattedTotalMemory; // 格式化的總內存
}
3.4 觸發堆快照 HeapDumpService.java
package com.example.memviz.service;import com.example.memviz.util.SafeExecs;
import org.springframework.stereotype.Service;import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;@Service
public class HeapDumpService {private static final String HOTSPOT_BEAN = "com.sun.management:type=HotSpotDiagnostic";private static final String DUMP_METHOD = "dumpHeap";private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");/*** 生成 HPROF 快照文件* @param live 是否僅包含存活對象(會觸發一次 STW)* @param dir 目錄(建議掛到獨立磁盤/大空間)* @return hprof 文件路徑*/public File dump(boolean live, File dir) throws Exception {if (!dir.exists() && !dir.mkdirs()) {throw new IllegalStateException("Cannot create dump dir: " + dir);}String name = "heap_" + LocalDateTime.now().format(FMT) + (live ? "_live" : "") + ".hprof";File out = new File(dir, name);MBeanServer server = ManagementFactory.getPlatformMBeanServer();ObjectName objName = new ObjectName(HOTSPOT_BEAN);// 防御:限制最大文件大小(環境變量控制)SafeExecs.assertDiskHasSpace(dir.toPath(), 512L * 1024 * 1024); // 至少 512MB 空間server.invoke(objName, DUMP_METHOD, new Object[]{ out.getAbsolutePath(), live },new String[]{ "java.lang.String", "boolean" });return out;}
}
使用 HotSpotDiagnosticMXBean.dumpHeap
生成 HPROF 是 HotSpot 標準做法。live=true 時會只保留可達對象(可能出現 STW);live=false 代價更小。Eclipse MAT 官方也推薦用該方式產出供分析。eclipse.dev
3.5 解析 HPROF → 構圖 HprofParseService.java
package com.example.memviz.service;import com.example.memviz.model.GraphModel;
import org.netbeans.lib.profiler.heap.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;import java.util.*;
import java.util.function.Predicate;@Service
public class HprofParseService {private static final Logger log = LoggerFactory.getLogger(HprofParseService.class);/*** 安全閾值:最多加載多少對象/邊進入圖(避免前端崩潰)* 圖上顯示Top100類,保持完整但可讀*/private static final int MAX_GRAPH_NODES = 100; // 圖上顯示的類數private static final int MAX_COLLECTION_NODES = 2000; // 收集的節點數,用于統計private static final int MAX_LINKS = 200; // 增加連線數以適應更多類/*** 性能優化參數*/private static final int BATCH_SIZE = 1000; // 批量處理大小private static final int LARGE_CLASS_THRESHOLD = 10000; // 大類閾值public GraphModel parseToGraph(java.io.File hprofFile,Predicate<String> classNameFilter,boolean collapseCollections) throws Exception {log.info("開始解析HPROF文件: {}", hprofFile.getName());// 檢查文件大小和可用內存long fileSize = hprofFile.length();Runtime runtime = Runtime.getRuntime();long maxMemory = runtime.maxMemory();long totalMemory = runtime.totalMemory();long freeMemory = runtime.freeMemory();long availableMemory = maxMemory - (totalMemory - freeMemory);log.info("HPROF文件大小: {}MB, 可用內存: {}MB",fileSize / 1024.0 / 1024.0, availableMemory / 1024.0 / 1024.0);// 如果文件太大,警告用戶并嘗試優化加載if (fileSize > availableMemory * 0.3) {log.warn("檢測到大型HPROF文件,啟用內存優化加載模式");// 強制垃圾回收,釋放更多內存System.gc();Thread.sleep(100);System.gc();}// Create heap from HPROF file with optimized settingsHeap heap = null;try {heap = HeapFactory.createHeap(hprofFile);log.info("HPROF文件加載完成");} catch (OutOfMemoryError e) {log.error("內存不足:HPROF文件過大");throw new Exception("HPROF文件過大,內存不足。請增加JVM內存參數(-Xmx)或使用較小的堆轉儲文件", e);}try {return parseHeapData(heap, classNameFilter, collapseCollections);} finally {// 在finally塊中確保釋放資源if (heap != null) {try {heap = null;System.gc();Thread.sleep(100);System.gc();log.info("已在finally塊中釋放HPROF文件引用");} catch (InterruptedException e) {log.warn("釋放文件引用時中斷: {}", e.getMessage());}}}}private GraphModel parseHeapData(Heap heap, Predicate<String> classNameFilter, boolean collapseCollections) {// 1) 收集對象(可按類名過濾)- 極速優化版本,帶內存監控List<Instance> all = new ArrayList<>(MAX_COLLECTION_NODES * 2); // 預分配適量容量log.info("開始收集對象實例,使用激進優化策略");long startTime = System.currentTimeMillis();int processedClasses = 0;int skippedEmptyClasses = 0;int memoryCheckCounter = 0;// 使用優先隊列在收集過程中就維護Top對象,避免后期排序PriorityQueue<Instance> topInstances = new PriorityQueue<>(MAX_COLLECTION_NODES * 2, Comparator.comparingLong(Instance::getSize));// 直接處理,不預掃描,使用更激進的策略for (JavaClass javaClass : heap.getAllClasses()) {String className = javaClass.getName();// 更嚴格的早期過濾 - 臨時放寬過濾條件if (classNameFilter != null && !classNameFilter.test(className)) {// 為了調試,記錄被過濾掉的重要類if (className.contains("MemvizApplication") || className.contains("String") || className.contains("byte")) {log.info("類被過濾掉: {}", className);}continue;}// 跳過明顯的系統類和空類(基于類名)- 暫時禁用以確保不漏掉重要對象/*if (isLikelySystemClass(className)) {continue;}*/// 記錄被處理的類log.debug("處理類: {}", className);// 定期檢查內存使用情況if (++memoryCheckCounter % 100 == 0) {long currentFree = Runtime.getRuntime().freeMemory();long currentTotal = Runtime.getRuntime().totalMemory();long usedMemory = currentTotal - currentFree;double usedPercent = (double) usedMemory / Runtime.getRuntime().maxMemory() * 100;if (usedPercent > 85) {log.warn("內存使用率高: {:.1f}%, 執行垃圾回收", usedPercent);System.gc();// 重新檢查currentFree = Runtime.getRuntime().freeMemory();currentTotal = Runtime.getRuntime().totalMemory();usedMemory = currentTotal - currentFree;usedPercent = (double) usedMemory / Runtime.getRuntime().maxMemory() * 100;if (usedPercent > 90) {log.error("內存使用率危險,提前停止收集");break;}}}long classStart = System.currentTimeMillis();try {// 直接獲取實例,設置超時檢查List<Instance> instances = javaClass.getInstances();int instanceCount = instances.size();if (instanceCount == 0) {skippedEmptyClasses++;continue;}// 智能采樣:使用優先隊列自動維護Top對象if (instanceCount > LARGE_CLASS_THRESHOLD) {// 超大類:激進采樣,直接加入優先隊列int sampleSize = Math.min(100, instanceCount / 10); int step = Math.max(1, instanceCount / sampleSize);for (int i = 0; i < instanceCount; i += step) {Instance inst = instances.get(i);addToTopInstances(topInstances, inst, MAX_GRAPH_NODES * 2);}log.debug("大類采樣: {}, 采樣數: {}", className, Math.min(sampleSize, instanceCount));} else {// 小類:全部加入優先隊列for (Instance inst : instances) {addToTopInstances(topInstances, inst, MAX_COLLECTION_NODES * 2);}}// 處理完大量數據后,幫助GC回收臨時對象if (instanceCount > 1000) {instances = null; // 顯式清除引用}processedClasses++;long classEnd = System.currentTimeMillis();// 只記錄耗時較長的類if (classEnd - classStart > 100) {log.debug("耗時類: {}, 實例數: {}, 耗時: {}ms, 總計: {}", className, instanceCount, (classEnd - classStart), all.size());}// 每處理一定數量的類就檢查是否應該停止if (processedClasses % 50 == 0) {long elapsed = System.currentTimeMillis() - startTime;if (elapsed > 30000) { // 30秒超時log.warn("處理時間過長,停止收集");break;}log.info("進度: {}個類, {}個實例, 耗時{}ms", processedClasses, all.size(), elapsed);}} catch (Exception e) {log.warn("處理類失敗: {}, 錯誤: {}", className, e.getMessage());continue;}}// 從優先隊列中提取所有結果用于統計List<Instance> allCollectedInstances = new ArrayList<>(topInstances);allCollectedInstances.sort(Comparator.comparingLong(Instance::getSize).reversed());// 圖顯示用的Top10對象List<Instance> graphInstances = new ArrayList<>();int graphNodeCount = Math.min(MAX_GRAPH_NODES, allCollectedInstances.size());for (int i = 0; i < graphNodeCount; i++) {graphInstances.add(allCollectedInstances.get(i));}long totalTime = System.currentTimeMillis() - startTime;log.info("收集完成: {}個類已處理, {}個空類跳過, {}個實例收集完成(圖顯示{}個), 耗時{}ms", processedClasses, skippedEmptyClasses, allCollectedInstances.size(), graphInstances.size(), totalTime);log.info("圖節點數量: {}, 統計節點數量: {}", graphInstances.size(), allCollectedInstances.size());// 3) 建立 id 映射,統計類型和數量信息,生成增強數據Map<Long, GraphModel.Node> nodeMap = new LinkedHashMap<>();Map<String, Integer> classCountMap = new HashMap<>(); // 統計每個類的實例數量GraphModel graph = new GraphModel();// 用所有收集的實例進行類統計(不僅僅是圖顯示的Top10)for (Instance obj : allCollectedInstances) {String cn = className(heap, obj);classCountMap.put(cn, classCountMap.getOrDefault(cn, 0) + 1);}// 計算總內存占用 - 使用原始數據而不是過濾后的數據long totalMemoryBeforeFilter = 0;int totalObjectsBeforeFilter = 0;// 統計所有對象(用于準確的總內存計算)for (JavaClass javaClass : heap.getAllClasses()) {String className = javaClass.getName();// 應用類名過濾器進行統計boolean passesFilter = (classNameFilter == null || classNameFilter.test(className));// 記錄重要的類信息if (className.contains("MemvizApplication") || className.contains("GraphModel")) {log.info("發現重要類: {}, 通過過濾器: {}", className, passesFilter);}if(!passesFilter){continue;}// instances 前后加耗時日志統計long start = System.currentTimeMillis();List<Instance> instances = javaClass.getInstances();long end = System.currentTimeMillis();if ((end - start) > 50) { // 只記錄耗時的調用log.info("獲取類 {} 的實例耗時: {}ms, 實例數: {}", className, (end - start), instances.size());}for (Instance instance : instances) {totalObjectsBeforeFilter++;totalMemoryBeforeFilter += instance.getSize();// 記錄大對象if (instance.getSize() > 500 * 1024) { // 大于500KB的對象log.info("發現大對象: 類={}, 大小={}, ID={}", className, formatSize(instance.getSize()), instance.getInstanceId());}}}long instanceTotalMemory = allCollectedInstances.stream().mapToLong(Instance::getSize).sum();graph.totalObjects = totalObjectsBeforeFilter; // 顯示總對象數,而不是過濾后的graph.totalMemory = totalMemoryBeforeFilter; // 顯示總內存,而不是過濾后的graph.formattedTotalMemory = formatSize(totalMemoryBeforeFilter);log.info("內存統計: 總對象數={}, 總內存={}", graph.totalObjects, graph.formattedTotalMemory);log.info("收集對象數={}, 收集內存={}, 圖中對象數={}, 圖中內存={}", allCollectedInstances.size(), formatSize(instanceTotalMemory),graphInstances.size(), formatSize(graphInstances.stream().mapToLong(Instance::getSize).sum()));// 直接從所有類創建Top100類統計列表(不依賴收集的實例,確保統計完整)List<GraphModel.TopClassStat> allClassStats = new ArrayList<>();for (JavaClass javaClass : heap.getAllClasses()) {String className = javaClass.getName();// 應用過濾條件if (classNameFilter != null && !classNameFilter.test(className)) {continue;}try {List<Instance> instances = javaClass.getInstances();int instanceCount = instances.size();// 跳過沒有實例的類if (instanceCount == 0) {continue;}// 跳過Lambda表達式生成的匿名類if (className.contains("$$Lambda") || className.contains("$Lambda")) {continue;}// 跳過其他JVM生成的內部類if (className.contains("$$EnhancerBySpringCGLIB$$") || className.contains("$$FastClassBySpringCGLIB$$") ||className.contains("$Proxy$")) {continue;}// 計算該類的總內存占用long totalSize = instances.stream().mapToLong(Instance::getSize).sum();long avgSize = totalSize / instanceCount;// 研究深度大小計算的可能性long totalDeepSize = calculateTotalDeepSize(instances);long avgDeepSize = totalDeepSize / instanceCount;// 記錄深度大小計算結果(特別是大差異的情況)if (totalDeepSize > totalSize * 2) { // 深度大小是淺表大小的2倍以上log.info("類 {} 深度大小顯著大于淺表大小: 淺表={}({}) vs 深度={}({})", className, totalSize, formatSize(totalSize), totalDeepSize, formatSize(totalDeepSize));}String displayCategory = formatCategory(categoryOf(className));String packageName = extractPackageName(className);// 創建該類的Top實例列表(按內存大小排序,最多100個)List<Instance> sortedInstances = new ArrayList<>(instances);sortedInstances.sort(Comparator.comparingLong(Instance::getSize).reversed());List<GraphModel.ClassInstance> classInstances = new ArrayList<>();for (int i = 0; i < Math.min(100, sortedInstances.size()); i++) {Instance inst = sortedInstances.get(i);String instClassName = className(heap, inst);String instPackageName = extractPackageName(instClassName);String objectType = determineObjectType(instClassName);boolean isArray = instClassName.contains("[");// 計算該實例在該類中的內存占比double sizePercent = totalSize > 0 ? (double) inst.getSize() / totalSize * 100.0 : 0.0;GraphModel.ClassInstance classInstance = new GraphModel.ClassInstance(String.valueOf(inst.getInstanceId()),inst.getSize(),formatSize(inst.getSize()),i + 1,instPackageName,objectType,isArray,sizePercent);classInstances.add(classInstance);}GraphModel.TopClassStat stat = new GraphModel.TopClassStat(className,shortName(className),packageName,displayCategory,instanceCount,totalSize,formatSize(totalSize),totalDeepSize, // 新增:深度大小formatSize(totalDeepSize), // 新增:格式化的深度大小avgSize,formatSize(avgSize),avgDeepSize, // 新增:平均深度大小formatSize(avgDeepSize), // 新增:格式化的平均深度大小0,classInstances);allClassStats.add(stat);} catch (Exception e) {log.warn("處理類{}時出錯: {}", className, e.getMessage());}}// 按總內存占用排序并設置排名allClassStats.sort(Comparator.comparingLong((GraphModel.TopClassStat s) -> s.totalSize).reversed());for (int i = 0; i < Math.min(100, allClassStats.size()); i++) {allClassStats.get(i).rank = i + 1;graph.top100Classes.add(allClassStats.get(i));}log.info("類統計完成: 共{}個類符合過濾條件,Top100類已生成", allClassStats.size());// 用Top100類統計數據創建圖顯示用的類節點// 按總內存大小排序,取Top100用于圖顯示List<GraphModel.TopClassStat> topClassesForGraph = new ArrayList<>(allClassStats);topClassesForGraph.sort(Comparator.comparingLong((GraphModel.TopClassStat s) -> s.totalSize).reversed());// 為圖顯示的Top100類創建節點int graphClassCount = Math.min(MAX_GRAPH_NODES, topClassesForGraph.size());for (int i = 0; i < graphClassCount; i++) {GraphModel.TopClassStat classStat = topClassesForGraph.get(i);String cn = classStat.className;// 創建類級別的節點,顯示類的聚合信息String enhancedLabel = String.format("%s (%d個實例, %s, %s)", classStat.shortName, classStat.instanceCount, classStat.formattedTotalSize, classStat.category);GraphModel.Node n = new GraphModel.Node("class_" + cn.hashCode(), // 使用類名hash作為節點IDenhancedLabel,cn,classStat.totalSize,classStat.category,classStat.instanceCount,classStat.formattedTotalSize,classStat.packageName,cn.contains("["),determineObjectType(cn));nodeMap.put((long)cn.hashCode(), n); // 用類名hash作為keygraph.nodes.add(n);}// 4) 建立類級別的引用邊(基于堆中真實的對象引用關系)log.info("開始建立類級別引用邊,圖類數: {}", graphClassCount);int linkCount = 0;int potentialLinks = 0;// 分析類之間的引用關系 - 只基于堆中真實的對象引用Map<String, Set<String>> classReferences = new HashMap<>();for (Instance obj : allCollectedInstances) {String sourceClassName = className(heap, obj);for (FieldValue fieldValue : obj.getFieldValues()) {potentialLinks++;if (fieldValue instanceof ObjectFieldValue) {ObjectFieldValue objFieldValue = (ObjectFieldValue) fieldValue;Instance target = objFieldValue.getInstance();if (target != null) {String targetClassName = className(heap, target);// 避免自引用,也避免Lambda和代理類的連線if (!sourceClassName.equals(targetClassName) && !isGeneratedClass(targetClassName) && !isGeneratedClass(sourceClassName)) {classReferences.computeIfAbsent(sourceClassName, k -> new HashSet<>()).add(targetClassName);}}}}}log.info("檢測到類引用關系: {}", classReferences.size());// 為圖中顯示的類創建連線for (int i = 0; i < graphClassCount && linkCount < MAX_LINKS; i++) {String sourceClass = topClassesForGraph.get(i).className;Set<String> targets = classReferences.get(sourceClass);if (targets != null) {for (String targetClass : targets) {// 檢查目標類是否也在圖顯示范圍內boolean targetInGraph = topClassesForGraph.stream().limit(graphClassCount).anyMatch(stat -> stat.className.equals(targetClass));if (targetInGraph) {String sourceId = "class_" + sourceClass.hashCode();String targetId = "class_" + targetClass.hashCode();// 添加更詳細的連線信息String linkLabel = "引用";graph.links.add(new GraphModel.Link(sourceId, targetId, linkLabel));linkCount++;if (linkCount >= MAX_LINKS) {log.info("達到最大連線數限制: {}", MAX_LINKS);break;}}}}}log.info("連線建立完成: 處理了{}個潛在連線,實際創建{}個連線", potentialLinks, linkCount);// 5) 可選:把大型集合折疊為"聚合節點",減少噪音if (collapseCollections) {log.info("開始折疊集合類型節點");collapseCollectionLikeNodes(graph);}log.info("圖構建完成: {}個節點, {}個鏈接", graph.nodes.size(), graph.links.size());return graph;}private static String className(Heap heap, Instance instance) {return instance.getJavaClass().getName();}private static String shortName(String fqcn) {int p = fqcn.lastIndexOf('.');return p >= 0 ? fqcn.substring(p + 1) : fqcn;}private static String categoryOf(String fqcn) {if (fqcn.startsWith("java.") || fqcn.startsWith("javax.") || fqcn.startsWith("jdk.")) return "JDK";if (fqcn.startsWith("org.") || fqcn.startsWith("com.")) return "3rd";return "app";}/*** 格式化字節大小,讓顯示更直觀*/private static String formatSize(long sizeInBytes) {if (sizeInBytes < 1024) {return sizeInBytes + "B";} else if (sizeInBytes < 1024 * 1024) {return String.format("%.1fKB", sizeInBytes / 1024.0);} else if (sizeInBytes < 1024 * 1024 * 1024) {return String.format("%.2fMB", sizeInBytes / (1024.0 * 1024));} else {return String.format("%.2fGB", sizeInBytes / (1024.0 * 1024 * 1024));}}/*** 格式化類別名稱,讓顯示更直觀*/private static String formatCategory(String category) {switch (category) {case "JDK":return "JDK類";case "3rd":return "第三方";case "app":return "業務代碼";default:return "未知";}}/*** 提取包名*/private static String extractPackageName(String className) {int lastDot = className.lastIndexOf('.');if (lastDot > 0) {return className.substring(0, lastDot);}return "默認包";}/*** 確定對象類型*/private static String determineObjectType(String className) {if (className.contains("[")) {return "數組";} else if (className.contains("$")) {if (className.contains("Lambda")) {return "Lambda表達式";} else {return "內部類";}} else if (className.startsWith("java.util.") && (className.contains("List") || className.contains("Set") || className.contains("Map"))) {return "集合類";} else if (className.startsWith("java.lang.")) {return "基礎類型";} else {return "普通類";}}/*** 向優先隊列添加實例,自動維護Top-N*/private void addToTopInstances(PriorityQueue<Instance> topInstances, Instance instance, int maxSize) {if (topInstances.size() < maxSize) {topInstances.offer(instance);} else if (instance.getSize() > topInstances.peek().getSize()) {topInstances.poll();topInstances.offer(instance);}}/*** 快速選擇Top-N最大的對象,避免全排序的性能問題*/private List<Instance> quickSelectTopN(List<Instance> instances, int n) {if (instances.size() <= n) {return instances;}// 使用優先隊列(小頂堆)來維護Top-NPriorityQueue<Instance> topN = new PriorityQueue<>(Comparator.comparingLong(Instance::getSize));int processed = 0;for (Instance instance : instances) {if (topN.size() < n) {topN.offer(instance);} else if (instance.getSize() > topN.peek().getSize()) {topN.poll();topN.offer(instance);}// 每處理10000個對象記錄一次進度if (++processed % 10000 == 0) {log.debug("快速選擇進度: {}/{}", processed, instances.size());}}// 將結果轉換為List并按大小降序排序List<Instance> result = new ArrayList<>(topN);result.sort(Comparator.comparingLong(Instance::getSize).reversed());log.info("快速選擇完成,從{}個對象中選出{}個最大對象", instances.size(), result.size());return result;}private static boolean isLikelySystemClass(String className) {// 跳過一些已知很慢或不重要的類return className.startsWith("java.lang.Class") ||className.startsWith("java.lang.String") ||className.startsWith("java.lang.Object[]") ||className.startsWith("java.util.concurrent") ||className.contains("$$Lambda") ||className.contains("$Proxy") ||className.startsWith("sun.") ||className.startsWith("jdk.internal.") ||className.endsWith("[][]") || // 多維數組通常很慢className.contains("reflect.Method") ||className.contains("reflect.Field");//return false;}/*** 集合折疊策略:將集合類型的多個元素聚合顯示*/private void collapseCollectionLikeNodes(GraphModel graph) {Map<String, Integer> collectionElementCount = new HashMap<>();Set<String> collectionNodeIds = new HashSet<>();Set<GraphModel.Link> linksToRemove = new HashSet<>();Map<String, GraphModel.Link> collectionLinks = new HashMap<>();// 1. 識別集合類型的節點for (GraphModel.Node node : graph.nodes) {if (isCollectionType(node.className)) {collectionNodeIds.add(node.id);}}// 2. 統計每個集合的元素數量,并準備聚合連線for (GraphModel.Link link : graph.links) {if (collectionNodeIds.contains(link.source)) {// 這是從集合指向元素的連線String collectionId = link.source;collectionElementCount.put(collectionId, collectionElementCount.getOrDefault(collectionId, 0) + 1);linksToRemove.add(link);// 保留一條代表性連線,用于顯示聚合信息String key = collectionId + "->elements";if (!collectionLinks.containsKey(key)) {GraphModel.Node sourceNode = graph.nodes.stream().filter(n -> n.id.equals(collectionId)).findFirst().orElse(null);if (sourceNode != null) {collectionLinks.put(key, new GraphModel.Link(collectionId, "collapsed_" + collectionId, collectionElementCount.get(collectionId) + "個元素"));}}}}// 3. 移除原始的集合元素連線graph.links.removeAll(linksToRemove);// 4. 更新集合節點的顯示信息for (GraphModel.Node node : graph.nodes) {if (collectionNodeIds.contains(node.id)) {int elementCount = collectionElementCount.getOrDefault(node.id, 0);if (elementCount > 0) {// 更新節點標簽,顯示元素數量String originalLabel = node.label;node.label = String.format("%s [%d個元素]", originalLabel.split("\(")[0].trim(), elementCount);// 添加聚合信息到對象類型node.objectType = node.objectType + " (已折疊)";}}}// 5. 移除被折疊的元素節點(可選,這里保留以維持圖的完整性)// 實際應用中可以選擇性移除孤立的元素節點log.info("集合折疊完成: {}個集合被處理", collectionElementCount.size());}/*** 計算一組實例的總深度大小*/private long calculateTotalDeepSize(List<Instance> instances) {long totalDeepSize = 0;Set<Long> globalVisited = new HashSet<>(); // 全局訪問記錄,避免重復計算共享對象for (Instance instance : instances) {totalDeepSize += calculateDeepSize(instance, globalVisited, 0, 5); // 最大遞歸深度5}return totalDeepSize;}/*** 遞歸計算單個對象的深度大小* @param obj 要計算的對象* @param visited 已訪問的對象ID集合,防止循環引用* @param depth 當前遞歸深度* @param maxDepth 最大遞歸深度限制* @return 深度大小(包含所有引用對象)*/private long calculateDeepSize(Instance obj, Set<Long> visited, int depth, int maxDepth) {if (obj == null || depth >= maxDepth) {return 0;}long objId = obj.getInstanceId();if (visited.contains(objId)) {return 0; // 已經計算過,避免重復}visited.add(objId);long totalSize = obj.getSize(); // 從淺表大小開始try {// 遍歷所有對象字段for (FieldValue fieldValue : obj.getFieldValues()) {if (fieldValue instanceof ObjectFieldValue) {ObjectFieldValue objFieldValue = (ObjectFieldValue) fieldValue;Instance referencedObj = objFieldValue.getInstance();if (referencedObj != null) {// 遞歸計算引用對象的大小totalSize += calculateDeepSize(referencedObj, visited, depth + 1, maxDepth);}}}} catch (Exception e) {// 如果訪問字段失敗,記錄日志但繼續log.debug("計算深度大小時訪問對象字段失敗: {}, 對象類型: {}", e.getMessage(), obj.getJavaClass().getName());}return totalSize;}/*** 判斷是否為JVM生成的類(Lambda、CGLIB代理等)*/private static boolean isGeneratedClass(String className) {return className.contains("$$Lambda") || className.contains("$Lambda") ||className.contains("$$EnhancerBySpringCGLIB$$") ||className.contains("$$FastClassBySpringCGLIB$$") ||className.contains("$Proxy$") ||className.contains("$$SpringCGLIB$$");}/*** 判斷是否為集合類型*/private boolean isCollectionType(String className) {return className.contains("ArrayList") || className.contains("LinkedList") ||className.contains("HashMap") ||className.contains("LinkedHashMap") ||className.contains("TreeMap") ||className.contains("HashSet") ||className.contains("LinkedHashSet") ||className.contains("TreeSet") ||className.contains("Vector") ||className.contains("Stack") ||className.contains("ConcurrentHashMap");}
}
注:hprof-heap
的 API 能遍歷對象實例、淺表大小、以及字段引用。對超大堆你一定要限制 N,并提供過濾條件,否則前端渲染會頂不住。
3.6 控制器 MemvizController.java
package com.example.memviz.controller;import com.example.memviz.model.GraphModel;
import com.example.memviz.service.HeapDumpService;
import com.example.memviz.service.HprofParseService;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;import java.io.File;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Predicate;@RestController
@RequestMapping("/memviz")
public class MemvizController {private final HeapDumpService dumpService;private final HprofParseService parseService;public MemvizController(HeapDumpService dumpService, HprofParseService parseService) {this.dumpService = dumpService;this.parseService = parseService;}/** 觸發一次快照,返回文件名(安全:默認 live=false) */@PostMapping("/snapshot")public Map<String, String> snapshot(@RequestParam(defaultValue = "false") boolean live,@RequestParam(defaultValue = "/tmp/memviz") String dir) throws Exception {File f = dumpService.dump(live, new File(dir));return Map.of("file", f.getAbsolutePath());}/** 解析指定快照 → 圖模型(支持過濾&折疊) */@GetMapping(value = "/graph", produces = MediaType.APPLICATION_JSON_VALUE)public GraphModel graph(@RequestParam String file,@RequestParam(required = false) String include, // 例如: com.myapp.,java.util.HashMap@RequestParam(defaultValue = "true") boolean collapseCollections) throws Exception {Predicate<String> filter = null;if (StringUtils.hasText(include)) {String[] prefixes = include.split(",");filter = fqcn -> {for (String p : prefixes) if (fqcn.startsWith(p.trim())) return true;return false;};}return parseService.parseToGraph(new File(file), filter, collapseCollections);}
}
3.7 防御工具 SafeExecs.java
package com.example.memviz.util;import java.io.IOException;
import java.nio.file.*;public class SafeExecs {public static void assertDiskHasSpace(Path dir, long minFreeBytes) throws IOException {FileStore store = Files.getFileStore(dir);if (store.getUsableSpace() < minFreeBytes) {throw new IllegalStateException("Low disk space for heap dump: need " + minFreeBytes + " bytes free");}}
}
3.8 純前端頁面 src/main/resources/static/memviz.html
說明:純 HTML + JS。提供文件選擇/生成、過濾條件、力導向圖、節點詳情面板、大小/類別著色等交互。
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>JVM 內存對象拓撲圖</title><meta name="viewport" content="width=device-width, initial-scale=1" /><style>body { margin:0; font:14px/1.4 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial; background:#0b0f14; color:#e6edf3; }header { padding:12px 16px; display:flex; gap:12px; align-items:center; position:sticky; top:0; background:#0b0f14; border-bottom:1px solid #1f2937; z-index:10;}header input, header select, header button { padding:6px 10px; background:#111827; color:#e6edf3; border:1px solid #374151; border-radius:8px; }header button { cursor:pointer; }#panel { width:400px; position:fixed; right:10px; top:70px; bottom:10px; background:#0f172a; border:1px solid #1f2937; border-radius:12px; padding:10px; overflow:auto; }#graph { position:absolute; left:0; top:56px; right:420px; bottom:0; }.legend { display:flex; gap:8px; align-items:center; }.pill { display:inline-block; padding:2px 8px; border-radius:999px; border:1px solid #334155; }.muted { color:#9ca3af; }.tab-buttons { display:flex; gap:4px; margin-bottom:12px; }.tab-btn { padding:6px 12px; background:#1f2937; border:1px solid #374151; border-radius:6px; cursor:pointer; font-size:12px; }.tab-btn.active { background:#3b82f6; color:white; }.tab-content { display:none; }.tab-content.active { display:block; }.top-item { padding:4px 8px; margin:2px 0; background:#1f2937; border-radius:4px; cursor:pointer; font-size:12px; }.top-item:hover { background:#374151; }.top-rank { display:inline-block; width:20px; color:#6b7280; }.stat-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-bottom:12px; }.stat-item { padding:6px; background:#1f2937; border-radius:4px; text-align:center; }.stat-value { font-weight:bold; color:#3b82f6; }.detail-row { display:flex; justify-content:space-between; margin:4px 0; padding:2px 0; }.detail-label { color:#9ca3af; }.detail-value { font-weight:bold; }.loading { position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:#1f2937; padding:20px; border-radius:8px; border:1px solid #374151; z-index:1000; display:none; }.loading-spinner { width:40px; height:40px; border:3px solid #374151; border-top:3px solid #3b82f6; border-radius:50%; animation:spin 1s linear infinite; margin:0 auto 10px; }@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }.loading-overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:999; display:none; }/* svg */svg { width:100%; height:100%; }.link { stroke:#6b7280; stroke-opacity:0.45; }.node circle { stroke:#111827; stroke-width:1; cursor:grab; }.node text { fill:#d1d5db; font-size:12px; pointer-events:none; }.highlight { stroke:#f59e0b !important; stroke-width:2.5 !important; }</style>
</head>
<body><header><strong>MemViz</strong><button id="btnSnap">生成快照</button><label>HPROF 文件</label><input id="file" size="50" placeholder="/tmp/memviz/heap_*.hprof" /><label>類過濾</label><input id="include" size="30" placeholder="com.myapp.,java.util." value="com.example" /><label>折疊集合</label><select id="collapse" title="將ArrayList、HashMap等集合類型的多個元素聚合顯示,減少圖的復雜度"><option value="true">是 (推薦)</option><option value="false">否</option></select><button id="btnLoad">加載圖</button><span class="muted">提示:先“生成快照”,再“加載圖”</span>
</header><div id="graph"></div>
<aside id="panel"><div class="stat-grid"><div class="stat-item"><div class="stat-value" id="totalObjects">-</div><div class="muted">總對象數</div></div><div class="stat-item"><div class="stat-value" id="totalMemory">-</div><div class="muted">總內存</div></div><div class="stat-item"><div class="stat-value" id="graphObjects">-</div><div class="muted">圖中對象</div></div><div class="stat-item"><div class="stat-value" id="graphMemory">-</div><div class="muted">圖中內存</div></div></div><div class="tab-buttons"><div class="tab-btn active" onclick="switchTab('detail')">對象詳情</div><div class="tab-btn" onclick="switchTab('top100')">Top100類</div><div class="tab-btn" onclick="switchTab('instances')" style="display:none;">類實例</div></div><div id="tab-detail" class="tab-content active"><h3>對象詳情</h3><div id="info" class="muted">點擊節點查看詳細信息</div><hr style="border-color:#374151; margin:12px 0;"/><div class="legend"><span class="pill" style="background:#1f2937">圖例說明</span><span class="muted">節點大小=內存占用;顏色=代碼類別</span></div></div><div id="tab-top100" class="tab-content"><h3>內存占用Top100類</h3><div id="top100-list" class="muted">加載圖后顯示</div></div><div id="tab-instances" class="tab-content"><div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;"><h3 id="instances-title">類實例列表</h3><button onclick="switchTab('top100')" style="padding:4px 8px; background:#374151; border:1px solid #4b5563; border-radius:4px; color:#e6edf3; font-size:11px; cursor:pointer;">返回</button></div><div style="font-size:11px; color:#9ca3af; margin-bottom:8px; padding:4px 8px; background:#1f2937; border-radius:4px;">💡 顯示該類中內存占用最大的實例,右側數字表示:實例大小 / 在該類中的占比</div><div id="instances-list" class="muted">選擇一個類查看其實例</div></div>
</aside><!-- Loading 提示 -->
<div id="loading-overlay" class="loading-overlay"></div>
<div id="loading" class="loading"><div class="loading-spinner"></div><div style="text-align:center; color:#e6edf3;">正在解析HPROF文件...</div>
</div><script>const qs = s => document.querySelector(s);const btnSnap = qs('#btnSnap');const btnLoad = qs('#btnLoad');let currentData = null;// Loading控制函數function showLoading() {qs('#loading-overlay').style.display = 'block';qs('#loading').style.display = 'block';}function hideLoading() {qs('#loading-overlay').style.display = 'none';qs('#loading').style.display = 'none';}// 標簽頁切換function switchTab(tabName) {// 更新按鈕狀態document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));document.querySelector(`.tab-btn[onclick="switchTab('${tabName}')"]`).classList.add('active');// 更新內容顯示document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));document.getElementById(`tab-${tabName}`).classList.add('active');}btnSnap.onclick = async () => {const resp = await fetch('/memviz/snapshot', { method:'POST' });const data = await resp.json();qs('#file').value = data.file;alert('快照完成:' + data.file);};btnLoad.onclick = async () => {const file = qs('#file').value.trim();if (!file) return alert('請填寫 HPROF 文件路徑');try {showLoading();const include = encodeURIComponent(qs('#include').value.trim());const collapse = qs('#collapse').value;const url = `/memviz/graph?file=${encodeURIComponent(file)}&include=${include}&collapseCollections=${collapse}`;const data = await fetch(url).then(r => r.json());currentData = data;renderGraph(data);updateStats(data);renderTop100(data);// 重置界面狀態resetUIState();} catch (error) {alert('加載失敗: ' + error.message);} finally {hideLoading();}};function resetUIState() {// 隱藏實例標簽頁const instancesTab = document.querySelector('.tab-btn[onclick="switchTab('instances')"]');instancesTab.style.display = 'none';// 切換回詳情標簽頁if (document.getElementById('tab-instances').classList.contains('active')) {switchTab('detail');}}function updateStats(data) {qs('#totalObjects').textContent = data.totalObjects || 0;qs('#totalMemory').textContent = data.formattedTotalMemory || '0B';// 計算圖中的統計信息const graphObjects = data.nodes ? data.nodes.length : 0;const graphMemoryBytes = data.nodes ? data.nodes.reduce((sum, node) => sum + (node.shallowSize || 0), 0) : 0;const graphMemoryFormatted = formatBytes(graphMemoryBytes);qs('#graphObjects').textContent = graphObjects;qs('#graphMemory').textContent = graphMemoryFormatted;}function renderTop100(data) {const container = qs('#top100-list');if (!data.top100Classes || data.top100Classes.length === 0) {container.innerHTML = '<div class="muted">暫無數據</div>';return;}const html = data.top100Classes.map(classStat => `<div class="top-item" onclick="selectClassByName('${classStat.className}')"><span class="top-rank">#${classStat.rank}</span><strong>${classStat.shortName}</strong><div style="font-size:11px; color:#9ca3af;">${classStat.instanceCount}個實例 | 淺表: ${classStat.formattedTotalSize} | 深度: ${classStat.formattedTotalDeepSize || classStat.formattedTotalSize}</div><div style="font-size:10px; color:#6b7280;">平均淺表: ${classStat.formattedAvgSize} | 平均深度: ${classStat.formattedAvgDeepSize || classStat.formattedAvgSize}</div><div style="font-size:10px; color:#6b7280;">${classStat.category} | ${classStat.packageName}</div></div>`).join('');container.innerHTML = html;}function selectClassByName(className) {if (!currentData) return;// 找到該類的統計信息const classStat = currentData.top100Classes.find(c => c.className === className);if (!classStat) return;// 顯示該類的實例列表showClassInstances(classStat);// 找到該類的所有節點并高亮const classNodes = currentData.nodes.filter(n => n.className === className);if (classNodes.length > 0) {// 顯示第一個節點的信息(代表這個類)showInfo(classNodes[0]);// 在SVG中高亮所有該類的節點const svgNodes = document.querySelectorAll('.node');svgNodes.forEach(n => n.querySelector('circle').classList.remove('highlight'));classNodes.forEach(nodeData => {const targetNode = Array.from(svgNodes).find(n => {const svgNodeData = d3.select(n).datum();return svgNodeData && svgNodeData.id === nodeData.id;});if (targetNode) {targetNode.querySelector('circle').classList.add('highlight');}});}}function showClassInstances(classStat) {// 顯示實例標簽頁按鈕const instancesTab = document.querySelector('.tab-btn[onclick="switchTab('instances')"]');instancesTab.style.display = 'block';// 切換到實例標簽頁switchTab('instances');// 更新標題qs('#instances-title').textContent = `${classStat.shortName} (${classStat.instanceCount}個實例)`;// 渲染實例列表const container = qs('#instances-list');if (!classStat.topInstances || classStat.topInstances.length === 0) {container.innerHTML = '<div class="muted">該類暫無實例數據</div>';return;}const html = classStat.topInstances.map(instance => `<div class="top-item" onclick="selectInstanceById('${instance.id}')"><div style="display:flex; justify-content:space-between; align-items:center;"><div><span class="top-rank">#${instance.rank}</span><strong>對象@${instance.id.slice(-8)}</strong><div style="font-size:9px; color:#6b7280; margin-top:1px;">ID: ${instance.id}</div></div><div style="text-align:right;"><div style="font-weight:bold; color:#3b82f6;">${instance.formattedSize}</div><div style="font-size:10px; color:#9ca3af;">${instance.sizePercentInClass.toFixed(1)}%</div></div></div><div style="font-size:11px; color:#9ca3af; margin-top:4px;">${instance.objectType}${instance.isArray ? ' (數組)' : ''} | ${instance.packageName}</div></div>`).join('');container.innerHTML = html;}function selectInstanceById(instanceId) {if (!currentData) return;// 找到對應的節點const node = currentData.nodes.find(n => n.id === instanceId);if (node) {// 顯示詳情信息showInfo(node);// 在SVG中高亮該節點const svgNodes = document.querySelectorAll('.node');svgNodes.forEach(n => n.querySelector('circle').classList.remove('highlight'));const targetNode = Array.from(svgNodes).find(n => {const nodeData = d3.select(n).datum();return nodeData && nodeData.id === instanceId;});if (targetNode) {targetNode.querySelector('circle').classList.add('highlight');}// 切換到詳情標簽頁顯示具體信息switchTab('detail');}}function renderGraph(data) {const root = qs('#graph');root.innerHTML = '';const rect = root.getBoundingClientRect();const width = rect.width || window.innerWidth - 440;const height = window.innerHeight - 60;const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');root.appendChild(svg);// 顏色映射 - 更新顏色策略const color = (cat) => {if (cat === 'JDK類') return '#60a5fa';if (cat === '第三方') return '#a78bfa'; if (cat === '業務代碼') return '#34d399';return '#6b7280';};// 力導向const nodes = data.nodes.map(d => Object.assign({}, d));const links = data.links.map(l => Object.assign({}, l));const sim = d3.forceSimulation(nodes).force('link', d3.forceLink(links).id(d => d.id).distance(80).strength(0.4)).force('charge', d3.forceManyBody().strength(-120)).force('center', d3.forceCenter(width/2, height/2));// zoom/panconst g = d3.select(svg).append('g');d3.select(svg).call(d3.zoom().scaleExtent([0.1, 4]).on('zoom', (ev) => g.attr('transform', ev.transform)));const link = g.selectAll('.link').data(links).enter().append('line').attr('class', 'link');const node = g.selectAll('.node').data(nodes).enter().append('g').attr('class','node').call(d3.drag().on('start', (ev, d) => { if (!ev.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }).on('drag', (ev, d) => { d.fx = ev.x; d.fy = ev.y; }).on('end', (ev, d) => { if (!ev.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }));node.append('circle').attr('r', d => Math.max(5, Math.min(30, Math.sqrt(d.shallowSize)))).attr('fill', d => color(d.category)).on('click', (ev, d) => showInfo(d));node.append('text').attr('dy', -10).attr('text-anchor','middle').text(d => d.label); // 顯示完整標簽信息sim.on('tick', () => {link.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);node.attr('transform', d => `translate(${d.x},${d.y})`);});function showInfo(d) {// 清除之前的高亮document.querySelectorAll('.node circle').forEach(circle => circle.classList.remove('highlight'));// 添加當前高亮event.target.classList.add('highlight');qs('#info').innerHTML = `<div class="detail-row"><span class="detail-label">對象ID:</span><span class="detail-value">${d.id}</span></div><div class="detail-row"><span class="detail-label">類名:</span><span class="detail-value">${d.className}</span></div><div class="detail-row"><span class="detail-label">內存大小:</span><span class="detail-value">${d.formattedSize || formatBytes(d.shallowSize)}</span></div><div class="detail-row"><span class="detail-label">實例數量:</span><span class="detail-value">${d.instanceCount}個</span></div><div class="detail-row"><span class="detail-label">代碼類型:</span><span class="detail-value">${d.category}</span></div><div class="detail-row"><span class="detail-label">包名:</span><span class="detail-value">${d.packageName || '未知'}</span></div><div class="detail-row"><span class="detail-label">對象類型:</span><span class="detail-value">${d.objectType || '普通類'}${d.isArray ? ' (數組)' : ''}</span></div><hr style="border-color:#374151; margin:8px 0;"/><div class="muted" style="font-size:11px;">提示:連線上的字段信息可通過鼠標懸停查看</div>`;}// 給每條邊加上 title(字段名)g.selectAll('.link').append('title').text(l => l.field || '');}function formatBytes(bytes) {if (bytes < 1024) return bytes + 'B';if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + 'KB';if (bytes < 1024*1024*1024) return (bytes/(1024*1024)).toFixed(2) + 'MB';return (bytes/(1024*1024*1024)).toFixed(2) + 'GB';}
</script>
<!-- 僅本頁使用:D3 from CDN -->
<script src="https://d3js.org/d3.v7.min.js"></script>
</body>
</html>
4. 使用指南(線上可用的“安全姿勢”)
1. 默認不開銷:頁面只是個靜態資源;只有在你點擊「生成快照」時,JVM 才會 dump。
2. 限制大小:HprofParseService
里 MAX_NODES/MAX_LINKS
,避免前端卡死;用 include
參數過濾包前綴,目標更聚焦。
3. 磁盤與權限:把 /tmp/memviz
換成你線上的大磁盤目錄;SafeExecs.assertDiskHasSpace
防炸盤。
4. 鑒權:對 /memviz/**
增加登錄/白名單 IP 校驗;生產不要裸露。
5. 壓測:先在預發/灰度環境跑一遍,確認 dump 時間、解析耗時(通常幾十 MB~幾百 MB 的 HPROF 在幾秒~十幾秒級)。
5. 實戰:如何用它定位內存問題
第一步:線上卡頓/內存飆升 → 打開 /memviz.html
→ 生成快照。
第二步:加載圖 → 先把 include
定位到你業務包,如 com.myapp.
;觀察大節點、強連通。
第三步:點擊節點看類名 → 根據連線查看引用關系。
第四步:必要時擴大過濾范圍或關閉“折疊集合”,看更細的對象鏈。
第五步:修復后再 dump 一次,對比圖譜變化。
6. 進階:Live-Sampling 實時方案(給想更“炫”的你)
如果你要更實時的效果,可以考慮:
JVMTI/Agent + IterateThroughHeap:可真正遍歷堆與引用,并打上對象 tag,做增量圖更新。但需要 native agent 與更復雜部署。
JFR(Java Flight Recorder) :低開銷采集對象分配事件(非全量),在前端做采樣級拓撲與趨勢。
混合模式:平時跑 JFR 采樣展示“熱對象網絡”,當有疑似泄漏時,一鍵切換到 Snapshot 做全量證據。
如果你計劃把本文工具演進為“線上常駐監控”,Live-Sampling 作為常態,Snapshot 作為取證,是個很穩的組合。
7. 性能 & 安全評估
Dump 成本:live=true
會觸發 STW,通常在百毫秒~數秒(取決于堆大小/活躍度);不緊急時優先 live=false
。
解析成本:同一進程內解析 HPROF 會額外占用內存;建議限制節點數,或把解析放到獨立服務(把 HPROF 傳過去解析再回結果)。
安全合規:HPROF 含敏感對象內容;務必開啟鑒權、按需權限控制;生成后自動清理舊文件(可加定時任務清理 3 天前的快照)。
可觀測性:為 dump/parse 過程打埋點(耗時、文件大小、節點/邊數量),避免工具本身成為黑盒。
8. 常見問題(FAQ)
Q:為什么不直接用 MAT?
A:MAT 非常強大(推薦用來做深度溯源),但不嵌入你的業務系統、鏈路跳轉不順手。本文方案是輕量內嵌,適合“隨手開網頁看一眼”。
Q:HPROF 解析庫為何選 GridKit?
A:org.gridkit.jvmtool:hprof-heap
輕量、API 簡單,非常適合做在線可視化的快速集成。
Q:能否在不 dump 的情況下拿到“所有對象”?
A:純 Java 層做全堆枚舉不可行;需要 JVMTI 層的 IterateThroughHeap
或類似能力(需要 agent)。這就需要 Live-Sampling 路線,相對復雜。
9. 結語:把艱深的內存分析,變成一張圖
這套方案把“重流程”的內存排查,壓縮成兩步:生成快照 → 在線出圖。當前實現還比較粗糙,不適合大面積進行分析, 適合局部鎖定小范圍定向分析,可作為基礎原型DEMO參考。
它不是取代 MAT,而是提供了一種“嵌入式、輕交互、隨手查”的輕量解決方案作為一種補充手段。
https://github.com/yuboon/java-examples/tree/master/springboot-memviz