偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

JVM 透視神器:把“對象拓?fù)鋱D”塞進(jìn)你的 Spring Boot

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

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

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

于是,這個(gè)方案就誕生了:在 Spring Boot 里嵌一個(gè)“內(nèi)存對象拓?fù)浞?wù)”。只要訪問 /memviz.html 就能在瀏覽器里看到對象圖;支持類/包過濾、按對象大小高亮、點(diǎn)擊看詳情;默認(rèn)零開銷,只有你點(diǎn)擊“生成快照”時(shí)才工作,足夠線上可用。

你將獲得什么

  • 一個(gè)可嵌入任何 Spring Boot 的“內(nèi)存對象拓?fù)浞?wù)”

訪問:/memviz.html

功能:按類/包名過濾、按對象大小高亮、點(diǎn)擊節(jié)點(diǎn)看詳情

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

為什么不用傳統(tǒng)工具直接上?

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

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

架構(gòu)設(shè)計(jì):為什么是“HPROF 快照 + 在線解析”

目標(biāo):

  • 全量對象 + 真實(shí)引用鏈
  • 無需預(yù)埋、無需重啟
  • 只在手動(dòng)觸發(fā)時(shí)才消耗資源

方案:

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

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

可運(yùn)行代碼

項(xiàng)目結(jié)構(gòu)

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,請根據(jù)你的制品庫加上相應(yīng)依賴(不同公司/倉庫坐標(biāo)可能不同)。下方示例僅展示一個(gè)常見組合,真實(shí)項(xiàng)目請按你環(huán)境調(diào)整。

<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,請按你的倉庫坐標(biāo)引入
    <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>

應(yīng)用入口 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);
  }
}

領(lǐng)域模型 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/第三方/業(yè)務(wù)
        public int instanceCount;     // 該類實(shí)例總數(shù)
        public String formattedSize;  // 格式化顯示
        public String packageName;    // 包名
        public boolean isArray;       // 是否數(shù)組
        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;   // 通過哪個(gè)字段/元素引用


        public Link(String s, String t, String field) {
            this.source = s;
            this.target = t;
            this.field = field;
        }
    }


    // Top100類統(tǒng)計(jì)
    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;
}

觸發(fā)堆快照 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 是否僅包含存活對象(會(huì)觸發(fā)一次 STW)
   * @param dir  目錄(建議掛到獨(dú)立磁盤/大空間)
   */
  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.* 風(fēng)格的 API(你可替換為等價(jià)的 HPROF 解析實(shí)現(xiàn))。核心邏輯:

  • 收集實(shí)例(可按類名過濾)
  • 統(tǒng)計(jì) Top 類與大小
  • 生成類級節(jié)點(diǎn)與引用邊(限制數(shù)量防止前端崩潰)
  • 可選折疊集合類型節(jié)點(diǎn),降低噪點(diǎn)
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,請?zhí)鎿Q為其 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;     // 圖上顯示的類數(shù)
    private static final int MAX_LINKS = 200;           // 連線上限(防止前端卡頓)


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


        GraphModel graph = new GraphModel();


        // 1) 假設(shè)我們拿到了所有類與其實(shí)例,這里只演示構(gòu)圖策略:
        //    - 統(tǒng)計(jì)每個(gè)類的總 shallow size、實(shí)例數(shù)
        //    - 取 Top100 類作為可視化節(jié)點(diǎn)
        //    - 基于對象引用關(guān)系推導(dǎo)類之間的引用邊(受限于 MAX_LINKS)


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


        // 模擬聚合結(jié)果(請刪除)
        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", "業(yè)務(wù)代碼", "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個(gè)實(shí)例, %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);
        }


        // 模擬類間引用邊(實(shí)際根據(jù)對象引用聚合成類引用)
        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++;
            }
        }


        // 匯總統(tǒng)計(jì)(總對象數(shù)、總內(nèi)存)——實(shí)際按解析結(jié)果填入
        graph.totalObjects = 123456;
        graph.totalMemory = 120_000_000L + 80_000_000L + 30_000_000L;
        graph.formattedTotalMemory = formatSize(graph.totalMemory);


        return graph;
    }


    // === 下方是一些工具方法 + 演示用內(nèi)部聚合類 ===
    private static class ClassStatAgg {
        String className;
        String category;    // JDK / 第三方 / 業(yè)務(wù)代碼
        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 "數(shù)組";
        if (className.contains("$")) return className.contains("Lambda") ? "Lambda表達(dá)式" : "內(nèi)部類";
        if (className.startsWith("java.util.") &&
                (className.contains("List") || className.contains("Set") || className.contains("Map"))) return "集合類";
        if (className.startsWith("java.lang.")) return "基礎(chǔ)類型";
        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));
    }
}

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

安全工具 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

前端將調(diào)用以下 REST 接口:

  • POST /api/memviz/snapshot?live=true&filter=xxx&collapse=false:一鍵 dump + 解析 + 返回圖
  • GET  /memviz.html:靜態(tài)頁面(在 resources/static 下,Spring Boot 會(huì)自動(dòng)映射)
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 力導(dǎo)向圖)

功能點(diǎn):

  • 觸發(fā)快照(live 開關(guān))
  • 類/包名過濾(后端過濾)
  • 節(jié)點(diǎn)按大小映射半徑,并可基于“最小顯示大小閾值”進(jìn)行前端過濾
  • 顏色按類別(JDK/第三方/業(yè)務(wù))區(qū)分
  • 搜索定位、點(diǎn)擊彈出詳情面板
  • 懸停高亮相鄰節(jié)點(diǎn)與邊
  • 自適應(yīng)窗口大小、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 對象拓?fù)鋱D</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 內(nèi)存對象拓?fù)鋱D</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" /> 折疊集合節(jié)點(diǎn)</label>
    <button id="snapshotBtn" class="primary">生成快照并可視化</button>
    <button id="resetBtn" class="ghost">重置視圖</button>
  </div>
  <div class="meta">
    <span id="metaTotal">總對象數(shù):--</span> | 
    <span id="metaMem">總內(nèi)存:--</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>業(yè)務(wù)代碼
    </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>節(jié)點(diǎn)詳情</h3>
      <div id="detail">點(diǎn)擊圖中的節(jié)點(diǎn)查看詳情</div>
    </div>


    <div class="footer">
      提示:拖拽節(jié)點(diǎn)可重新布局,滾輪縮放,拖拽空白處平移。
    </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'; // 業(yè)務(wù)
  }


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


  // 閾值過濾(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">業(yè)務(wù)</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>實(shí)例總數(shù):${fmt(d.instanceCount)}</li>
        <li>淺表大?。?{d.formattedSize || (d.shallowSize + ' B')}</li>
        <li>對象類型:${d.objectType || '--'}</li>
        <li>數(shù)組:${d.isArray ? '是' : '否'}</li>
        <li>節(jié)點(diǎn)ID:${d.id}</li>
      </ul>
      <div class="hint">提示:將節(jié)點(diǎn)拖到合適位置,可調(diào)整局部布局;再次點(diǎn)擊可查看其它節(jié)點(diǎn)。</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);
    });
  }


  // 數(shù)據(jù)索引(用于 link 坐標(biāo)快速訪問)
  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 = '總對象數(shù):' + (data.totalObjects || '--');
      metaMem.textContent = '總內(nèi)存:' + (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);
  });


  // 重置視圖(清空高亮、復(fù)位縮放)
  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. 啟動(dòng)應(yīng)用后,打開:http://localhost:8080/memviz.html
  2. 輸入過濾關(guān)鍵詞(可選):如 com.icoderoad、HashMap
  3. 勾選 live(可選):只保留存活對象(會(huì)觸發(fā)一次 STW)
  4. 勾選“折疊集合節(jié)點(diǎn)”(可選):減少集合類噪點(diǎn)
  5. 點(diǎn)擊“生成快照并可視化”
  6. 調(diào)整“大小閾值(MB)”過濾細(xì)碎節(jié)點(diǎn),聚焦大戶
  7. 點(diǎn)擊節(jié)點(diǎn)查看詳情;懸??筛吡拎従?;拖拽/縮放調(diào)整視圖

實(shí)戰(zhàn)建議

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

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

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

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

結(jié)論

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

責(zé)任編輯:武曉燕 來源: 路條編程
相關(guān)推薦

2019-09-05 11:14:12

監(jiān)控系統(tǒng)拓?fù)鋱D

2009-06-22 17:15:50

Java Applet拓?fù)鋱D

2021-02-01 09:13:34

Zabbix5.2拓?fù)鋱D運(yùn)維

2019-07-03 10:16:11

網(wǎng)絡(luò)監(jiān)控拓?fù)鋱D

2016-09-29 09:33:06

Html5站點(diǎn)地圖拓?fù)鋱D

2020-11-09 14:03:51

Spring BootMaven遷移

2020-02-26 15:35:17

Spring Boot項(xiàng)目優(yōu)化JVM調(diào)優(yōu)

2020-11-10 09:19:23

Spring BootJava開發(fā)

2023-07-27 08:53:44

2020-08-12 09:44:10

AI 數(shù)據(jù)人工智能

2022-11-10 15:45:02

模型APP

2009-03-02 16:22:18

網(wǎng)絡(luò)拓?fù)?/a>網(wǎng)絡(luò)管理摩卡軟件

2024-10-31 09:42:08

2019-10-25 16:50:51

網(wǎng)絡(luò)安全網(wǎng)絡(luò)安全技術(shù)周刊

2025-10-24 10:51:05

2011-02-23 10:20:45

2025-09-12 07:55:54

2019-07-23 17:52:59

Spring BootJava開發(fā)

2024-02-04 00:00:00

@ValidSpring@Validated
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)