目录
- 一、内容结构
- 二、核心知识
- 三、JVM 堆内存划分
- 四 、堆的使用
- 1. 分配
- 2. GC
- GC 分类
- Minor GC 触发机制
- Major GC 触发机制
- Full GC 触发机制
- 3. 对象晋升(Promotion)
- 五、内存分配的优化
- 1. 逃逸分析
- 2. JIT 优化手段
- 栈上分配
- 同步省略
- 标量替换
- 3. 代码优化的一些总结
- 六、堆空间相关参数设置
- 附录
- Java 常用性能分析和调优工具
- 通过命令查看设置参数
- 通过 jvisualvm 查看
- JProfile 工具
- 系列清单
- 附件
一、内容结构

二、核心知识
- 一个 JVM 中只有一个堆空间, 在 JVM 创建时生成。 也是 JVM 管理的最大的内存空间。
- 所有对象和数组都应该分配在堆上,栈中保存对象的引用,这些引用指向对象和数组在堆中的位置。
- 所有线程共享堆空间,但堆也可以划分线程私有的缓冲区(TLAB)。TLAB 用于提升内存分配的吞吐量
- 堆采用分代思想来使用内存,堆中对象使用完后不会立即销毁,会等待 GC 回收。
三、JVM 堆内存划分
1. 堆内存划分

JDK 1.7 及之前版本
堆内存中包括堆和方法区(永久代)

JDK 1.8 开始
将方法区移出堆内存, 并更名为 元空间(Meta Space)
2. 堆(Heap)空间划分
堆空间划分为年轻代(新生代)和老年代,其中年轻代又分为 Eden、S0 和 S1 三个区。S0 和 S1 区工作时轮流被叫做 From 区和 To 区。其中总有一块是空的。
为什么要用分代思想来划分堆空间?
其实不分代也完全可以,分代的唯一理由就是优化空间使用(GC) 的性能。
- 对象的生命周期不同, 70%-99% 的对象是临时对象(“朝生夕死”)
- 不同分区存放不同类型的对象。新生代存放刚刚创建的对象,老年代存放经历多次 GC 仍然存活(使用)的对象
- GC 可对不同分区(存放的对象类型不同)采用不同的收集算法,从而提高收集效率。
四 、堆的使用
1. 分配

基本流程
- 新创建的对象优先分配到 Eden 区
- Eden 区空间不足,会触发一次 Minor GC(Young GC)
- Eden 区在垃圾回收后,仍然无法放下新建对象,会尝试将对象放到老年代
- 老年代空间仍然不足时, 将进行一次 Full GC
- Full GC 后还没有空间存放, 或抛出 OutOfMemoryError
特殊情况
- 大对象 直接分配到老年代,
尽量避免程序中出现大对象, 尤其是生命周期较短的大对象
TLAB
TLAB,线程本地分配缓存(Thread Local Allocation Buffer),有以下特点:
- 在 Eden 区中划分,为线程提供独立的对象分配空间(线程私有)
- 该区域非常小,通常只有 Eden 区空间的 1%
- 基于 OpenJDK 的 VM 几乎都提供了 TLAB 设计
为什么要使用 TLAB
堆空间是线程共享的, 为了避免多线程操作相同的内存地址,通常要采用一些同步手段(如加锁等),这样会影响分配速度。
多线程创建对象时,通过使用 TLAB 可以避免一些安全问题。从而提升内存分配的吞吐量。
2. GC
GC 分类
- 部分收集
- 新生代收集:Minor GC
- 老年代收集:Major GC, 只有 CMS 收集器有单独的老年代收集行为
- 混合收集: 收集新生代和部分老年代, G1 收集器
- 整堆收集
Full GC 收集整个 Java 堆和方法区
Minor GC 触发机制
- Eden 区空间不足触发, 但是新生代的 Survivor 区空间不足
不会触发
- Minor GC 回收速度快,触发相对频繁
- 会暂停所有用户任务(Stop the world),收集结束后用户线程才恢复
Major GC 触发机制
- 老年代空间不足时触发
- 通常会伴随着一次 Minor GC
- 速度慢,通常 Minor GC 的十分之一, STW 时间也更长
- Major GC 内存仍不够,抛出 OOM
Full GC 触发机制
触发情况包括以下 5 种:
- 调用 System.gc() 时, 系统建议执行 Full GC
- 老年代空间不足
- 方法区空间不足
- 通过 Minor GC 后进入老年代所有对象的平均大小大于老年代可用空间
- 由 Eden 区、From 区向 To 区复制对象时,对象大小大于 To 区可用内存(Survivor 区卡空间不足),则会把对象转存到老年代,但老年代空间小于转存对象大小。
3. 对象晋升(Promotion)
- 对象在 Eden 区出生
- 对象每经过一次 Minor GC,年龄计数加 1
- 对象年龄达到阈值(默认为 15),会被晋升到老年代。 晋升阈值可以通过
-XX:MaxTenuringThreshold
来设置
- 动态年龄判定
Survivor 区中相同年龄对象大小超过 Survivor 区空间的一半,年龄等于或大于这个相同年龄的对象直接进入老年代,而不用等到达阈值
五、内存分配的优化
1. 逃逸分析
逃逸分析(Escape Analysis)是一种有效减少同步负载和内存堆分配压力的跨函数全局数据流分析算法(我也不懂,:》)。 主要用于分析一个新的对象的使用范围,也就是分析对象的动态作用域:
- 一个对象被创建后,只在方法内部使用,就被认为没有发生逃逸
- 一个对象被创建后,被外部方法所引用,则被认为发生逃逸。如作为返回值返回给其调用者。
2. JIT 优化手段
通过逃逸分析,JIT 可以对程序进行优化:
- 栈上分配:将堆分配转换成栈分配
- 同步省略:如果一个对象被发现只会被一个线程访问,可以取消相关同步控制
- 标量替换:将对象(复合量)替换成标量(基本类型),从而实现对象的栈上分配
栈上分配
JIT 编译器在编译期间根据逃逸分析结果,发现一个对象没有逃逸,就可能优化成栈上分配,分配完成后继续栈内执行。最后栈帧执行完成,栈看空间被收回。栈上的对象也被收回。
同步省略
- 线程同步代价非常高,同步会影响并发性和性能。
- 在动态编译时,JIT 可以借助逃逸分析来判断同步块所使用的锁对象是否能被一个线程访问而没被其他线程访问,如果没有,通过 JIT 编译器编译同步块时取消同步,也叫锁消除。

标量替换
标量(Scalar)
:指一个无法再分解成更小数据的数据。如 Java 中的基本数据类型
聚合量(Aggregate)
:指一个由标量和其他聚合量组成的数据。如 Java 中的对象
- JIT 编译阶段,如果经逃逸分析发现对象不会被外界访问,那么这个对象可能被优化为若干标量来替代。替换为标量后的变量可以存储在栈中(栈帧局部变量表?)

3. 代码优化的一些总结
- JVM 启用 Server 模式,才可以启用逃逸分析(使用用 -server 参数)
- 逃逸分析目前还不成熟,根本原因是无法保证逃逸分析所带来的性能提升高于其自身消耗
- HotSpot VM 中并没有将不发生逃逸的对象分配到栈中
- 标量替换后,会将对象拆解后的标量分配到栈上。
六、堆空间相关参数设置
1. 官方说明
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
2. 部分摘要


分类 |
参数 |
说明 |
大小 |
-Xms |
初始堆内存空间(默认为物理内存的 1/64) |
-Xmx |
最大堆内存空间(默认为物理内存的 1/4) |
-Xmn |
设置新生代的大小(初始值及最大值) |
比例 |
-XX:NewRatio |
配置老年代与新生代的空间比例关系,Old:Eden = N:1 |
-XX:SurvivorRatio |
设置新生代中空间比例关系, Eden:S0:S1 = N : 1 : 1 |
GC |
-XX:MaxTenuringThreshold |
设置晋升老年代的年龄阈值(默认值 15) |
-XX:+PrintGCDetails |
输出详细的 GC 处理日志 |
-XX:PrintGC |
打印 GC 简要信息 |
-verbose:gc |
打印 GC 简要信息 |
-XX:HandlePromotionFailure |
是否设置空间分配担保 |
TLAB |
-XX:UseTLAB |
设置是否开启 TLAB 空间 |
-XX:TLABWasteTargetPercent |
设置 TLAB 所占 Eden 区空间的百分比 |
参数查看 |
-XX:PrintFlagsInitial |
查看所有参数的默认初始值 |
-XX:PrintFlagsFinal |
查看所有参数的最终值(可能会存在修改,不再是初始值) |
逃逸分析及优化 |
-XX:+DoEscapeAnalysis |
显式的开启逃逸分析 |
-XX:+PrintEscapeAnalysis |
查看逃逸分析的筛选结果 |
-XX:+EliminateAllocations |
开启标量替换(默认打开的) |
–server |
设置 JVM 启用 Server 模式 |
附录
Java 常用性能分析和调优工具
- JDK 命令行
- Jprofiler
- Eclipse Memory Analyer Tool
- Jconsole
- Visual VM
- Java Flighting Record
- GCViewer
- GC Easy
通过命令查看设置参数
- 使用 jps 命令查看进程 id
- 使用 jstat -gc $id 查看

- S0C、S1C、EC、OC 是相关区域的总的大小
- S0U、S1U、EU、OU 是相关区域已经使用的内存大小
通过 jvisualvm 查看
- 运行
jvisualvm
命令,打开 JVisualVM
- 通过 工具 -> 插件 安装 Visual GC
- 选中一个 JVM ,可以查看其运行情况

JProfile 工具
系列清单
画图学 JVM 系列目录
附件
尚硅谷2020最新版宋红康JVM 08 堆 重绘 PPT