精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

JVM 透視神器:把“對象拓撲圖”塞進你的 Spring Boot

開發 開發工具
把 JVM 內存從“黑箱”變成“可視化地圖”,不是為了替代 MAT,而是為了把“隨手看一眼”做到極致:零侵入、線上可用、一鍵生成、就地可視化。

內存問題向來詭譎:明明 GC 在跑,堆卻在漲;明明寫得很純凈,偏偏對象不釋放。經典工具(jmap + MAT、VisualVM)都很能打,但流程要么割裂、要么難以嵌入,線上臨時接管還會牽扯權限與風險。

很多時候我們想要的其實很樸素:在服務本機打開一個網頁,點一下按鈕,就能看到 JVM 內部的“對象世界地圖”——哪些類占得多、誰指著誰、引用鏈大體走向如何,能快速做初步判斷。

于是,這個方案就誕生了:在 Spring Boot 里嵌一個“內存對象拓撲服務”。只要訪問 /memviz.html 就能在瀏覽器里看到對象圖;支持類/包過濾、按對象大小高亮、點擊看詳情;默認零開銷,只有你點擊“生成快照”時才工作,足夠線上可用。

你將獲得什么

  • 一個可嵌入任何 Spring Boot 的“內存對象拓撲服務”

訪問:/memviz.html

功能:按類/包名過濾、按對象大小高亮、點擊節點看詳情

  • 線上可用、日常零成本:默認不做任何事,只有你點“生成快照”才會 dump→解析→可視化
  • 真實引用鏈:基于 HPROF 堆快照解析出的對象級別/類級別關系

為什么不用傳統工具直接上?

  • jmap + MAT:強,但離線、割裂、跨機拷文件麻煩;用來“深挖”很好,用來“隨手看一眼”太重。
  • VisualVM:不便嵌入業務,線上權限和安全邊界是剛需考量。
  • 線上需求更簡單:在服務本機開網頁 → 一鍵 dump → 立刻在本頁面可視化 → 初步定位。

我們的方案就是:點按鈕 → dump → 解析 → D3 可視化,全都在應用自己的 Web 界面完成。

架構設計:為什么是“HPROF 快照 + 在線解析”

目標:

  • 全量對象 + 真實引用鏈
  • 無需預埋、無需重啟
  • 只在手動觸發時才消耗資源

方案:

  • 用 HotSpotDiagnosticMXBean 在線觸發 堆快照(HPROF),可選 live=true/false
  • 在應用內使用輕量解析庫解析 HPROF,構建 nodes/links 的 Graph JSON
  • 前端用 純 HTML + JS + D3 力導向圖渲染;支持搜索、過濾、點擊詳情、大小高亮

解析庫示例使用 org.gridkit.jvmtool:hprof-heap。由于社區里很多人也使用 org.netbeans.lib.profiler.heap API(下文示例采用該風格),如果你的依賴環境不同,可按需替換為同等能力的 HPROF 解析實現。

可運行代碼

項目結構

memviz/
 ├─ pom.xml
 ├─ src/main/java/com/icoderoad/memviz/
 │   ├─ MemvizApplication.java
 │   ├─ controller/MemvizController.java
 │   ├─ service/HeapDumpService.java
 │   ├─ service/HprofParseService.java
 │   ├─ model/GraphModel.java
 │   └─ util/SafeExecs.java
 └─ src/main/resources/static/
     └─ memviz.html

注:示例中保留 hprof-heap 與 jackson-databind。如果你使用 org.netbeans.lib.profiler.heap API,請根據你的制品庫加上相應依賴(不同公司/倉庫坐標可能不同)。下方示例僅展示一個常見組合,真實項目請按你環境調整。

<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.icoderoad</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>
    <!-- Spring Web -->
    <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>


    <!-- 如需使用 org.netbeans.lib.profiler.heap API,請按你的倉庫坐標引入
    <dependency>
      <groupId>org.netbeans.lib.profiler</groupId>
      <artifactId>profiler</artifactId>
      <version>YOUR_VERSION</version>
    </dependency>
    -->
  </dependencies>


  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

應用入口 MemvizApplication.java

package com.icoderoad.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);
  }
}

領域模型 GraphModel.java

package com.icoderoad.memviz.model;


import java.util.*;


public class GraphModel {
    public static class Node {
        public String id;             // objectId 或 class@id
        public 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;     // 對象類型


        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, label, className, shallowSize, 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<>();
    public int totalObjects;
    public long totalMemory;
    public String formattedTotalMemory;
}

觸發堆快照 HeapDumpService.java

package com.icoderoad.memviz.service;


import com.icoderoad.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  目錄(建議掛到獨立磁盤/大空間)
   */
  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);


    server.invoke(objName, DUMP_METHOD,
        new Object[]{ out.getAbsolutePath(), live },
        new String[]{ "java.lang.String", "boolean" });
    return out;
  }
}

解析 HPROF → 生成圖 HprofParseService.java

下方示例采用 org.netbeans.lib.profiler.heap.* 風格的 API(你可替換為等價的 HPROF 解析實現)。核心邏輯:

  • 收集實例(可按類名過濾)
  • 統計 Top 類與大小
  • 生成類級節點與引用邊(限制數量防止前端崩潰)
  • 可選折疊集合類型節點,降低噪點
package com.icoderoad.memviz.service;


import com.icoderoad.memviz.model.GraphModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;


// 如用 NetBeans Heap API:import org.netbeans.lib.profiler.heap.*;
// 如你改用 GridKit hprof-heap,請替換為其 API 并保持同等語義
// 這里僅保留方法簽名與邏輯說明


import java.io.File;
import java.util.*;
import java.util.function.Predicate;


@Service
public class HprofParseService {
    private static final Logger log = LoggerFactory.getLogger(HprofParseService.class);


    private static final int MAX_GRAPH_NODES = 100;     // 圖上顯示的類數
    private static final int MAX_LINKS = 200;           // 連線上限(防止前端卡頓)


    public GraphModel parseToGraph(File hprofFile,
                                   Predicate<String> classNameFilter,
                                   boolean collapseCollections) throws Exception {
        // ===== 這里替換為你的 HPROF 解析實現 =====
        // 偽代碼結構(請按你選用的庫實現):
        //
        // Heap heap = HeapFactory.createHeap(hprofFile);
        // List<JavaClass> classes = heap.getAllClasses();
        // 按過濾器收集統計,并創建 GraphModel
        //
        // 下面給出一個結構化的“參考實現骨架”(偽代碼 + 注釋)


        GraphModel graph = new GraphModel();


        // 1) 假設我們拿到了所有類與其實例,這里只演示構圖策略:
        //    - 統計每個類的總 shallow size、實例數
        //    - 取 Top100 類作為可視化節點
        //    - 基于對象引用關系推導類之間的引用邊(受限于 MAX_LINKS)


        // ====== demo 構造一些假數據結構,實際請用解析結果填充 ======
        Map<String, ClassStatAgg> agg = new HashMap<>();
        // ... 從堆中迭代對象、按類名聚合 totalSize/instanceCount
        // ... 并記錄類間引用關系 refEdges: Map<String, Set<String>>


        // 模擬聚合結果(請刪除)
        agg.put("java.util.HashMap", new ClassStatAgg("java.util.HashMap", "JDK", "java.util", 1234, 120_000_000L));
        agg.put("com.icoderoad.demo.User", new ClassStatAgg("com.icoderoad.demo.User", "業務代碼", "com.icoderoad.demo", 20000, 80_000_000L));
        agg.put("org.slf4j.Logger", new ClassStatAgg("org.slf4j.Logger", "第三方", "org.slf4j", 3000, 30_000_000L));


        List<ClassStatAgg> top = new ArrayList<>(agg.values());
        top.sort(Comparator.comparingLong((ClassStatAgg s) -> s.totalSize).reversed());


        int nodeCount = Math.min(MAX_GRAPH_NODES, top.size());
        for (int i = 0; i < nodeCount; i++) {
            ClassStatAgg s = top.get(i);
            String label = String.format("%s (%d個實例, %s)", shortName(s.className), s.instanceCount, formatSize(s.totalSize));
            GraphModel.Node n = new GraphModel.Node(
                    "class_" + s.className.hashCode(),
                    label,
                    s.className,
                    s.totalSize,
                    s.category,
                    s.instanceCount,
                    formatSize(s.totalSize),
                    s.packageName,
                    s.className.contains("["),
                    determineObjectType(s.className)
            );
            graph.nodes.add(n);
        }


        // 模擬類間引用邊(實際根據對象引用聚合成類引用)
        int links = 0;
        for (int i = 0; i < Math.min(3, nodeCount); i++) {
            for (int j = i + 1; j < Math.min(6, nodeCount); j++) {
                if (links >= MAX_LINKS) break;
                GraphModel.Node a = graph.nodes.get(i);
                GraphModel.Node b = graph.nodes.get(j);
                graph.links.add(new GraphModel.Link(a.id, b.id, "引用"));
                links++;
            }
        }


        // 匯總統計(總對象數、總內存)——實際按解析結果填入
        graph.totalObjects = 123456;
        graph.totalMemory = 120_000_000L + 80_000_000L + 30_000_000L;
        graph.formattedTotalMemory = formatSize(graph.totalMemory);


        return graph;
    }


    // === 下方是一些工具方法 + 演示用內部聚合類 ===
    private static class ClassStatAgg {
        String className;
        String category;    // JDK / 第三方 / 業務代碼
        String packageName;
        int instanceCount;
        long totalSize;
        ClassStatAgg(String c, String category, String pkg, int cnt, long size) {
            this.className = c;
            this.category = category;
            this.packageName = pkg;
            this.instanceCount = cnt;
            this.totalSize = size;
        }
    }


    private static String shortName(String fqcn) {
        int p = fqcn.lastIndexOf('.');
        return p >= 0 ? fqcn.substring(p + 1) : fqcn;
    }
    private static String determineObjectType(String className) {
        if (className.contains("[")) return "數組";
        if (className.contains("$")) return className.contains("Lambda") ? "Lambda表達式" : "內部類";
        if (className.startsWith("java.util.") &&
                (className.contains("List") || className.contains("Set") || className.contains("Map"))) return "集合類";
        if (className.startsWith("java.lang.")) return "基礎類型";
        return "普通類";
    }
    private static String formatSize(long sizeInBytes) {
        if (sizeInBytes < 1024) return sizeInBytes + "B";
        if (sizeInBytes < 1024 * 1024) return String.format("%.1fKB", sizeInBytes / 1024.0);
        if (sizeInBytes < 1024L * 1024 * 1024) return String.format("%.2fMB", sizeInBytes / (1024.0 * 1024));
        return String.format("%.2fGB", sizeInBytes / (1024.0 * 1024 * 1024));
    }
}

說明:為便于你快速集成,我保留了“完整可視化側 API 結構與字段定義”。你只需把 parseToGraph 中標注的“偽實現”替換為真實 HPROF 解析(無論 GridKit 還是 NetBeans Heap API),按注釋填入對象、類與引用關系即可。

安全工具 SafeExecs.java

package com.icoderoad.memviz.util;


import java.io.IOException;
import java.nio.file.*;


public class SafeExecs {
    public static void assertDiskHasSpace(Path dir, long minBytes) throws IOException {
        FileStore store = Files.getFileStore(dir);
        long usable = store.getUsableSpace();
        if (usable < minBytes) {
            throw new IllegalStateException("Disk space low: need " + minBytes + " bytes, but only " + usable + " usable");
        }
    }
}

控制器 MemvizController.java

前端將調用以下 REST 接口:

  • POST /api/memviz/snapshot?live=true&filter=xxx&collapse=false:一鍵 dump + 解析 + 返回圖
  • GET  /memviz.html:靜態頁面(在 resources/static 下,Spring Boot 會自動映射)
package com.icoderoad.memviz.controller;


import com.icoderoad.memviz.model.GraphModel;
import com.icoderoad.memviz.service.HeapDumpService;
import com.icoderoad.memviz.service.HprofParseService;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;


import java.io.File;
import java.util.function.Predicate;


@RestController
@RequestMapping("/api/memviz")
public class MemvizController {


    private final HeapDumpService dumpService;
    private final HprofParseService parseService;


    public MemvizController(HeapDumpService dumpService, HprofParseService parseService) {
        this.dumpService = dumpService;
        this.parseService = parseService;
    }


    @Value("${memviz.dump.dir:./heap-dumps}")
    private String dumpDir;


    /**
     * 一鍵快照(dump + parse)
     */
    @PostMapping(value = "/snapshot", produces = MediaType.APPLICATION_JSON_VALUE)
    public GraphModel snapshot(@RequestParam(defaultValue = "true") boolean live,
                               @RequestParam(required = false) String filter,
                               @RequestParam(defaultValue = "false") boolean collapse) throws Exception {
        File dir = new File(dumpDir);
        File hprof = dumpService.dump(live, dir);


        Predicate<String> classFilter = (filter == null || filter.isBlank())
                ? null
                : (name -> name.contains(filter));


        return parseService.parseToGraph(hprof, classFilter, collapse);
    }
}

前端頁面 src/main/resources/static/memviz.html(D3.js 力導向圖)

功能點:

  • 觸發快照(live 開關)
  • 類/包名過濾(后端過濾)
  • 節點按大小映射半徑,并可基于“最小顯示大小閾值”進行前端過濾
  • 顏色按類別(JDK/第三方/業務)區分
  • 搜索定位、點擊彈出詳情面板
  • 懸停高亮相鄰節點與邊
  • 自適應窗口大小、Zoom/Pan
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0"
/>
<title>MemViz - JVM 對象拓撲圖</title>
<style>
  body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
  header { display: flex; gap: 12px; align-items: center; padding: 10px 14px; box-shadow: 0 1px 8px rgba(0,0,0,0.06); position: sticky; top: 0; background: #fff; z-index: 10; }
  header .title { font-weight: 700; }
  header .meta { margin-left: auto; font-size: 12px; color: #666; }
  .toolbar input[type="text"] { padding: 6px 10px; border: 1px solid #ddd; border-radius: 8px; width: 220px; }
  .toolbar label { font-size: 13px; color: #333; }
  .toolbar button { padding: 8px 12px; border: 0; border-radius: 10px; box-shadow: 0 4px 14px rgba(0,0,0,0.08); cursor: pointer; }
  .toolbar button.primary { background: #2563eb; color: #fff; }
  .toolbar button.ghost { background: #f9fafb; color: #111827; }
  .container { display: grid; grid-template-columns: 1fr 320px; height: calc(100vh - 60px); }
  .graph { background: #fff; }
  .side { border-left: 1px solid #eee; padding: 12px; overflow: auto; }
  .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
  .badge.jdk { background: #e0f2fe; color: #075985; }
  .badge.third { background: #ecfccb; color: #3f6212; }
  .badge.app { background: #fee2e2; color: #991b1b; }
  .legend { display:flex; gap: 8px; align-items:center; font-size: 12px; color:#444; }
  .legend .dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
  .legend .dot.jdk { background:#60a5fa; }
  .legend .dot.third { background:#86efac; }
  .legend .dot.app { background:#fca5a5; }
  .hint { font-size: 12px; color: #666; }
  .kv { font-size: 13px; line-height: 1.6; }
  .footer { font-size: 12px; color: #999; padding: 8px 0; }
  .slider-row { display:flex; align-items:center; gap:10px; font-size:12px; color:#333; }
  .slider-row input[type="range"] { width: 160px; }


  /* link/node styles */
  .link { stroke: #aaa; stroke-opacity: 0.6; }
  .link.highlight { stroke: #111827; stroke-width: 2.2px; }
  .node { cursor: pointer; stroke: #fff; stroke-width: 1.2px; }
  .node.highlight { stroke: #111827; stroke-width: 2px; }
  .label { pointer-events:none; user-select:none; font-size: 11px; fill:#374151; }
  .overlay { fill: none; pointer-events: all; }
</style>
</head>
<body>
<header>
  <div class="title">MemViz - JVM 內存對象拓撲圖</div>
  <div class="toolbar">
    <label>過濾(類名/包名包含):</label>
    <input id="filterInput" type="text" placeholder="例如:com.icoderoad 或 HashMap" />
    <label style="margin-left:10px;"><input id="liveChk" type="checkbox" checked /> 只保留存活對象(live)</label>
    <label style="margin-left:10px;"><input id="collapseChk" type="checkbox" /> 折疊集合節點</label>
    <button id="snapshotBtn" class="primary">生成快照并可視化</button>
    <button id="resetBtn" class="ghost">重置視圖</button>
  </div>
  <div class="meta">
    <span id="metaTotal">總對象數:--</span> | 
    <span id="metaMem">總內存:--</span>
  </div>
</header>


<div class="container">
  <div class="graph" id="graph"></div>
  <aside class="side">
    <div class="legend" style="margin-bottom:10px;">
      <span class="dot jdk"></span>JDK
      <span class="dot third" style="margin-left:10px;"></span>第三方
      <span class="dot app" style="margin-left:10px;"></span>業務代碼
    </div>


    <div class="slider-row" style="margin:8px 0 16px;">
      <span>大小閾值(最小顯示):</span>
      <input type="range" id="sizeSlider" min="0" max="10" step="1" value="0" />
      <span id="sizeLabel">0 MB</span>
    </div>


    <div class="kv">
      <h3>節點詳情</h3>
      <div id="detail">點擊圖中的節點查看詳情</div>
    </div>


    <div class="footer">
      提示:拖拽節點可重新布局,滾輪縮放,拖拽空白處平移。
    </div>
  </aside>
</div>


<!-- D3 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js" integrity="sha512-F4mJFwS8cZBqOqV5v4FQXn3y0H4PUq9y4hfy0zCPI5F+uJXb8M9W1Qy7uWz2wOqX8kMsQh3mZ1jWwAZV2TTJYg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
(() => {
  const graphEl = document.getElementById('graph');
  const detailEl = document.getElementById('detail');
  const snapshotBtn = document.getElementById('snapshotBtn');
  const resetBtn = document.getElementById('resetBtn');
  const filterInput = document.getElementById('filterInput');
  const liveChk = document.getElementById('liveChk');
  const collapseChk = document.getElementById('collapseChk');
  const metaTotal = document.getElementById('metaTotal');
  const metaMem = document.getElementById('metaMem');
  const sizeSlider = document.getElementById('sizeSlider');
  const sizeLabel = document.getElementById('sizeLabel');


  let width = graphEl.clientWidth || window.innerWidth - 320;
  let height = graphEl.clientHeight || (window.innerHeight - 60);


  const svg = d3.select('#graph').append('svg')
    .attr('width', width)
    .attr('height', height);


  const overlay = svg.append('rect')
    .attr('class','overlay')
    .attr('width', width)
    .attr('height', height)
    .call(d3.zoom().scaleExtent([0.1, 5]).on('zoom', (e) => g.attr('transform', e.transform)));


  const g = svg.append('g');


  const linkGroup = g.append('g').attr('class','links');
  const nodeGroup = g.append('g').attr('class','nodes');
  const labelGroup = g.append('g').attr('class','labels');


  window.addEventListener('resize', () => {
    width = graphEl.clientWidth || window.innerWidth - 320;
    height = graphEl.clientHeight || (window.innerHeight - 60);
    svg.attr('width', width).attr('height', height);
    overlay.attr('width', width).attr('height', height);
    if (simulation) {
      simulation.force('center', d3.forceCenter(width/2, height/2)).alpha(0.3).restart();
    }
  });


  let simulation = null;
  let rawData = null;
  let filtered = { nodes: [], links: [] };


  // 顏色映射
  function colorByCategory(cat) {
    if (!cat) return '#9ca3af';
    if (cat.includes('JDK')) return '#60a5fa';
    if (cat.includes('第三方') || cat.includes('3rd')) return '#86efac';
    return '#fca5a5'; // 業務
  }


  // 半徑映射(按淺表大小)
  function radiusBySize(size) {
    const base = 4;
    if (!size || size <= 0) return base;
    return Math.max(base, Math.sqrt(size) / 300); // 調整系數可微調
  }


  // 閾值過濾(MB)
  function applyThreshold(data, minMB) {
    const minBytes = minMB * 1024 * 1024;
    const nodes = data.nodes.filter(n => (n.shallowSize || 0) >= minBytes);
    const nodeIds = new Set(nodes.map(n => n.id));
    const links = data.links.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
    return { nodes, links };
  }


  // 渲染圖
  function render(data) {
    // link
    const link = linkGroup.selectAll('line').data(data.links, d => d.source + '->' + d.target);
    link.exit().remove();
    const linkEnter = link.enter().append('line').attr('class', 'link');
    const linkSel = linkEnter.merge(link);


    // node
    const node = nodeGroup.selectAll('circle').data(data.nodes, d => d.id);
    node.exit().remove();
    const nodeEnter = node.enter().append('circle')
      .attr('class','node')
      .attr('r', d => radiusBySize(d.shallowSize))
      .attr('fill', d => colorByCategory(d.category))
      .call(drag(simulation))
      .on('click', (_, d) => showDetail(d))
      .on('mouseover', (_, d) => highlightNeighbors(d, true))
      .on('mouseout',  (_, d) => highlightNeighbors(d, false));
    const nodeSel = nodeEnter.merge(node)
      .attr('r', d => radiusBySize(d.shallowSize))
      .attr('fill', d => colorByCategory(d.category));


    // label
    const label = labelGroup.selectAll('text').data(data.nodes, d => 'label_'+d.id);
    label.exit().remove();
    const labelEnter = label.enter().append('text')
      .attr('class','label')
      .text(d => d.label || d.className || d.id);
    const labelSel = labelEnter.merge(label);


    // simulation
    if (simulation) simulation.stop();


    simulation = d3.forceSimulation(data.nodes)
      .force('link', d3.forceLink(data.links).id(d => d.id).distance(80).strength(0.2))
      .force('charge', d3.forceManyBody().strength(-120))
      .force('center', d3.forceCenter(width/2, height/2))
      .force('collision', d3.forceCollide().radius(d => radiusBySize(d.shallowSize) + 6))
      .on('tick', () => {
        linkSel
          .attr('x1', d => getNode(d.source).x)
          .attr('y1', d => getNode(d.source).y)
          .attr('x2', d => getNode(d.target).x)
          .attr('y2', d => getNode(d.target).y);
        nodeSel
          .attr('cx', d => d.x = clamp(d.x, 0, width))
          .attr('cy', d => d.y = clamp(d.y, 0, height));
        labelSel
          .attr('x', d => d.x + 6)
          .attr('y', d => d.y - 6);
      });
  }


  function getNode(n) { return n.id ? n : (rawIndex.get(n) || n); }
  function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }


  function drag(sim) {
    function dragstarted(e, d) {
      if (!e.active) sim.alphaTarget(0.3).restart();
      d.fx = d.x; d.fy = d.y;
    }
    function dragged(e, d) {
      d.fx = e.x; d.fy = e.y;
    }
    function dragended(e, d) {
      if (!e.active) sim.alphaTarget(0);
      d.fx = null; d.fy = null;
    }
    return d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
  }


  function showDetail(d) {
    const badge = (c) => {
      if (!c) return '';
      if (c.includes('JDK')) return '<span class="badge jdk">JDK</span>';
      if (c.includes('第三方') || c.includes('3rd')) return '<span class="badge third">第三方</span>';
      return '<span class="badge app">業務</span>';
    };
    detailEl.innerHTML = `
      <div style="font-weight:700;font-size:14px;margin-bottom:6px;">${d.className || d.label || d.id}</div>
      <div>${badge(d.category)} <span style="color:#666;">${d.packageName || ''}</span></div>
      <ul style="padding-left:18px;line-height:1.6;margin:8px 0;">
        <li>實例總數:${fmt(d.instanceCount)}</li>
        <li>淺表大小:${d.formattedSize || (d.shallowSize + ' B')}</li>
        <li>對象類型:${d.objectType || '--'}</li>
        <li>數組:${d.isArray ? '是' : '否'}</li>
        <li>節點ID:${d.id}</li>
      </ul>
      <div class="hint">提示:將節點拖到合適位置,可調整局部布局;再次點擊可查看其它節點。</div>
    `;
  }
  function fmt(v){ return (v===undefined||v===null) ? '--' : v; }


  // 懸停高亮鄰居
  function highlightNeighbors(node, on) {
    const neighbors = new Set();
    filtered.links.forEach(l => {
      if (l.source === node.id || l.source?.id === node.id) neighbors.add(l.target.id || l.target);
      if (l.target === node.id || l.target?.id === node.id) neighbors.add(l.source.id || l.source);
    });
    nodeGroup.selectAll('circle').classed('highlight', d => on && (d.id === node.id || neighbors.has(d.id)));
    linkGroup.selectAll('line').classed('highlight', d => {
      const s = d.source.id || d.source;
      const t = d.target.id || d.target;
      return on && (s === node.id || t === node.id);
    });
  }


  // 數據索引(用于 link 坐標快速訪問)
  let rawIndex = new Map();


  // 生成快照
  snapshotBtn.addEventListener('click', async () => {
    snapshotBtn.disabled = true; snapshotBtn.textContent = '生成中...';
    try {
      const params = new URLSearchParams();
      params.set('live', String(liveChk.checked));
      if (filterInput.value.trim()) params.set('filter', filterInput.value.trim());
      params.set('collapse', String(collapseChk.checked));


      const resp = await fetch('/api/memviz/snapshot', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: params.toString()
      });
      if (!resp.ok) throw new Error('請求失敗:' + resp.status);
      const data = await resp.json();


      rawData = data;
      rawIndex = new Map(rawData.nodes.map(n => [n.id, n]));
      metaTotal.textContent = '總對象數:' + (data.totalObjects || '--');
      metaMem.textContent = '總內存:' + (data.formattedTotalMemory || '--');


      const minMB = Number(sizeSlider.value || 0);
      sizeLabel.textContent = minMB + ' MB';
      filtered = applyThreshold(rawData, minMB);
      render(filtered);
    } catch (err) {
      alert(err.message || err);
    } finally {
      snapshotBtn.disabled = false; snapshotBtn.textContent = '生成快照并可視化';
    }
  });


  // 尺寸閾值滑條
  sizeSlider.addEventListener('input', () => {
    const mb = Number(sizeSlider.value || 0);
    sizeLabel.textContent = mb + ' MB';
    if (!rawData) return;
    filtered = applyThreshold(rawData, mb);
    render(filtered);
  });


  // 重置視圖(清空高亮、復位縮放)
  resetBtn.addEventListener('click', () => {
    nodeGroup.selectAll('circle').classed('highlight', false);
    linkGroup.selectAll('line').classed('highlight', false);
    svg.transition().duration(300).call(
      d3.zoom().transform, d3.zoomIdentity
    );
  });
})();
</script>
</body>
</html>

使用說明

  1. 啟動應用后,打開:http://localhost:8080/memviz.html
  2. 輸入過濾關鍵詞(可選):如 com.icoderoad、HashMap
  3. 勾選 live(可選):只保留存活對象(會觸發一次 STW)
  4. 勾選“折疊集合節點”(可選):減少集合類噪點
  5. 點擊“生成快照并可視化”
  6. 調整“大小閾值(MB)”過濾細碎節點,聚焦大戶
  7. 點擊節點查看詳情;懸停可高亮鄰居;拖拽/縮放調整視圖

實戰建議

  • 存儲目錄:通過 memviz.dump.dir 指定到大空間磁盤,避免和業務日志盤共振。
  • 權限:確保進程用戶對 dump 目錄有寫權限。
  • 風險隔離:live=true 會觸發 STW,建議僅在業務低峰操作。
  • 限流與鑒權:對 /api/memviz/snapshot 加鑒權/防抖(如僅內網訪問/管理端 Token)。
  • 拓展:

a.快照對比(兩次 HPROF 的 Top 類變化)

b.可疑集合 Drilldown(Map/Set 大 key 列表)

c.泄漏鏈線索(Dominators / Retained Set,按庫能力擴展)

結論

把 JVM 內存從“黑箱”變成“可視化地圖”,不是為了替代 MAT,而是為了把“隨手看一眼”做到極致:零侵入、線上可用、一鍵生成、就地可視化。 當你需要深挖時,依舊可以導出 HPROF 到專業工具;但在日常排障里,這個嵌入式拓撲圖能讓你以更低的成本定位“誰多、誰大、誰指著誰”,大幅縮短問題收斂時間。

責任編輯:武曉燕 來源: 路條編程
相關推薦

2019-09-05 11:14:12

監控系統拓撲圖

2009-06-22 17:15:50

Java Applet拓撲圖

2021-02-01 09:13:34

Zabbix5.2拓撲圖運維

2019-07-03 10:16:11

網絡監控拓撲圖

2016-09-29 09:33:06

Html5站點地圖拓撲圖

2020-11-09 14:03:51

Spring BootMaven遷移

2020-11-10 09:19:23

Spring BootJava開發

2020-02-26 15:35:17

Spring Boot項目優化JVM調優

2023-07-27 08:53:44

2020-08-12 09:44:10

AI 數據人工智能

2025-10-24 10:51:05

2022-11-10 15:45:02

模型APP

2009-03-02 16:22:18

網絡拓撲網絡管理摩卡軟件

2024-10-31 09:42:08

2025-10-15 14:05:44

AI模型視頻生成

2019-10-25 16:50:51

網絡安全網絡安全技術周刊

2023-05-18 07:32:47

Ventoy開源軟件

2025-09-12 07:55:54

2011-02-23 10:20:45

點贊
收藏

51CTO技術棧公眾號

一出一进一爽一粗一大视频| 视频一区二区三区免费观看| 久久久无码精品亚洲国产| 91大神精品| 婷婷久久综合九色综合伊人色| 欧美日韩精品免费观看视一区二区| 亚洲综合成人av| 欧美一区久久| 亚洲视频第一页| 91欧美一区二区三区| 黄色污网站在线观看| 中文字幕不卡在线播放| 国产富婆一区二区三区| 波多野结衣视频观看| 国产尤物精品| 色老头一区二区三区| 成人免费看片载| 日韩漫画puputoon| 亚洲sss视频在线视频| 一区二区免费在线视频| 欧洲亚洲在线| 东方aⅴ免费观看久久av| 国产成人一区二区三区| 国产精品 欧美 日韩| 久久久久免费av| 亚洲系列中文字幕| 欧美一区二区三区小说| 不卡日韩av| 一级全黄裸体免费视频| 久久精品官网| 午夜精品一区二区三区在线| 午夜国产福利一区二区| 久久99精品久久久久久园产越南| 欧美本精品男人aⅴ天堂| 久久综合伊人77777麻豆最新章节| 大桥未久在线播放| 亚洲欧美另类久久久精品| 日韩色妇久久av| 国产精品国产高清国产| 大胆亚洲人体视频| 91手机在线视频| 国产精品视频无码| 久久精品国产免费| 国产精品美女主播| 糖心vlog精品一区二区| 日韩成人精品在线观看| 国产精国产精品| 欧美激情黑白配| 国产亚洲精品久久久久婷婷瑜伽| 久久久久在线观看| 国产一级做a爰片在线看免费| 91av精品| 久久999免费视频| 欧美日韩成人免费观看| 欧美二区视频| 久久91亚洲精品中文字幕| 国产成人无码aa精品一区| 综合天堂av久久久久久久| 久久久精品影院| 极品盗摄国产盗摄合集| 欧美一区成人| 欧美高跟鞋交xxxxhd| 久久免费精彩视频| 99国产精品| 欧美一级电影在线| 不卡av电影在线| 免费黄网站欧美| 国产中文字幕亚洲| 国产成人精品免费看视频| 国产精品一级在线| 国产一区二区三区av在线 | 无遮挡动作视频在线观看免费入口| 国产精品一区二区无线| 风间由美一区二区三区| 三级小视频在线观看| 26uuu亚洲| 婷婷久久五月天| 国产黄a三级三级三级av在线看| 综合色天天鬼久久鬼色| 91免费国产精品| 牛牛精品视频在线| 色综合色狠狠综合色| 黄色小视频免费网站| 91成人入口| 亚洲欧美国产精品久久久久久久| 美女av免费看| 欧美色图麻豆| 国产成人精品优优av| 国产乱淫av片免费| 9l国产精品久久久久麻豆| 日韩电影免费观看高清完整| 精品国产白色丝袜高跟鞋| 亚洲成av人在线观看| 亚洲免费av一区二区三区| 国产精品久久久久9999小说| 一级一级黄色片| 韩国午夜理伦三级不卡影院| 国产二区视频在线| 精品国产一级片| 99精品久久久久久| 亚洲欧美精品| 国产人成视频在线观看| 99re6热只有精品免费观看| 日韩精品极品在线观看| 自拍偷拍第9页| 夜久久久久久| 成人欧美在线观看| 日韩a在线看| 亚洲精品第1页| 可以免费观看av毛片| 视频国产一区| 在线观看午夜av| 亚洲国产成人在线| 久久这里只有精品18| 亚洲不卡系列| 亚洲成人久久电影| 九九热视频在线免费观看| 性欧美暴力猛交另类hd| 91在线在线观看| www.亚洲视频| 欧美性20hd另类| 日韩欧美理论片| av一区二区高清| 68精品久久久久久欧美| 国产精品毛片一区二区在线看舒淇 | a级在线免费观看| 亚洲成人资源| 91手机在线视频| 好了av在线| 欧美色图12p| 丰满少妇在线观看资源站| 午夜精品婷婷| 国产午夜久久久久| 中文网丁香综合网| 国产91精品在线| 亚洲小视频在线| 国产又粗又爽视频| 26uuuu精品一区二区| 国产欧美日韩小视频| 香港久久久电影| 九九久久综合网站| 99视频国产精品免费观看a | 欧美少妇一区| 日韩欧美另类一区二区| 亚洲精品国产综合久久| 偷偷操不一样的久久| 成人黄色小视频在线观看| 男人c女人视频| 中文在线免费一区三区| 欧美精品一区二区三区国产精品| 精品国产av 无码一区二区三区| 国产精品无圣光一区二区| 中文字幕永久视频| 久久精品国产99久久| 国产精品一区二区久久久久| 1024国产在线| 3d动漫精品啪啪| 好吊日在线视频| 国产一区二区三区四| 91精品一区二区三区四区| 日韩欧美一级| 97视频com| 黄网站在线观看| 欧美日韩一区小说| 欧美国产日韩在线观看成人| 国产成人免费av在线| 亚洲精品无码国产| 五月天亚洲色图| 国产精品久久视频| av在线导航| 亚洲国模精品一区| 精品黑人一区二区三区| 国产精品久久久久久久久搜平片 | 久久这里只有精品8| 成午夜精品一区二区三区软件| 久久久久久久久久久91| 黄色视屏网站在线免费观看| 欧美日韩成人一区| 久久精品第一页| 久久噜噜亚洲综合| 中文字幕中文在线| 国产精品黄色| 日本精品一区| 精品一区二区三区亚洲| 91wwwcom在线观看| 伊人免费在线| 亚洲国产成人精品久久| 国产成人自拍偷拍| 一区二区三区在线观看网站| 中文人妻一区二区三区| 国产综合色视频| 国产免费观看高清视频| 我不卡神马影院| 精品综合在线| 国产午夜久久av| 日韩av免费在线观看| www国产在线观看 | 免费黄色在线| 亚洲精品ady| 国产又粗又长又黄| 黑人巨大精品欧美一区二区| 日本黄色免费片| 92精品国产成人观看免费| 涩多多在线观看| 久久久久久夜| 免费网站在线观看视频 | 粉嫩av一区二区三区天美传媒 | 精品小视频在线观看| 国产欧美一区二区三区沐欲| 无码国产69精品久久久久网站| 毛片一区二区三区| av之家在线观看| 激情91久久| 五月天男人天堂| 国产一区二区三区四区五区传媒| 国产九色91| 国产亚洲字幕| 国产在线观看91精品一区| 神马午夜在线视频| 久久久久久久久久婷婷| 免费黄色在线观看| 色诱女教师一区二区三区| 色猫av在线| 欧美精品一区二区三区一线天视频| 91午夜交换视频| 欧美午夜精品电影| 精品国产乱子伦| 色综合久久综合网| av黄色在线看| 亚洲国产精品一区二区尤物区| 蜜臀av午夜精品久久| 国产精品女主播在线观看| 中文字幕高清视频| 26uuu成人网一区二区三区| 国产精品成人99一区无码| 国产999精品久久久久久绿帽| 爽爽爽在线观看| 精品一区二区三区不卡| 国产一二三区av| 免费久久99精品国产| 成年网站在线播放| 日韩av电影天堂| 亚洲免费av一区二区三区| 日韩高清欧美激情| 久久精品免费网站| 日本aⅴ免费视频一区二区三区 | 国产视频精品免费播放| 五月婷婷深深爱| 亚洲精品自产拍| 飘雪影院手机免费高清版在线观看| 精品无人区太爽高潮在线播放 | 日日夜夜精品| 91午夜在线播放| 久久久久九九精品影院| 91网免费观看| 免费萌白酱国产一区二区三区| 国产私拍一区| 国产亚洲电影| 亚洲精品一区二区三| 99久久综合狠狠综合久久aⅴ| 在线视频欧美一区| 欧美三级乱码| 欧美日韩亚洲一| 日本成人在线不卡视频| 亚洲综合av在线播放| 国产精品一品视频| 亚洲视频在线播放免费| 久久综合九色综合97婷婷女人| 中字幕一区二区三区乱码| 国产精品电影院| 欧美另类视频在线观看| 欧美色视频日本版| 最近日韩免费视频| 日韩丝袜情趣美女图片| 欧美一级片免费| 亚洲色图在线观看| 国内外激情在线| 久久久免费高清电视剧观看| 午夜伦理福利在线| 国产日韩欧美在线| caoporn成人| 欧美专区一二三| 亚洲国产精品久久久天堂| 欧美视频在线观看网站| 美腿丝袜亚洲三区| 一级黄色免费视频| 国产亚洲污的网站| 国产性生活网站| 日本高清不卡在线观看| 亚洲黄色片视频| 国产亚洲欧洲在线| 草美女在线观看| 国产精品一区二区三区在线播放 | 奇米777欧美一区二区| 丰满人妻一区二区三区免费视频棣| 久久综合九色综合欧美就去吻| 中文字幕在线观看2018| 欧美小视频在线| 亚洲国产精品suv| 中文在线资源观看视频网站免费不卡 | 欧一区二区三区| 日韩精品最新在线观看| 亚洲午夜极品| 超碰成人在线播放| 91麻豆国产自产在线观看| 欧美精品xxxxx| 欧美三区免费完整视频在线观看| 亚洲成人一二三区| www.日韩av.com| 一二三四视频在线中文| 97超碰最新| 欧美顶级大胆免费视频| 波多野结衣50连登视频| 国产精品91xxx| www.4hu95.com四虎| 欧美午夜精品久久久久久浪潮 | 色香蕉在线观看| 日日骚欧美日韩| 好吊一区二区三区视频| 一区二区三区资源| 在线观看日批视频| 亚洲欧美在线x视频| 久色国产在线| 91日韩在线视频| 久久免费大视频| 亚洲xxxx2d动漫1| 国产亚洲欧美日韩日本| 毛片在线免费视频| 亚洲成人黄色网址| 丁香花在线影院| 国产91精品一区二区绿帽| 欧美在线三级| 亚洲热在线视频| 亚洲黄一区二区三区| 国产精品久久欧美久久一区| 日日狠狠久久偷偷四色综合免费 | **亚洲第一综合导航网站| 日韩在线看片| 成人性生交免费看| 国产精品久久久久久亚洲伦| 在线视频精品免费| 亚洲视频一区二区| 69堂免费精品视频在线播放| 欧美中文娱乐网| 男女性色大片免费观看一区二区 | 激情综合网站| www黄色在线| 中文字幕电影一区| 中文字幕一二区| 日韩最新中文字幕电影免费看| 日韩在线电影| 日本福利视频网站| av在线播放成人| 毛片视频网站在线观看| 亚洲天堂av高清| a屁视频一区二区三区四区| 亚洲一区二区三区乱码| 国内精品自线一区二区三区视频| 波多野结衣亚洲一区二区| 精品免费视频.| 日本不卡1234视频| 欧美日韩国产一二| 日本麻豆一区二区三区视频| 蜜桃av.com| 日韩精品在线看片z| 涩涩视频在线免费看| 色综合久久av| 国产一区二区不卡在线| 久久久久久国产精品视频| 日韩av在线天堂网| 亚洲综合在线电影| 天天在线免费视频| 粉嫩av亚洲一区二区图片| 亚洲免费黄色网址| 色偷偷av一区二区三区乱| 欧美视频精品全部免费观看| 精品久久久久久久久久中文字幕| 久久久久久毛片| 99热这里只有精品66| 欧美亚洲国产另类| 欧美www视频在线观看| 亚洲国产精品第一页| 91福利在线观看| 国产传媒在线播放| 麻豆91av| 国产一区不卡在线| 天堂网视频在线| 欧美精品一区三区| 久青青在线观看视频国产| 秋霞av国产精品一区| 亚洲成人精选| 在哪里可以看毛片| 欧美一区二区视频免费观看| 久草在线资源站手机版| 一本久久a久久精品vr综合| 成人免费毛片片v| 这里只有精品9| 992tv在线成人免费观看| 三上亚洲一区二区| 免费看污黄网站在线观看|