JVM从浅入深

目录

一.JVM内存结构

1.程序计数器

定义​

作用

2.虚拟机栈

2.1定义

2.2问题辨析(线程安全)

2.3栈内存溢出

2.4线程运行判断

3.本地方法栈

4.堆

堆内存溢出

堆内存诊断

5.方法区

[5.1 定义](#5.1 定义)

[5.2 组成](#5.2 组成)

[5.3 方法区内存溢出](#5.3 方法区内存溢出)

[5.4 运行时常量池](#5.4 运行时常量池)

[5.5 StringTable(串池)](#5.5 StringTable(串池))

[5.6 StringTable特性](#5.6 StringTable特性)

[5.7 StringTable位置](#5.7 StringTable位置)

[5.8 StringTable垃圾回收](#5.8 StringTable垃圾回收)

[5.9 StringTable性能调优](#5.9 StringTable性能调优)

口诀总结

6.直接内存

[6.1 定义](#6.1 定义)

[6.2 基本使用](#6.2 基本使用)

[6.3 内存溢出(OOM)](#6.3 内存溢出(OOM))

[6.4 释放原理(重点)](#6.4 释放原理(重点))

[6.5 禁用显式回收对直接内存影响](#6.5 禁用显式回收对直接内存影响)

二.垃圾回收

1.如何判断对象可以回收

[1.1 引用计数法](#1.1 引用计数法)

[1.2 可达性分析算法](#1.2 可达性分析算法)

[1.3 五种引用](#1.3 五种引用)

引用队列​

2.垃圾回收算法

3.分代垃圾回收

4.垃圾回收器

[4.1 串行](#4.1 串行)

[4.2 吞吐量优先](#4.2 吞吐量优先)

[4.3 响应时间优先](#4.3 响应时间优先)

[4.4 G1](#4.4 G1)

(1)G1回收阶段

[(2)Young Collection(年轻代收集)](#(2)Young Collection(年轻代收集))

[(3)Young Collection + CM(并发标记)](#(3)Young Collection + CM(并发标记))

[(4)Mixed Collection(混合收集)](#(4)Mixed Collection(混合收集))

[(5) FUll GC](#(5) FUll GC)

[(6)Young GC(跨代引用)](#(6)Young GC(跨代引用))

[① 为什么会有"跨代引用"](#① 为什么会有“跨代引用”)

[② 卡表(Card Table)------"老年代→新生代"的 粗粒度地图](#② 卡表(Card Table)——“老年代→新生代”的 粗粒度地图)

[③ 后置写屏障(Post-Write Barrier)+ 脏卡队列](#③ 后置写屏障(Post-Write Barrier)+ 脏卡队列)

[④ Remembered Set(RSet)------"卡表→Region"的 精确定位](#④ Remembered Set(RSet)——“卡表→Region”的 精确定位)

[⑤ Young GC 时的工作流程(结合图)](#⑤ Young GC 时的工作流程(结合图))

(7)Remark

[① 问题背景:并发标记时对象图还在变](#① 问题背景:并发标记时对象图还在变)

[② SATB(Snapshot-At-The-Beginning) 思路](#② SATB(Snapshot-At-The-Beginning) 思路)

[③ 预写屏障(Pre-Write Barrier)(底层实现)](#③ 预写屏障(Pre-Write Barrier)(底层实现))

[④ SATB 标记队列(SATB Mark Queue)→ 全局队列](#④ SATB 标记队列(SATB Mark Queue)→ 全局队列)

[⑤ 时间线(配合图)](#⑤ 时间线(配合图))

(8)JDK8u20字符串去重

[(9)JDK 8u40 并发标记类卸载](#(9)JDK 8u40 并发标记类卸载)

[(10)JDK 8u60 回收巨型对象](#(10)JDK 8u60 回收巨型对象)

(11)JDK9并发标记起始时间的调整


一.JVM内存结构

区域 线程私有? 说明 生命周期
PC 寄存器(Program Counter) ✅ 当前字节码行号 与线程同生同灭
Java 虚拟机栈(Java Stack) ✅ 局部变量、操作数栈、方法出口 与线程同生同灭
本地方法栈(Native Method Stack) ✅ JNI native 方法用的 C 栈 与线程同生同灭
堆(Heap) ❌ 所有对象实例、数组 JVM 启动到退出
方法区(Metaspace / PermGen) ❌ 类元数据、常量池、静态变量 JVM 启动到退出
直接内存(Direct Memory) ❌ ByteBuffer.allocateDirect、NIO 映射 JVM 启动到退出

1.程序计数器

定义

作用

2.虚拟机栈

2.1定义

栈顶部的栈帧就是活动栈帧

2.2问题辨析(线程安全)

垃圾回收不涉及栈内存: 栈帧(方法调用的内存块)随方法调用而创建,随方法结束而自动销毁(由编译器预设的字节码指令控制,无需GC干预)。

栈内存分配不是越大越好: 栈内存是"线程私有的、一次性全额申请、生命周期同线程、永不归还"的内存 。它不像堆那样"按需分配、GC 回收",所以给得越大,只是白白预占、浪费、甚至直接创不出线程。

2.3栈内存溢出

2.4线程运行判断

3.本地方法栈

4.堆

堆内存溢出

可通过设置较小的堆内存加速问题的排查

堆内存诊断

5.方法区

5.1 定义

方法区域是 JVM 中的一个逻辑内存区域,用于存储每个类的相关结构,例如运行时常量池、字段和方法数据、方法和构造函数的代码,以及类执行所需的其他元数据。

5.2 组成

5.3 方法区内存溢出

方法区(Java 8 以后叫 Metaspace )存的是"类的元数据"------类名、字段、方法、字节码、常量池、注解、JIT 编译后的机器码等。
"方法区内存溢出" 就是这片区域被撑爆,JVM 无法再加载/扩展类信息,于是抛出:

  • Java 7 及之前:java.lang.OutOfMemoryError: PermGen space 永久代空间

  • Java 8 及之后:java.lang.OutOfMemoryError: Metaspace 元空间

场景 典型触发方式 观察指标
1. 动态类过多 多次 Class.forName、反射、代理、JSON/Bean 拷贝库(CGLIB、Objenesis、Javassist)、Groovy/Scala/Kotlin 脚本引擎、JSP 热加载 加载类数(jstat -loader)持续上升,GC 不下降
2. 热部署 / 热替换 Tomcat、Spring Boot DevTools、OSGi、Arthas retransform,旧 ClassLoader 应该被回收却还被引用 类加载器+类实例双重泄漏
3. 巨型常量池 代码里写了几万个 String s = "xxx" 或 static final String,或 MyBatis 拼接 SQL 用 ++ 拼出大量不同字面量 常量池占 Metaspace 70 % 以上
4. JIT 代码缓存过大 -XX:ReservedCodeCacheSize 太小,或大量 lambda/invokedynamic 生成巨量机器码 Code Cache 接近上限
5. 框架 Bug 早期 Hibernate、Spring CGLIB 重复生成代理类未复用 同一接口出现 UserService$$EnhancerBySpringCGLIB$$a3c4d5f2 几百次

5.4 运行时常量池

5.5 StringTable(串池)

5.6 StringTable特性

第一行:

读取到常量加入串池"a","b"

new了对象加入堆new String("a") new String("b") new String("ab")

s="ab"来自堆中

第二行:

s2=s.intern由于串池中没有"ab",将堆中"ab"加入串池并返回给s2(1.8版本)

String x="ab"由于串池中已经存在"ab"所以直接取串池中的

s2==x 为ture s==x为ture

(1.6版本)s.intern在串池没有的情况下,会拷贝一个加入串池并返回

此时s2为串池中拷贝的,x取出串池中拷贝的"ab"与s不同

s2==x ture s==x false

5.7 StringTable位置

5.8 StringTable垃圾回收

版本 所在区域 触发 GC 类型 说明
JDK≤6 PermGen(永久代) Full GC 老年代或永久代不足时才触发,回收频率极低,容易堆积无用字符串。
JDK≥7 Java 堆 Young GC / Full GC 只要 Minor GC 发生就可能清理,回收及时性大幅提高。

5.9 StringTable性能调优

复制代码
# 启动参数
-XX:StringTableSize=200003          # 桶数放大 3 倍
-XX:+PrintStringTableStatistics     # 事后对账
-Xms4g -Xmx4g -XX:+UseG1GC          # 保持 GC 轻快

# 代码模板
private static final Interner<String> POOL = WeakInterners.create();
String key = POOL.intern(raw);       // 高重复但可丢弃用弱引用池

口诀总结

"先监控、再桶数、后代码,GC 友好并发怕。"

只要 桶数 > 1.5 × 预计条目数 、不乱 intern() 、GC 健康 ,StringTable 就能一直保持 O(1) 的查询速度。

6.直接内存

6.1 定义

直接内存(Direct Memory)是 JVM 堆外 的一块内存区域,不在 Java 堆里 ,由操作系统直接分配和释放,Java 代码通过 sun.misc.Unsafe 或 java.nio.ByteBuffer.allocateDirect() 访问。

6.2 基本使用

  1. 最典型入口:
    ByteBuffer.allocateDirect(size) → 返回 DirectByteBuffer 对象。

  2. 背后实现:

    通过**Unsafe.allocateMemory** 向操作系统 malloc 申请一块堆外 内存,地址存在 address 字段。

  3. 使用场景:

    NIO、Netty、Kafka、RocketMQ、G1 的巨型对象、零拷贝、JNI 库(TensorFlow、OpenCV)等。

  4. 优势:

    • 不受 -Xmx 限制,只在 MaxDirectMemorySize(默认 ≈ 堆大小)以内。

    • 省去一次 堆内↔堆外 拷贝,IO 性能高。

    • 减轻 GC 压力(逻辑上不在 GC 堆)。

6.3 内存溢出(OOM)

  1. 异常信号:
    java.lang.OutOfMemoryError: Direct buffer memory

  2. 触发条件:

    • 已分配量 ≥ MaxDirectMemorySize;

    • 或 物理内存 + swap 被耗尽。

  3. 定位三板斧:

    ① jmap -dump → MAT / YourKit 看 DirectBuffer 对象个数;

    ② NMT(Native Memory Tracking)jcmd VM.native_memory summary;

    ③ -XX:MaxDirectMemorySize=size 调大做对比实验。

  4. 常见代码级泄漏:

    • 只 allocateDirect 不 release(Netty 未 Unpooled.release);

    • 使用 FileChannel.map() 反复映射文件,忘记 unmap();

    • JNI 库内部 malloc 不报 JVM 账目。

6.4 释放原理(重点)

  1. 分配对象 vs 分配内存:

    • JVM 端 :DirectByteBuffer 实例(很小)仍在 Java 堆,受 GC 管理。

    • OS 端 :真正的堆外页由 malloc / mmap 完成,GC 不负责。

  2. 两种回收路径:

    1. 显式 :
      ((DirectBuffer) buf).cleaner().clean() → Unsafe.freeMemory(address) 立即归还 OS。

    2. 依赖 GC(默认):

      • 对象进入老年代 → Full GC 或 System.gc() 触发 → Cleaner(PhantomReference)钩子执行 → 调用 freeMemory。

      • 因此「堆越慢,直接内存越晚释放」。

  3. 关键源码:
    jdk/internal/misc/Unsafe.allocateMemory
    java.nio.DirectByteBuffer.Deallocator.run()

6.5 禁用显式回收对直接内存影响

  1. 参数:-XX:+DisableExplicitGC(生产常用,防代码里频繁 System.gc())。

  2. 副作用:

    • 直接内存的 Cleaner 钩子仍会被 Full GC 触发 ,但 Young GC 不再处理。

    • 若应用从不触发老年代收集 (对象很快死亡或晋升阈值高),堆外内存可能长时间得不到释放,看起来就像"泄漏"。

  3. 官方建议:

    • 开启 -XX:+DisableExplicitGC 时,主动定期触发 Full GC 或 手动 clean;

    • 或者使用 Netty-4 的 -Dio.netty.maxDirectMemory=0 让 Netty 自己管理计数并强制 clean;

    • JDK 17+ 的 Foreign Memory API (MemorySegment) 已支持 scoped 释放,无需依赖 GC。

二.垃圾回收

1.如何判断对象可以回收

1.1 引用计数法

1.2 可达性分析算法

JVM 进行垃圾回收时,并不会一次性遍历整个堆,而是从这些 Roots 出发,标记所有仍然存活的对象 ;从 Roots 无法到达的对象,就被判定为"已死",可以回收。

MAT 中的名称 对应 JVM 内部概念 为什么能成为 Root
System Class 由 Bootstrap/Extension 类加载器 加载的类(如 java.lang.*) 这些类永远不会被卸载,其静态字段引用的对象始终存活。
Co Native Stack JNI 本地方法栈中的引用 本地 C/C++ 代码可能通过 JNI 访问 Java 对象,JVM 必须保证它们存活。
Thread 正在执行的 Java 线程 每个线程的 栈帧局部变量表 里的引用都是活的。
Busy Monitor 被 synchronized 加锁的对象(Monitor 入口集) 同步块还没退出,锁对象必须存活,否则解锁时会崩溃。

1.3 五种引用

引用类型 类 回收时机 与队列配合 典型场景
强引 用 Strong Object o = new Object() 永远不会被 GC 无 日常代码
软引用 Soft SoftReference<Object>(obj) 内存不足时(OOM 前) 可以带队列,用于 缓存清理通知 Guava 内存缓存、图片缓存
弱引用 Weak WeakReference<Object>(obj) 下一次 GC 无论内存够不够 必带队列 ,用于 清理通知 ThreadLocalMap 的 key、WeakHashMap
虚引用 Phantom PhantomReference<Object>(obj, queue) 对象已被回收后 (get() 永远 null) 必须带队列 ,用于 回收后置动作 DirectByteBuffer 堆外内存释放(Cleaner)
终引用 Finalizer (仅内部 FinalizerReference) 第一次不可达 → 加入 Finalizer 队列 → 执行 finalize() → 再次 GC 才回收 JVM 内部队列,开发者无感知 兼容历史 Object.finalize(),性能差,已废弃

**方法1:**当List与byte 为强引用时,内存溢出不会执行GC垃圾回收。

方法2:当List强引用一个软引用对象,而软引用对象再引用byte 时,当内存不足时(OOM 前)执行GC

引用队列

2.垃圾回收算法

3.分代垃圾回收

4.垃圾回收器

4.1 串行

4.2 吞吐量优先

参数 含义 示例值
-XX:+UseParallelGC 新生代用 Parallel Scavenge(并行复制) 默认即开启
-XX:+UseParallelOldGC 老年代用 Parallel Old(并行标记-整理) 与上面配对
-XX:+UseAdaptiveSizePolicy 自动调整新生代大小、Survivor 比例等,以达到目标停顿/吞吐量 默认开启
-XX:GCTimeRatio=ratio 设定 吞吐量目标 :1 / (1 + ratio) 的时间用于 GC,默认 99 → 允许 1% 时间做 GC 99
-XX:MaxGCPauseMillis=ms 最大停顿时间目标(毫秒),软目标,JVM 会尽量缩小新生代来缩短停顿 200
-XX:ParallelGCThreads=n GC 线程数,默认 = CPU 核心数,可手动减少避免抢占业务线程 n

吞吐量和停顿时间为相反的目标。

4.3 响应时间优先

参数 作用
-XX:+UseConcMarkSweepGC 开启 CMS 老年代收集器(JDK 8 默认即启用)
-XX:+UseParNewGC 新生代配套使用 ParNew(并行复制,只有它支持 CMS)
-XX:ParallelGCThreads=n STW 阶段(初始标记、重新标记)的并行线程数
-XX:ConcGCThreads=threads 并发阶段 (并发标记、并发清理)的线程数,一般设为 (n+3)/4
-XX:CMSInitiatingOccupancyFraction=percent 老年代使用率 ≥ percent% 时启动 CMS GC,默认 68
-XX:+CMSScavengeBeforeRemark 在"重新标记"前先做一次 Young GC,减少跨代引用扫描量
  1. 初始标记(Initial Mark)

    • Stop-The-World(图里 CPU0~3 短暂阻塞)

    • 只标记 GC Roots 直接关联的对象,耗时极短。

  2. 并发标记(Concurrent Mark)

    • 与应用线程并发执行(CPU 继续跑业务)

    • 从 Roots 出发,标记 所有可达对象。

  3. 重新标记(Remark)

    • 第二次 STW(图里 CPU0/2/3 再次短暂阻塞)

    • 修正并发阶段因用户线程运行而产生的 变动。

  4. 并发清理(Concurrent Sweep)

    • 与应用线程并发执行

    • 回收 标记为垃圾的对象,不产生压缩,因此有碎片。

✅ 优点

  • 大部分 GC 工作与业务并发,停顿时间极短(通常 < 200 ms)。

  • 适合 Web/API、交互式应用 等对响应时间敏感的场景。

❌ 缺点

  • 内存碎片 + 浮动垃圾(并发清理时用户线程仍在分配)。

  • JDK 9 已废弃,JDK 14 移除;官方推荐 G1 或 ZGC 替代。


一句话记忆

"CMS = ** Initial STW → 并发标记 → Remark STW → 并发清理 **;
参数控线程数、触发阈值、预清理 ;
停顿短,但碎片多,已过时。"

4.4 G1

(1)G1回收阶段

(2)Young Collection**(年轻代收集)**

  • Stop-The-World ,但极短(毫秒级)。

  • 并行复制 Eden + Survivor → 新 Survivor / Old Region。

  • 完成后 Eden Region 被清空,存活对象年龄 +1。

(3)Young Collection + CM(并发标记)

  • 与应用线程并发执行,无停顿。

  • 从 GC Roots 出发,三色标记 整个堆,记录每个 Region 的存活比例。

  • 子阶段:

    • 初始标记(STW,极短)

    • 根区域扫描

    • 并发标记

    • 最终标记(STW,修正并发期间变动)

  • 目的:选出"垃圾最多"的 Old Region ,为下一步 Mixed GC 做准备。

(4)Mixed Collection(混合收集)

  • Stop-The-World ,但只回收垃圾最多的部分 Old Region + 全部 Young Region。

  • 用户可设 停顿目标 (-XX:MaxGCPauseMillis),G1 根据历史成本动态挑选 Region 数量,保证在目标时间内完成。

  • 连续多次 Mixed 直到 Old Region 垃圾占比 < G1MixedGCLiveThreshold 或 Mixed 次数上限。

(5)FUll GC

收集器 新生代算法 Minor GC 停顿时长 老年代算法 Full GC 停顿时长 特点
Serial Serial 长(单线程) Serial Old 长(单线程) 简单,小堆
Parallel ParNew 短(多线程) Parallel Old 较长(多线程) 吞吐量优先
CMS ParNew 短 Concurrent Mark-Sweep 两次极短 STW + 并发清理 响应优先,碎片
G1 Parallel Region 可预测短 Region-Based Mixed 可预测短(只收垃圾最多 Region) 大堆、低停顿

(6)Young GC(跨代引用)

"卡表 + 脏卡 + 后置写屏障" = G1 在 Young GC 时快速找到 老年代→新生代 的引用,避免全堆扫描。


① 为什么会有"跨代引用"

Young GC 只收 Eden/Survivor,但
老年代对象可能持有新生代对象的引用 (图里箭头),如果 Young GC 时不扫描"老年代 → 新生代"的引用 ,就会出现 "存活对象被误回收" ,导致 数据丢失甚至 JVM 崩溃。。

若不做记录,就必须 扫描整个老年代 才能确定存活 → 代价太大。


② 卡表(Card Table)------"老年代→新生代"的 粗粒度地图
  • 把 整个堆 划分成 512 字节的 卡片(Card);

  • 每个卡片对应 1 字节 的 卡表项:

    • 0 = 干净(无跨代引用)

    • 1 = 脏(Dirty)(可能持有新生代引用)


③ 后置写屏障(Post-Write Barrier)+ 脏卡队列

当 应用线程 修改字段时,JVM 插入一段 极小汇编代码:

复制代码
; 伪汇编
cmp   rdi, young_start
jb    skip             ; 如果写入目标是老年代,继续
mov   byte [card_table + addr>>9], 1   ; 把对应卡标记为脏
skip:
  • 写入 老年代→新生代 引用 → 当前卡被标记为 脏;

  • 为了 不阻塞应用线程 ,真正的"脏卡"先放进 Dirty Card Queue ,由 并发 refinement 线程 异步处理。


④ Remembered Set(RSet)------"卡表→Region"的 精确定位
  • 每个 Region 维护一个 RSet (HashMap 结构):
    Key = 其他 Region 的起始地址
    Value = 这些 Region 中 **哪些卡** 指向我

  • Young GC 时 只扫描 RSet 记录的脏卡 → 瞬间得到 跨代引用集合,无需全堆扫描。


⑤ Young GC 时的工作流程(结合图)
  1. Root Scan → 扫描线程栈、JNI 等 固定根;

  2. Scan RSet → 只遍历 脏卡里的对象 ,找到 老→新引用;

  3. Copy & Update → 把存活对象复制到 Survivor/Old,并 更新指针;

  4. Clear Dirty Card → 当前 Region 的脏卡清零,等待下次写入。


一句话记忆

"写操作 → 写屏障 → 脏卡 → RSet → Young GC 只扫脏卡 ",
"卡表是地图,RSet是坐标,写屏障是 GPS 更新器"。

(7)Remark

这是 CMS / G1 在 "Remark"(最终标记)阶段 用来 修正并发标记期间被改动的引用 的核心机制:
"预写屏障 + SATB 标记队列" 保证 "并发标记快照" 的完整性。

① 问题背景:并发标记时对象图还在变
  • 并发标记阶段 应用线程仍在运行,字段随时被改写。

  • 若不做记录,新产生的跨代引用 或 存活对象被误标为垃圾 就会漏标 → 漂浮垃圾 甚至 误回收。


② SATB(Snapshot-At-The-Beginning) 思路
  • 在 并发标记开始那一刻 拍一张快照 S。

  • 只要当时快照里是活的,就必须被标记为活(即使后面变成垃圾也无所谓)。

  • 新分配的对象 默认 黑色 (存活),旧对象的引用字段被删除/修改 时才需要记录。


③ 预写屏障(Pre-Write Barrier)(底层实现)

位置 :在 应用线程 的 putfield/putstatic/A*STORE 指令前插入一段 极小汇编(几乎无性能损耗):

复制代码
; 伪汇编
oldValue = [field]          ; 先读旧值
if oldValue != null
    satb_enqueue(oldValue)  ; 把旧值压入 SATB 队列
[field] = newValue          ; 再写新值
  • 写前 先把 旧引用 记录下来(不是新引用)。

  • 记录内容:旧目标对象地址 → 压入 线程私有的 SATB 标记队列(SATB Mark Queue)。


④ SATB 标记队列(SATB Mark Queue)→ 全局队列
  • 每个应用线程一个 本地 SATB 队列(无锁,极快)。

  • 队列满后 批量刷新到全局 SATB 队列。

  • Remark 阶段 只需 扫描全局队列里的对象 ,就能 把"被删掉"的引用重新标记为存活 ,保证 快照 S 的完整性。


⑤ 时间线(配合图)
  1. 并发标记开始 → 生成快照 S。

  2. 应用线程 A 执行 :obj.f = newObj(原 f 指向 X)

    → 预写屏障记录 X → X 进入 SATB 队列。

  3. Remark 阶段 → 扫描 SATB 队列 → 把 X 重新标记为活。

  4. 结果 :X 不会被 Young GC/Mixed GC 误回收 ;浮动垃圾 最多留到下次。


一句话记忆

"预写屏障 = 写前拍照 ,SATB 队列 = 照片底片 ,Remark = 把底片洗出来 ,保证并发标记不漏活对象。"

(8)JDK8u20字符串去重

(9)JDK 8u40 并发标记类卸载

(10)JDK 8u60 回收巨型对象

(11)JDK9并发标记起始时间的调整

相关推荐
夜之眷属1 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
用户094248568032 小时前
第31章:JVM字节码解释器与模板解释器源码路径
java·jvm
阿俊-全栈开发4 小时前
LikeShop 版本升级与备份:从旧版平滑迁移的完整步骤
jvm·数据库·oracle·开源·likeshop·likeshop开源商城
Nebula_g6 小时前
JavaSE加强:线程Thread(线程安全重点)
java·开发语言·jvm·算法·javase·技术栈
JAVA面经实录9176 小时前
Java高级后端 · 全套面试通关手册(线上故障排查)
java·jvm·面试
sibylyue9 小时前
SQLite 运行模式
java·jvm·sqlite
toooooop89 小时前
宝塔Python项目管理器:环境变量踩坑记录
java·jvm·python
佳児素花痴╮1 天前
线程知识点
java·开发语言·jvm
用户094248568031 天前
第29章:【OpenJDK中级篇综合实战】百万长连接网关的JVM稳定性工程
java·jvm
夜之眷属1 天前
JVM实战:服务器堆外内存去哪了(NMT实测)
java·服务器·jvm·后端·性能优化