【地基篇】---JVM 内存结构知识点(Java 8 运行时数据区深度解析)

基于:HotSpot VM / Java 8(Oracle JDK 8u 与 OpenJDK 8,Linux x64 / macOS 64 位为例)

Java 8 时代 JVM 的内存划分:先给「运行时数据区」全景定位表与 Java 7→8 的关键变革(永久代退出、元空间登场),再逐区域拆解线程私有区(程序计数器 / 虚拟机栈 / 本地方法栈)与线程共享区(堆 / 方法区·元空间 / 直接内存),随后以「一个对象的内存之旅」串起 类加载 → 堆分配 → 分代晋升 的主线流程,最后给出全套内存参数速查表与 jstat / jmap / jcmd 观测命令。


目录

  1. 核心理论与定位 ★★★
    • [1.1 运行时数据区全景总表](#1.1 运行时数据区全景总表) ★★★
    • [1.2 Java 8 内存模型变革:永久代 → 元空间](#1.2 Java 8 内存模型变革:永久代 → 元空间)
    • [1.3 规范区域之外:直接内存与代码缓存](#1.3 规范区域之外:直接内存与代码缓存)
  2. 线程私有区
    • [2.1 程序计数器](#2.1 程序计数器)
    • [2.2 Java 虚拟机栈](#2.2 Java 虚拟机栈) ★★★
    • [2.3 本地方法栈](#2.3 本地方法栈)
  3. 线程共享区
    • [3.1 堆](#3.1 堆) ★★★
    • [3.2 方法区与元空间](#3.2 方法区与元空间) ★★★
    • [3.3 直接内存](#3.3 直接内存)
  4. 主线流程剖析:一个对象的内存之旅 ★★★
    • [4.1 运行时数据区全景图](#4.1 运行时数据区全景图)
    • [4.2 类加载完成后,各部分内容去哪儿](#4.2 类加载完成后,各部分内容去哪儿)
    • [4.3 new 一个对象的分配全流程](#4.3 new 一个对象的分配全流程) ★★★
    • [4.4 对象在堆里的内存布局](#4.4 对象在堆里的内存布局)
    • [4.5 对象的晋升之路与区域联动](#4.5 对象的晋升之路与区域联动)
  5. 内存参数速查与观测命令
    • [5.1 JVM 内存参数总表](#5.1 JVM 内存参数总表)
    • [5.2 观测命令速查](#5.2 观测命令速查)
  6. 扩展点与常见问题
    • [6.1 扩展点](#6.1 扩展点)
    • [6.2 常见问题 FAQ](#6.2 常见问题 FAQ)
    • [6.3 参考资料](#6.3 参考资料)

1. 核心理论与定位

1.1 运行时数据区全景总表

关键认知:JVM 内存结构由《Java 虚拟机规范》定义,但「规范区域」和「HotSpot 实现细节」是两回事------规范说方法区,HotSpot 8 用**元空间(Metaspace)**实现;规范没提直接内存,但它照样吃进程内存并会 OOM。面试与排障时按「实现」理解,写文章时按「规范」表述。

按线程归属划分,Java 8 的运行时数据区如下:

区域 线程归属 职责 / 存放内容 容量特征(Java 8 HotSpot) 异常形态
程序计数器 PC 私有 当前线程正在执行的字节码行号;native 方法时为 undefined 极小,几乎不讨论 ❌ 无 OOM(规范唯一不抛 OOM 的区域)
Java 虚拟机栈 私有 方法调用产生的栈帧(局部变量表、操作数栈等) 默认约 1MB/线程,参数 -Xss StackOverflowError;动态扩容失败时 OutOfMemoryError
本地方法栈 私有 native 方法(JNI)调用栈 与虚拟机栈相似,HotSpot 直接合并实现 同上
堆 Heap 共享 所有对象实例与数组(含字符串对象、StringTable) 默认上限 ≈ 物理内存 1/4,参数 -Xms/-Xmx/-Xmn OutOfMemoryError: Java heap space
方法区 → 元空间 Metaspace 共享 类元数据(Klass 结构)、方法字节码、运行时常量池(类文件常量池解析后)、字段/方法信息 默认无上限 (只受本地内存约束),参数 -XX:MaxMetaspaceSize OutOfMemoryError: Metaspace
直接内存(堆外) 共享(非规范区域) DirectByteBuffer 等 NIO 堆外缓冲区 默认上限 = -Xmx,参数 -XX:MaxDirectMemorySize OutOfMemoryError: Direct buffer memory
代码缓存 CodeCache(HotSpot 私有) 共享 JIT 编译后的机器码、内联缓存 默认 240MB,参数 -XX:ReservedCodeCacheSize CodeCache is full(编译器停摆告警)

GC 视角的第一性划分只有两条:

text 复制代码
线程私有区(栈、PC、本地方法栈)  → 随线程生灭,不需 GC,溢出即 SOE
线程共享区(堆、元空间、直接内存)→ 由 GC 负责回收;堆是 GC 主战场,元空间回收=类卸载

1.2 Java 8 内存模型变革:永久代 → 元空间

Java 8 最大的结构性变化是:永久代(PermGen)被彻底移除,类元数据改存本地内存中的元空间(Metaspace)。顺带迁走的还有两个"户口":

内容 JDK 6 及以前 JDK 7 JDK 8(本文基线)
类元数据(Klass、方法字节码...) 永久代 永久代 元空间(本地内存,不再受堆上限约束)
字符串常量池 StringTable 永久代 移入堆
静态变量(Class 镜像对象字段) 永久代 移入堆
运行时常量池 永久代(方法区) 永久代(方法区) 元空间
永久代相关参数 -XX:PermSize/-XX:MaxPermSize 同左 已移除(设置后告警忽略)
常见错误 OutOfMemoryError: PermGen space PermGen space(字符串相关已消失) OutOfMemoryError: Metaspace

关键认知 :Java 7 把「字符串常量池 + 静态变量」先搬进堆,Java 8 再端掉永久代本体,两步合起来 = 永久代在 Java 8 清零。从此只有堆 + 元空间(本地内存)两种共享区 ,「JVM 内存在 Xmx 内」的印象需要修正------元空间与直接内存在 -Xmx 之外按需吃掉系统内存。

为什么废除永久代,官方与社区共识集中在四点:

  • 永久代大小难调 :默认 82MB(-XX:MaxPermSize),动态加载类的应用(热部署、动态代理、Groovy/脚本)极易 PermGen space,调大又怕挤占堆;
  • 回收效率差:永久代回收需要 Full GC 顺带执行,卸载判定苛刻,长期驻留的类元数据变成"伪泄漏";
  • 规范与实现耦合:永久代把「概念上的方法区」和「GC 分代」绑死,而方法区在规范上本就不必纳入堆;
  • 为 HotSpot 与 JRockit 合并铺路(JEP 122),元空间用本地内存 + 独立回收策略,类元数据上限只受 OS 内存约束,开发者不再为 PermSize 头疼。

1.3 规范区域之外:直接内存与代码缓存

《JVM 规范》只承认 PC / 栈 / 本地方法栈 / 堆 / 方法区五块,但真实进程里还有两个高频出镜的非规范区:

  • 直接内存 :由 sun.misc.Unsafe#allocateMemory 与 NIO ByteBuffer.allocateDirect 分配,不受堆大小限制,随进程常驻(见 3.3);
  • 代码缓存 CodeCache :JIT 编译产物存放地,-XX:ReservedCodeCacheSize 默认 240MB,编译压力大(热代码多、分层编译)时可能打满,表现为"满后不再编译、应用性能骤降"------注意它和 Metaspace 里的 Compressed Class Space(类指针压缩区,默认 1GB)是两个概念,别混。

一句话概括:面试问"JVM 内存分几块"答规范五区,面试问"哪些会 OOM"把直接内存和代码缓存也算上,会显得比八股深一层。


2. 线程私有区

三个线程私有区随线程创建而生、线程销毁而灭,不需要 GC。它们合起来支撑"一次方法调用"的上下文。

2.1 程序计数器

  • 每个线程一个,记录当前线程正在执行的字节码指令地址(行号)
  • 执行 native 方法时值为 undefined(无需记录);
  • 它是唯一一个规范规定不会 OOM 的区域
  • 作用机理:线程切换(上下文切换)后靠它恢复到正确的执行位置,所以它本质是"线程的进度指针",各线程互不干扰。
java 复制代码
// 反编译一个简单方法即可看到行号与指令一一对应
// javap -c -v ClassName 中的 line number table 就是 PC 的"剧本"
public class PcDemo {
    public static int add(int a, int b) {
        return a + b;   // 字节码: iload_0 / iload_1 / iadd / ireturn
    }
}
bash 复制代码
javac PcDemo.java && javap -c PcDemo   # 查看字节码助记符与行号映射

2.2 Java 虚拟机栈

栈 = 线程私有,容量默认约 1MB(64 位平台,-Xss 可调) 。每发起一次方法调用,JVM 就压入一个栈帧,方法返回(正常 return 或抛异常)即弹出。栈帧内部结构:

栈帧组成 作用 备注
局部变量表 存放方法参数与局部变量,基本单位为 Slot(4 字节),long/double 占 2 个 Slot 引用类型存的是"指向堆对象/常量池的引用",不是对象本身
操作数栈 字节码运算的工作台:iadd 从栈顶取两个操作数、压回结果 深度编译期已确定
动态链接 指向运行时常量池中本方法的符号引用,运行时解析为直接引用(支持多态分派的关键) "静态链接"在类加载期已解析
方法返回地址 正常返回时恢复调用者 PC;异常时通过异常表兜底 异常表中没有匹配处理器则向上抛

StackOverflowError 触发演示(无限递归,每层递归压入一个栈帧):

java 复制代码
// StackOverflowDemo.java ------ Java 8 直接可运行
public class StackOverflowDemo {
    private static int depth = 0;

    static void recurse() {
        depth++;        // 记录当前递归深度
        recurse();      // 每调用一次 → 压入一个新栈帧
    }

    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError @ 深度 " + depth);
        }
    }
}
bash 复制代码
# 默认栈(约 1MB):能递归 1 万次左右(结果因平台而异)
java StackOverflowDemo
# 压小栈到 256KB:递归深度骤降
java -Xss256k StackOverflowDemo
# 对比:java -XX:+PrintFlagsFinal -version | grep ThreadStackSize 可查本机默认值(KB)

关键认知 :方法内声明一个 byte[1MB] 局部变量存的是引用(进堆),只有递归、长调用链、过深表达式才会真正吃栈 。所以生产上 -Xss 很少调大------单线程栈开得越大,同内存下可创建的线程数越少(线程数 ≈ (进程可用内存 - 堆 - 元空间) / 栈大小),高并发线程池服务切忌盲目调大 -Xss

栈区两种错误形态要分清:

场景 报错 本质
递归无限 / 调用链过深 StackOverflowError 容量不足(固定栈,最常见)
线程数暴增且栈允许动态扩展 OutOfMemoryError: unable to create native thread 创建线程时系统内存耗尽,多半发生在 Linux 进程级 ulimit 或 OS 内存层,而非 JVM 参数层

2.3 本地方法栈

  • 服务 JNI 调用的 native 方法(C/C++ 实现),例如 Object.hashCode() 的早期实现、System.currentTimeMillis()、Java 8 前的 Thread.start0 等;
  • 《规范》允许实现方将它与虚拟机栈合并------HotSpot 就是这么干的-Xss 同时作用于两者),所以平时看不到独立的本地方法栈配置;
  • 溢出形态与虚拟机栈相同:StackOverflowError / 创建线程失败 OOM。

3. 线程共享区

3.1 堆

堆是 GC 的主战场、对象实例的唯一归宿(有逃逸分析例外,见 FAQ Q8) 。Java 8 默认收集器为 Parallel Scavenge + Parallel Old(ParallelGC),堆按"分代收集"理论切成两代四块:
#mermaid-svg-FtNNLELBAh9KM3ah{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FtNNLELBAh9KM3ah .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FtNNLELBAh9KM3ah .error-icon{fill:#552222;}#mermaid-svg-FtNNLELBAh9KM3ah .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FtNNLELBAh9KM3ah .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FtNNLELBAh9KM3ah .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FtNNLELBAh9KM3ah .marker.cross{stroke:#333333;}#mermaid-svg-FtNNLELBAh9KM3ah svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FtNNLELBAh9KM3ah p{margin:0;}#mermaid-svg-FtNNLELBAh9KM3ah .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FtNNLELBAh9KM3ah .cluster-label text{fill:#333;}#mermaid-svg-FtNNLELBAh9KM3ah .cluster-label span{color:#333;}#mermaid-svg-FtNNLELBAh9KM3ah .cluster-label span p{background-color:transparent;}#mermaid-svg-FtNNLELBAh9KM3ah .label text,#mermaid-svg-FtNNLELBAh9KM3ah span{fill:#333;color:#333;}#mermaid-svg-FtNNLELBAh9KM3ah .node rect,#mermaid-svg-FtNNLELBAh9KM3ah .node circle,#mermaid-svg-FtNNLELBAh9KM3ah .node ellipse,#mermaid-svg-FtNNLELBAh9KM3ah .node polygon,#mermaid-svg-FtNNLELBAh9KM3ah .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FtNNLELBAh9KM3ah .rough-node .label text,#mermaid-svg-FtNNLELBAh9KM3ah .node .label text,#mermaid-svg-FtNNLELBAh9KM3ah .image-shape .label,#mermaid-svg-FtNNLELBAh9KM3ah .icon-shape .label{text-anchor:middle;}#mermaid-svg-FtNNLELBAh9KM3ah .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FtNNLELBAh9KM3ah .rough-node .label,#mermaid-svg-FtNNLELBAh9KM3ah .node .label,#mermaid-svg-FtNNLELBAh9KM3ah .image-shape .label,#mermaid-svg-FtNNLELBAh9KM3ah .icon-shape .label{text-align:center;}#mermaid-svg-FtNNLELBAh9KM3ah .node.clickable{cursor:pointer;}#mermaid-svg-FtNNLELBAh9KM3ah .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FtNNLELBAh9KM3ah .arrowheadPath{fill:#333333;}#mermaid-svg-FtNNLELBAh9KM3ah .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FtNNLELBAh9KM3ah .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FtNNLELBAh9KM3ah .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FtNNLELBAh9KM3ah .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FtNNLELBAh9KM3ah .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FtNNLELBAh9KM3ah .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FtNNLELBAh9KM3ah .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FtNNLELBAh9KM3ah .cluster text{fill:#333;}#mermaid-svg-FtNNLELBAh9KM3ah .cluster span{color:#333;}#mermaid-svg-FtNNLELBAh9KM3ah div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FtNNLELBAh9KM3ah .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FtNNLELBAh9KM3ah rect.text{fill:none;stroke-width:0;}#mermaid-svg-FtNNLELBAh9KM3ah .icon-shape,#mermaid-svg-FtNNLELBAh9KM3ah .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FtNNLELBAh9KM3ah .icon-shape p,#mermaid-svg-FtNNLELBAh9KM3ah .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FtNNLELBAh9KM3ah .icon-shape .label rect,#mermaid-svg-FtNNLELBAh9KM3ah .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FtNNLELBAh9KM3ah .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FtNNLELBAh9KM3ah .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FtNNLELBAh9KM3ah :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 堆 Heap(Java 8 分代结构)
新生代 Young 1/3 堆
大对象直接入老年代
Eden

新对象首选地
Survivor 0
Survivor 1
老年代 Old

2/3 堆

分区 默认占比 存放对象 回收动作
Eden 新生代的 8/10(SurvivorRatio=8 绝大多数新分配对象(先过 TLAB,见 4.3) Minor GC(新生代回收)主产区
Survivor S0/S1 各 1/10,同时只用一个(复制算法) Minor GC 后存活对象被复制到空的那块 每次 Minor GC 存活者 age+1,两块来回倒
老年代 Old 堆的 2/3(NewRatio=2 熬过多次 Minor GC 的长寿对象、大对象、分配担保下车的对象 Major/Full GC,标记整理

Java 8 默认堆参数推导链(可直接用 java -XX:+PrintFlagsFinal -version 验证本机值):

参数 默认(Java 8 x64) 说明
-Xms ≈ 物理内存 1/64 初始堆大小,启动即预留的内存
-Xmx ≈ 物理内存 1/4 最大堆上限
新生代 -Xmn 未设时为堆的 1/3 NewRatio=2 → 老:新 = 2:1
Eden:S0:S1 8:1:1(SurvivorRatio=8 Survivor 两块各占新生 1/10

Java heap space OOM 演示(配合堆转储,可复现可分析):

java 复制代码
// HeapOomDemo.java ------ java -Xms32m -Xmx32m -XX:+HeapDumpOnOutOfMemoryError HeapOomDemo
import java.util.ArrayList;
import java.util.List;

public class HeapOomDemo {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();   // list 被 main 的局部变量表引用 → GC Roots 可达,永不可回收
        while (true) {
            list.add(new byte[1024 * 1024]);     // 每次吞 1MB,32MB 堆 30 次左右即耗尽
        }
    }
}
bash 复制代码
javac HeapOomDemo.java
java -Xms32m -Xmx32m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. HeapOomDemo

# 关键报错(Java 8 默认 ParallelGC 输出形态)
# Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
# 	at HeapOomDemo.main(HeapOomDemo.java:7)
# 	...已生成 java_pidXXXX.hprof,可用 MAT / jhat / VisualVM 分析

生产建议一句话版:

堆太小 → 频繁 GC + OOM;堆太大 → Full GC 单次停顿长、系统内存被挤占。经验基准:先按最大负载的 1.5~2 倍估算活跃对象存量,-Xms-Xmx 设为相同值(防运行期动态伸缩带来 GC 抖动),再配合 GC 日志微调。

3.2 方法区与元空间

《规范》中方法区存放:类型信息(类/接口/枚举...)、字段与方法信息(含字节码)、运行时常量池、静态变量(规范语义)、类引用 。HotSpot 8 用元空间实现方法区:

元空间要点 Java 8 实现细节
物理位置 本地内存(Native Memory) ,不在 -Xmx 管辖内
默认上限 -XX:MaxMetaspaceSize 默认 0 = 不设上限,只受操作系统内存约束(→ 有泄漏风险,见 FAQ Q6)
初始高水位 -XX:MetaspaceSize ≈ 20.8~21MB:超过后 GC 会"介入"(按需扩容,每次扩容伴随一次 Full GC)
类指针压缩区 -XX:CompressedClassSpaceSize 默认 1GB:开启 UseCompressedClassPointers 时类元数据(Klass)放入该独立区,其余(方法字节码、注解等)仍归 Metaspace 主体
回收 由 Full GC 顺带执行:类加载器及其加载的所有类都不可达时,对应元空间才可卸载
OOM 形态 OutOfMemoryError: Metaspace

Metaspace OOM 演示(JDK 自带工具即可,无需第三方库------用 JavaCompiler 反复编译新源码生成新类):

java 复制代码
// MetaspaceOomDemo.java ------ 反复用新类加载器加载新类,逼爆元空间
// java -XX:MaxMetaspaceSize=64m MetaspaceOomDemo
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.net.URL;
import java.net.URLClassLoader;

public class MetaspaceOomDemo {
    static int count = 0;

    public static void main(String[] args) throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        File srcDir = new File("./_gen");
        srcDir.mkdirs();
        while (true) {
            String cls = "Gen" + (count++);
            File src = new File(srcDir, cls + ".java");
            try (FileWriter fw = new FileWriter(src)) {
                fw.write("public class " + cls + " { public static String hello() { return \"hi\"; } }");
            }
            if (compiler.run(null, null, null, src.getPath()) != 0) continue;
            try (URLClassLoader loader = new URLClassLoader(new URL[]{srcDir.toURI().toURL()})) {
                loader.loadClass(cls).getMethod("hello").invoke(null);   // 触发类加载 → 元空间写入
            }                                                             // loader 引用丢弃 → 可被卸载(这里故意循环太快不触发)
        }
    }
}
bash 复制代码
javac MetaspaceOomDemo.java
java -XX:MaxMetaspaceSize=64m MetaspaceOomDemo
# 报错:java.lang.OutOfMemoryError: Metaspace

两个"常量池"必须分清------它们是八股最高频的混淆点:

Class 文件常量池 运行时常量池 字符串常量池 StringTable
存在哪 .class 文件里(磁盘/静态) 元空间(类加载后解析) (JDK 7 起)
内容 编译期确定的字面量与符号引用(类名、字段名、方法签名) 由前者在类加载时载入并逐步把符号引用解析为直接引用;同时保存 intern 字符串的引用 全局哈希表,只存字符串对象的引用,key 为字符串内容
触发 javac 类加载 + 运行时解析 首次出现字面量 / 显式 intern()

演进史与 intern() 语义差异(Java 8 的答案):

java 复制代码
// InternDemo.java ------ JDK 8 输出 true / true;若在 JDK 6 上运行则是 false / false
public class InternDemo {
    public static void main(String[] args) {
        String s = new String("a") + new String("b");  // 堆上 new 出 "ab",与池无关
        String intern = s.intern();                    // 池中无 "ab" → JDK6: 复制一份进永久代;
                                                       //          JDK8: 直接把 s 的引用记入池(不复制)
        String literal = "ab";                         // 字面量命中池 → 拿到与 intern 同一引用
        System.out.println(s == intern);   // JDK8: true(引用了同一个堆对象)
        System.out.println(s == literal);  // JDK8: true
    }
}
JDK 版本 StringTable 所在 intern 行为 后果
≤ 6 永久代 复制字符串副本入池,池对象 ≠ 堆对象 大量 intern → PermGen space
7 池无则记录堆对象引用,不复制 溢出变为 Java heap space,不再是 PermGen
8(本文) 同 JDK 7 大量 intern 撑爆的是

关键认知StringTableSize 在 Java 8 默认 60013 个桶;大量 intern()/动态字符串拼接且常驻的场景会同时放大堆占用与哈希冲突------除非做池化缓存(如缓存 Key),否则生产慎用 intern。

3.3 直接内存

  • NIO 的 ByteBuffer.allocateDirect()Unsafe.allocateMemory() 分配,绕过堆、直接向 OS 申请,规避堆内缓冲区的两次拷贝(堆 ↔ 直接内存),Netty 大量使用;
  • 上限:-XX:MaxDirectMemorySize 默认 0 → 等于 -Xmx(注意:不是无限);
  • GC 与它若即若离:DirectByteBuffer 本身是堆上的小对象,其 Cleaner(虚引用)在堆 GC 时触发,进而释放堆外内存;堆外内存不足时会主动触发 System.gc() 兜底(-XX:+DisableExplicitGC 场景下此路不通,是 Netty 类应用 OOM 的经典坑)。
java 复制代码
// DirectBufferDemo.java ------ java -XX:MaxDirectMemorySize=32m DirectBufferDemo
import java.nio.ByteBuffer;

public class DirectBufferDemo {
    public static void main(String[] args) {
        while (true) {
            ByteBuffer.allocateDirect(8 * 1024 * 1024);   // 每次 8MB 堆外,绕过 -Xmx
        }
    }
}
// 报错:java.lang.OutOfMemoryError: Direct buffer memory

排查信号:Java heap 很小、GC 也正常,但进程 RSS(常驻内存)持续高企------优先怀疑直接内存 / Metaspace / JIT 代码缓存这类堆外占用。


4. 主线流程剖析:一个对象的内存之旅

4.1 运行时数据区全景图

#mermaid-svg-6tV8z956kFkfDsnn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6tV8z956kFkfDsnn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6tV8z956kFkfDsnn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6tV8z956kFkfDsnn .error-icon{fill:#552222;}#mermaid-svg-6tV8z956kFkfDsnn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6tV8z956kFkfDsnn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6tV8z956kFkfDsnn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6tV8z956kFkfDsnn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6tV8z956kFkfDsnn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6tV8z956kFkfDsnn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6tV8z956kFkfDsnn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6tV8z956kFkfDsnn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6tV8z956kFkfDsnn .marker.cross{stroke:#333333;}#mermaid-svg-6tV8z956kFkfDsnn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6tV8z956kFkfDsnn p{margin:0;}#mermaid-svg-6tV8z956kFkfDsnn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6tV8z956kFkfDsnn .cluster-label text{fill:#333;}#mermaid-svg-6tV8z956kFkfDsnn .cluster-label span{color:#333;}#mermaid-svg-6tV8z956kFkfDsnn .cluster-label span p{background-color:transparent;}#mermaid-svg-6tV8z956kFkfDsnn .label text,#mermaid-svg-6tV8z956kFkfDsnn span{fill:#333;color:#333;}#mermaid-svg-6tV8z956kFkfDsnn .node rect,#mermaid-svg-6tV8z956kFkfDsnn .node circle,#mermaid-svg-6tV8z956kFkfDsnn .node ellipse,#mermaid-svg-6tV8z956kFkfDsnn .node polygon,#mermaid-svg-6tV8z956kFkfDsnn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6tV8z956kFkfDsnn .rough-node .label text,#mermaid-svg-6tV8z956kFkfDsnn .node .label text,#mermaid-svg-6tV8z956kFkfDsnn .image-shape .label,#mermaid-svg-6tV8z956kFkfDsnn .icon-shape .label{text-anchor:middle;}#mermaid-svg-6tV8z956kFkfDsnn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6tV8z956kFkfDsnn .rough-node .label,#mermaid-svg-6tV8z956kFkfDsnn .node .label,#mermaid-svg-6tV8z956kFkfDsnn .image-shape .label,#mermaid-svg-6tV8z956kFkfDsnn .icon-shape .label{text-align:center;}#mermaid-svg-6tV8z956kFkfDsnn .node.clickable{cursor:pointer;}#mermaid-svg-6tV8z956kFkfDsnn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6tV8z956kFkfDsnn .arrowheadPath{fill:#333333;}#mermaid-svg-6tV8z956kFkfDsnn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6tV8z956kFkfDsnn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6tV8z956kFkfDsnn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6tV8z956kFkfDsnn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6tV8z956kFkfDsnn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6tV8z956kFkfDsnn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6tV8z956kFkfDsnn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6tV8z956kFkfDsnn .cluster text{fill:#333;}#mermaid-svg-6tV8z956kFkfDsnn .cluster span{color:#333;}#mermaid-svg-6tV8z956kFkfDsnn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6tV8z956kFkfDsnn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6tV8z956kFkfDsnn rect.text{fill:none;stroke-width:0;}#mermaid-svg-6tV8z956kFkfDsnn .icon-shape,#mermaid-svg-6tV8z956kFkfDsnn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6tV8z956kFkfDsnn .icon-shape p,#mermaid-svg-6tV8z956kFkfDsnn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6tV8z956kFkfDsnn .icon-shape .label rect,#mermaid-svg-6tV8z956kFkfDsnn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6tV8z956kFkfDsnn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6tV8z956kFkfDsnn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6tV8z956kFkfDsnn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地内存(OS 层,-Xmx 之外)
引用
加载类
线程私有区(随线程生灭)
程序计数器

字节码行号
虚拟机栈(-Xss ≈1MB)

栈帧:局部变量表/操作数栈

动态链接/返回地址
本地方法栈

HotSpot 中与虚拟机栈合并
堆 Heap(-Xms / -Xmx)
Minor GC 复制存活对象 age+1
age 达标 / 动态年龄
TLAB 溢出对象、大对象
Eden
Survivor S0 / S1

互相倒换
Old
Metaspace 元空间

类元数据 / 运行时常量池

-XX:MaxMetaspaceSize(默认无上限)
直接内存

NIO DirectByteBuffer

默认上限 = -Xmx
Compressed Class Space

默认 1GB
CodeCache 代码缓存

JIT 机器码 默认 240MB
Java 程序 main 线程启动

4.2 类加载完成后,各部分内容去哪儿

Person p = new Person() 第一次触发 Person 类加载为例(类加载三阶段:加载 → 链接验证/准备/解析 → 初始化),加载完成后各内容的落点:

类中的内容 落点(Java 8) 说明
类元数据(Klass 结构、字段方法描述、字节码) 元空间(含 Compressed Class Space 部分) 类的"图纸"
符号引用 → 直接引用(解析阶段) 元空间(运行时常量池) 准备阶段已为静态字段赋零值
字符串字面量 "张三" 值对象在 + StringTable(堆)记录引用 首次遇到即 intern
静态变量 static Person sp java.lang.Class 镜像对象内部的字段区) JDK 7 前在永久代,旧书说法已过时
java.lang.Class 实例(Person.class 堆(元空间 Klass 的镜像,oops 层) 反射入口

4.3 new 一个对象的分配全流程

Person p = new Person() 为例,new 字节码之后的完整动作:
#mermaid-svg-ViXzZGWKsIyDYIpg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ViXzZGWKsIyDYIpg .error-icon{fill:#552222;}#mermaid-svg-ViXzZGWKsIyDYIpg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ViXzZGWKsIyDYIpg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ViXzZGWKsIyDYIpg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ViXzZGWKsIyDYIpg .marker.cross{stroke:#333333;}#mermaid-svg-ViXzZGWKsIyDYIpg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ViXzZGWKsIyDYIpg p{margin:0;}#mermaid-svg-ViXzZGWKsIyDYIpg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg .cluster-label text{fill:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg .cluster-label span{color:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg .cluster-label span p{background-color:transparent;}#mermaid-svg-ViXzZGWKsIyDYIpg .label text,#mermaid-svg-ViXzZGWKsIyDYIpg span{fill:#333;color:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg .node rect,#mermaid-svg-ViXzZGWKsIyDYIpg .node circle,#mermaid-svg-ViXzZGWKsIyDYIpg .node ellipse,#mermaid-svg-ViXzZGWKsIyDYIpg .node polygon,#mermaid-svg-ViXzZGWKsIyDYIpg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ViXzZGWKsIyDYIpg .rough-node .label text,#mermaid-svg-ViXzZGWKsIyDYIpg .node .label text,#mermaid-svg-ViXzZGWKsIyDYIpg .image-shape .label,#mermaid-svg-ViXzZGWKsIyDYIpg .icon-shape .label{text-anchor:middle;}#mermaid-svg-ViXzZGWKsIyDYIpg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ViXzZGWKsIyDYIpg .rough-node .label,#mermaid-svg-ViXzZGWKsIyDYIpg .node .label,#mermaid-svg-ViXzZGWKsIyDYIpg .image-shape .label,#mermaid-svg-ViXzZGWKsIyDYIpg .icon-shape .label{text-align:center;}#mermaid-svg-ViXzZGWKsIyDYIpg .node.clickable{cursor:pointer;}#mermaid-svg-ViXzZGWKsIyDYIpg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ViXzZGWKsIyDYIpg .arrowheadPath{fill:#333333;}#mermaid-svg-ViXzZGWKsIyDYIpg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ViXzZGWKsIyDYIpg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ViXzZGWKsIyDYIpg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ViXzZGWKsIyDYIpg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ViXzZGWKsIyDYIpg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ViXzZGWKsIyDYIpg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ViXzZGWKsIyDYIpg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ViXzZGWKsIyDYIpg .cluster text{fill:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg .cluster span{color:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ViXzZGWKsIyDYIpg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ViXzZGWKsIyDYIpg rect.text{fill:none;stroke-width:0;}#mermaid-svg-ViXzZGWKsIyDYIpg .icon-shape,#mermaid-svg-ViXzZGWKsIyDYIpg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ViXzZGWKsIyDYIpg .icon-shape p,#mermaid-svg-ViXzZGWKsIyDYIpg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ViXzZGWKsIyDYIpg .icon-shape .label rect,#mermaid-svg-ViXzZGWKsIyDYIpg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ViXzZGWKsIyDYIpg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ViXzZGWKsIyDYIpg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ViXzZGWKsIyDYIpg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 小对象(首选)
TLAB 放不下
大对象 或 配置 PretenureSizeThreshold
① 类加载检查

常量池定位符号引用

类未加载则加载

(元数据进 Metaspace)
② 计算对象大小

对象头 + 实例字段

(对齐 8 字节)
③ 分配内存
TLAB 线程本地分配缓冲

Eden 内线程私有小块

无锁 CAS 分配
直接在 Eden 分配

(指针碰撞)
老年代
④ 内存空间清零

实例字段零值
⑤ 设置对象头

Mark Word:hashCode/GC分代年龄/锁标志

Klass 指针
⑥ 栈上压入引用

(构造方法 init 随后执行)
⑦ p 在局部变量表

GC Roots 可达 → 存活

各步骤的底层要点:

步骤 机制 关键细节
① 类加载检查 运行时常量池中类符号引用是否已解析 未加载则触发加载→链接→初始化(首次 new 有额外开销)
② 大小计算 对象头 + 实例字段对齐 开启压缩指针后普通对象头 12B,对齐粒度 8B(见 4.4)
③ 分配方式 TLAB(默认开启)→ Eden 兜底;指针碰撞 vs 空闲列表 堆规整(压缩 GC 后)用指针碰撞 Bump-the-Pointer;Serial/Parallel 之外使用空闲列表(CMS)
③-大对象 -XX:PretenureSizeThreshold(默认 0 不启用) Parallel/CMS 收集器下有效;避免大对象在新生代来回复制
④ 清零 分配即对实例字段置零值 所以 Java 字段天然有默认值(0 / null / false)
⑤ 对象头 Mark Word + Klass Pointer Mark Word 记录 hashcode、GC 年龄、偏向锁/轻量锁/重量锁标志------这是"无锁→偏向→轻量→重量"膨胀链路的内存载体
⑥⑦ 引用入栈 栈帧局部变量表持有引用 引用在栈、对象在堆,GC Roots 从此可达(见 4.5 与 FAQ Q9)

4.4 对象在堆里的内存布局

HotSpot 中堆内一个普通对象的布局 = 对象头 + 实例数据 + 对齐填充

组成 大小(64 位 JVM) 内容
Mark Word 8 字节 hashCode、GC 分代年龄(4bit)、锁状态位(偏向/轻量/重量)、是否可偏向
Klass Pointer 压缩开启 4B / 关闭 8B 指向元空间 Klass(UseCompressedClassPointers
数组长度(仅数组) 4 字节 int[] 等数组对象才有
实例数据 字段对齐排布 -XX:FieldsAllocationStyle 重排:long/double → int → short/char → byte/boolean → 引用
对齐填充 补到 8 的倍数 Object obj = new Object() 开压缩后 = 8+4+4(填充)= 16 字节

压缩指针(Compressed Oops) :堆 ≤ 32GB 时 -XX:+UseCompressedOops 默认开启,引用用 32 位表示、寻址时左移 3 位(对象 8 字节对齐),堆内引用瘦身一半。推论:堆 > 32GB 后压缩失效,指针回到 8 字节,内存占用反而可能出现"跳变",且对象头变大------大量 Java 应用把 -Xmx 卡在 30G 左右正是为此(FAQ Q10)。

经典问题回顾:new Object() 占多少内存?压缩指针开启(堆 ≤ 32G):12B 对象头 + 4B 对齐填充 = 16B;压缩指针关闭:对象头本身 16B(Mark Word 8 + Klass 8),已对齐无需填充 = 16B。两类计算题答案随配置变化,作答前先声明"是否开启压缩指针"。

4.5 对象的晋升之路与区域联动

对象分配后,它的一生与 GC、区域流转绑定:

text 复制代码
new 成功(TLAB/Eden)
   │
   ▼
Minor GC:存活对象复制到空 Survivor,age + 1;死对象随 Eden 整体清空
   │(Eden + 在用 Survivor 是"垃圾区",复制算法只扫存活对象,效率高)
   ▼
反复 Minor GC:age ≥ MaxTenuringThreshold(15) → 晋升老年代
       或:动态年龄判定(Survivor 中同龄对象总和 > Survivor 一半 → 这批全晋升)
       或:Survivor 装不下 → 直接进老年代(含大对象若配 PretenureSizeThreshold)
   ▼
老年代空间不足 → Major/Full GC(标记-整理;CMS 为标记-清除+并发)
   ▼
Full GC 仍不足 → OutOfMemoryError: Java heap space

Minor GC 前的空间分配担保(JDK 6u24 之后规则):

老年代最大连续可用空间 ≥ 新生代对象总大小 ,或 ≥ 历次晋升对象的平均大小 → 放心 Minor GC("冒险");否则转为 Full GC(安全失败)。一句话:Minor GC 兜底能力看老年代剩余连续空间


5. 内存参数速查与观测命令

5.1 JVM 内存参数总表

参数 Java 8 默认 作用 调优提示
-Xms<size> 物理内存 ≈1/64 初始堆大小 生产 = -Xmx,避免启动后扩容停顿
-Xmx<size> 物理内存 ≈1/4 最大堆 32G 以内保住压缩指针收益
-Xmn<size> 自动(≈堆 1/3) 新生代大小 设固定值可省去 GC 自适应调整
-XX:NewRatio=n 2 老:新 = n:1 与大堆+大对象场景配合
-XX:SurvivorRatio=n 8 Eden:S0:S1 = n:1:1 Survivor 太小→对象直接进老年代
-Xss<size> ≈1MB(x64) 单线程栈大小 高线程数应用不要调大
-XX:MetaspaceSize ≈21MB 元空间初始高水位 触发首次 Full GC 的阈值,可抬升减少前期 Full GC
-XX:MaxMetaspaceSize 0(无上限) 元空间上限 生产建议显式设置,防类泄漏拖垮机器
-XX:CompressedClassSpaceSize 1GB 类指针压缩区上限 类极多时按需调大
-XX:MaxDirectMemorySize 0(= -Xmx 直接内存上限 Netty 场景显式设置并按堆外用量估算
-XX:MaxTenuringThreshold 15 晋升年龄阈值 并行收集器有效
-XX:PretenureSizeThreshold 0(关闭) 超过该大小的对象直接入老年代 UseParallelGC/CMS 搭配,G1 无效
-XX:StringTableSize 60013 StringTable 桶数 intern 大量时调大减少冲突
-XX:+UseCompressedOops true(堆 ≤32G) 压缩对象引用 别超 32G
-XX:+UseTLAB true 线程本地分配缓冲 小对象分配性能关键
-XX:ReservedCodeCacheSize 240MB CodeCache 上限 JIT 编译量大时调大
-XX:+PrintGCDetails / -XX:+PrintGCDateStamps false 输出 GC 日志 Java 8 常用,9 起改 -Xlog:gc*
-XX:+HeapDumpOnOutOfMemoryError false OOM 时自动 dump hprof 生产必开,配合 -XX:HeapDumpPath
-XX:NativeMemoryTracking=summary off 开启 NMT 跟踪堆外内存 排查堆外泄漏利器

校验本机真实默认值(任意 Java 版本通用):

java -XX:+PrintFlagsFinal -version | grep -E 'ThreadStackSize|MaxHeapSize|MetaspaceSize|SurvivorRatio|NewRatio'

5.2 观测命令速查

以演示进程为例(后台跑个常驻 Java 程序):

bash 复制代码
# 1. 找进程
jps -l                          # 输出: 23456 com.example.MemDemo

# 2. 每 1 秒看一次 GC 与各区用量(jstat -gc <pid> <间隔ms> [次数])
jstat -gc 23456 1000 5

jstat -gc 字段解读(Java 8 特有 MC/MU/CCSC/CCSU 即元空间与压缩类空间):

字段 含义 字段 含义
S0C/S1C/S0U/S1U Survivor 容量/使用 EC/EU Eden 容量/使用
OC/OU 老年代容量/使用 MC/MU Metaspace 容量/使用
CCSC/CCSU 压缩类空间容量/使用 YGC/YGCT Young GC 次数/耗时(秒)
FGC/FGCT Full GC 次数/耗时(秒) GCT GC 总耗时
bash 复制代码
# 3. 堆与 GC 配置总览
jmap -heap 23456              # 堆内存配置 + 各分代当前使用量
jmap -clstats 23456           # 类加载器统计:加载类数 × 元空间字节(排查 Metaspace 泄漏)

# 4. 查/改运行参数
jcmd 23456 VM.flags           # 当前生效的关键参数(含用户未显式设置的默认值)
jinfo 23456                   # 同 VM.flags,输出 Java System Properties

# 5. 堆外内存跟踪(需启动参数开启)
# java -XX:NativeMemoryTracking=summary MemDemo
jcmd 23456 VM.native_memory summary
# 输出分项:Java Heap / Class(元空间) / Thread / Code / GC / Internal ------ 逐项对比定位堆外大头

常见判读组合拳:jstat -gc 看到 MC 持续上涨且 FGC 频繁 → 怀疑类加载泄漏;jmap -heap 各代正常但进程 RSS 远超 -Xmx → 查 Native Memory Tracking 与直接内存;jmap -clstats 类加载器数量只增不减 → 定位到未回收的加载器。


6. 扩展点与常见问题

6.1 扩展点

本文只覆盖「内存长什么样」。内存之上值得顺藤摸瓜的进阶话题:

  • GC 三兄弟:引用计数不可行 → 可达性分析,GC Roots 从哪些区域出发(栈引用、静态字段、常量池引用、JNI 引用、活跃线程...)------与本文 2、3 章区域直接挂钩;
  • 各收集器与内存结构的交互:Parallel 的指针碰撞、CMS 的空闲列表、G1 的 Region 化彻底改写 3.1 的"物理分代"描述;G1 下堆不再按 Eden/S0/S1/Old 连续排布,但逻辑分代仍在;
  • 逃逸分析 / 标量替换 / 锁消除:让"对象一定在堆"不再绝对成立(FAQ Q8);
  • 对象头与锁膨胀:Mark Word 里 4bit GC 年龄、锁标志位如何承载 synchronized 升级;
  • 元空间回收与类加载器泄漏:热部署框架(Spring Boot DevTools 外置部署、OSGi、自研插件)的元数据治理。

6.2 常见问题 FAQ

Q1:永久代到底哪去了?为什么元空间可以解决 PermGen OOM?

Java 8 类元数据移入本地内存中的元空间,MaxMetaspaceSize 默认不设限。PermGen OOM 常见于"类无限增长"的应用(动态代理、脚本引擎),元空间默认无上限只是把 OOM 推给操作系统层------所以生产上还是要显式设置 -XX:MaxMetaspaceSize 并监控 MC/MU 曲线,否则类泄漏最终以进程被杀收场。

Q2:为什么建议 -Xms 等于 -Xmx?

堆默认支持动态扩容/缩容:低于 -Xms 用量时触发堆缩容,高于时扩容,每次伸缩伴随 Full GC 停顿,且影响对象地址稳定性(压缩 GC)。生产设置相等后堆大小恒定,GC 行为可预期。

Q3:栈能开多大?线程数和栈大小的关系?

x64 默认约 1MB(ThreadStackSize,可用 PrintFlagsFinal 验证)。进程可创建线程数 ≈ 可用内存 / 栈大小:线程 1MB 栈、进程剩余 2GB → 上限约 2000 线程;调大到 2MB → 只剩 1000。线程池应用调小 -Xss 反而能提高并发上限(如 512KB),前提是确认业务无深递归。

Q4:JDK 8 里 s == s.intern() 为什么是 true?JDK 6 为什么是 false?

JDK 6 的 intern 在永久代里复制一份字符串,返回的是"池副本" ≠ 堆对象;JDK 7+ StringTable 迁到堆,池中无该字符串时直接记录堆对象引用 ,所以 sintern() 返回值、后续同内容字面量三者指向同一堆对象(前提:池中此前确无同内容字符串,见 3.2 演示)。

Q5:方法区/元空间会被 GC 吗?类什么时候能被卸载?

会,但条件苛刻:类卸载要求其类加载器连同所有已加载类全部不可达(GC Roots 无引用),再由 Full GC 顺带回收元空间。一个类加载器若被某个常驻对象持有(如 Spring 容器的 WebappClassLoader 被线程上下文引用),其全部类的元数据永远不卸载------这就是热部署多次后 Metaspace 暴涨的根因。

Q6:Metaspace 一直涨、甚至 Metaspace OOM,怎么排查?

jstat -gc 看 MC 单调上升 + FGC 频繁;② jmap -clstats 看类加载器个数与各加载器的字节占用,定位泄漏的加载器;③ 常见元凶:每次请求 new 类加载器、动态代理/反射生成类未缓存、CGLib 增强类、Groovy 等脚本引擎、热部署后旧容器未销毁;④ 治理:复用加载器与代理工厂 + 显式 MaxMetaspaceSize 兜底。

Q7:堆很小、GC 正常,但进程 RSS 居高不下,是什么在吃内存?

堆外四大户:Metaspace (含 Compressed Class Space)、直接内存 (Netty/自研 NIO)、CodeCache (JIT 机器码)、线程栈 。开 -XX:NativeMemoryTracking=summary 后用 jcmd <pid> VM.native_memory summary 逐项比对即可定位。

Q8:对象一定分配在堆上吗?

不绝对。服务端编译器默认开启逃逸分析(UseEscapeAnalysis):方法内创建、且未逃逸 (不被方法外引用/不暴露)的小对象,会被标量替换拆成字段直接分配在栈帧局部变量表,随方法返回即销毁,不产生 GC 压力。这是"栈上分配"的实际形态,也是 JIT 优化后的性能彩蛋------写代码时仍应默认"对象在堆",优化交给 JIT。

Q9:GC Roots 到底包括哪些?和内存结构怎么对应?

① 虚拟机栈/本地方法栈中局部变量表引用的对象;② 方法区(元空间)中静态字段 引用的对象(静态变量本体在堆,被元空间侧的结构引用);③ 运行时常量池引用的对象(如 intern 字符串);④ JNI 引用;⑤ 活跃线程、被 synchronized 持有的对象。映射到本文:栈区引用是绝大多数"可达起点",元空间存"类结构级引用",堆才是对象本体

Q10:为什么很多生产堆上限都设在 30G 左右而不是 64G?

开启压缩指针的硬性条件是堆 ≤ 32G(UseCompressedOops)。堆超 32G 后:引用从 4B 回 8B → 同规模对象占用上涨约 10%~15%,且堆越大单次 Full GC 停顿越长。于是 30G 附近成为"单实例堆的经济上限",更大负载倾向多实例横向扩容而非单机死磕。

6.3 参考资料

相关推荐
布吉岛的石头1 小时前
Java 程序员第 48 阶段1:Transformer 架构总览与自注意力直觉,从 RNN 痛点理解注意力机制
java·架构·transformer
吴声子夜歌1 小时前
Guava——事件总线
java·网络·guava
FfHUCisI1 小时前
Golang 切片扩容策略
开发语言·数据库·golang
en.en..1 小时前
Ubuntu嵌入式开发 export环境变量
java·开发语言·数据库
gf13211111 小时前
【python_回复邮件】
android·java·python
攻城有术1 小时前
专项攻克——重写 Redis 依赖包方法的 6 种实现方式
java·数据库·redis·bootstrap
ynchyong1 小时前
JavaScript Promise 实战:.then() 与 .catch() 的区别及回调函数封装指南
开发语言·javascript·ecmascript·promise·then
跨境数据猎手1 小时前
京东开放平台商品详情接口(jd.item_get):签名、缓存与批量采集
java·spring·缓存
名字还没想好☜1 小时前
Go 1.21 context.WithoutCancel 实战:父 context 取消了,收尾任务还要继续跑
开发语言·后端·golang·go