# 堆
- 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域
- Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确定了。是 JVM 管理的最大一块内存空间
- 堆内存的大小是可调节的
- 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
- 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区 (Thread Local Allocation Buffer, TLAB)
# 内存细分
Java 7 及之前堆内存逻辑分为三部分:新生区 + 养老区 + 永久区
Young Generation Space 新生区 (Young/New)
又被划分为 Eden 和 Survivor 区
Tenure generation space 养老区 (Old/Tenure)
Permanent Space 永久区 (Perm)
Java 8 及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
Young Generation Space 新生区 (Young/New)
又被划分为 Eden 和 Survivor 区
Tenure generation space 养老区 (Old/Tenure)
Meta Space 元空间 (Meta)
约定:新生区⬅➡新生代⬅➡年轻代 养老区⬅➡老年区⬅➡老年代 永久区⬅➡永久代
# 堆空间大小的设置
Java 堆区用于存储 Java 对象实例,那么堆的大小在 JVM 启动时就已经设定好了,可以通过选项 "-Xmx" 和 "-Xms" 来进行设置
- "-Xms" 用于表示堆区的起始内存,等价于 - XX: InitialHeapSize
- "-Xmx" 用于表示堆区的最大内存,等价于 - XX: MaxHeapSize
一旦堆区中的内存超过 "-Xmx" 所指定的最大内存时,将会抛出 OutOfMemoryError 异常
通常会将 - Xms 和 - Xmx 两个参数配置相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
默认情况下,初始内存大小:物理电脑内存大小 / 64
最大内存大小:物理电脑内存大小 / 4
# 年轻代与老年代
- 储存在 JVM 中的 Java 对象可以被划分为两类 :
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致
- Java 堆区进一步细分的话,可以划分为年轻代和老年代
- 其中年轻代又可以划分为 Eden 空间,Survivor0 空间和 Survivor1 空间 (有时也被称为 from 区和 to 区)

- 配置新生代与老年代在对结构的占比
- 默认 -XX:NewRadio=2, 表示新生代占 1, 老年代占 2, 新生代占整个堆的 1/3
- 可以修改 -XX:NewRatio=4, 表示新生代占 1, 老年代占 4, 新生代占整个堆的 1/5
- 在 HotSpot 中,Eden 空间和另外的两个 Survivor 空间缺省所长的比例是 8 : 1 : 1
- 可以通过选项 "-XX: SurvivorRatio" 调整这个空间比例
- 几乎所有的 Java 对象都是在 Eden 区被 new 出来的
- 绝大部分的 Java 对象的销毁都在新生代进行了
- 可以使用选项 "-Xmn" 设置新生代最大内存大小

# 对象分配概述
为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者不仅需要考虑如何分配,在哪里分配等问题,而且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收之后是否会在内存空间中产生内存碎片
- new 的对象先放 Eden 区,此区有大小限制
- 当 Eden 区的空间被填满时,程序又需要创建对象,JVM 的垃圾回收器将对 Eden 区进行垃圾回收 (Minor GC), 将 Eden 区中不再被其它对象所引用的对象进行销毁。再加载新的对象放到 Eden 区
- 然后将 Eden 中剩余对象移动到 Survivor0 区
- 如果再次出发垃圾回收,此时上次幸存下来的放到 Survivor0 区的,如果没有回收,就会放到 Survivor1 区
- 如果再次经历垃圾回收,此时会重新返回 Survivor0 区,接着再去 Survivor1 区
- 啥时候能去养老区呢?可以设置次数,默认是 15 次
- -XX: MacTenuringThreshold=
进行设置
- -XX: MacTenuringThreshold=
# 内存分配策略
针对不同年龄段的对象分配原则
优先分配到 Eden
大对象直接分配到老年代
长期存活的对象分配到老年代
动态对象年龄判断
如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一般,年龄大于或等于该年龄的对象可以直接进入老年代,无需等待到 MaxTenuringThreshold 中要求的年龄
空间分配担保
-XX: HandlePromotionFailure
# TLAB
- 从内存模型而不是垃圾收集的角度,对 Eden 区域继续划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
- 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此将这种分配方式称为快速分配策略
- 尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选
- 在程序中,开发人员可以通过选项: -XX: UseTLAB 设置是否开启 TLAB 空间
- 默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%, 可以通过: -XX: TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间百分比大小
- 一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试通过加锁机制确保操作的原子性,从而直接在 Eden 空间中分配内存

# 堆空间常用参数
-XX: +PrintFlagsInitial 查看所有的参数的默认初始值
-XX: +PrintFlagsFinal 查看所有的参数的最终值 (可能会存在修改,不再是初始值)
-Xms: 初始堆空间内存 (默认为物理内存的 1 / 64)
-Xmx: 最大堆空间内存 (默认为物理内存的 1 / 4)
-Xmn: 设置新生代的大小 (初始值及最大值)
-XX: NewRatio: 配置新生代与老年代在堆结构的占比
-XX: SurvivoerRatio: 设置新生代中 Eden 和 S0/S1 空间的占比
-XX: MaxTenuringThreshold: 设置新生代垃圾的最大年龄
-XX: +PrintGCDetails: 输出详细的 GC 处理日志
打印 gc 简要信息 : -XX: +PrintGC 或 -verbose: gc
-XX: HandlePromotionFailure: 是否设置空间分配担保
在发生 Minor GC 之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象的总空间
- 如果大于,则此次 Minor GC 是安全的
- 如果小于,则虚拟机会查看 - XX: HandlePromotionFailure 设置值是否允许担保失败
- 如果 HandlePromotionFailure=true, 那么会继续检查老年代中可用连续空间是否大于历次晋升到老年代的对象的平均大小
- 如果大于,则尝试进行一次 Minor GC, 但这次 Minor GC 依然是有风险的
- 如果小于,则改为进行一次 Full GC
- 如果 HandlePromotionFailure=false, 则改为进行一次 Full GC
- 如果 HandlePromotionFailure=true, 那么会继续检查老年代中可用连续空间是否大于历次晋升到老年代的对象的平均大小
在 JDK6 Update24 之后,HandlePromotionFailure 参数不会影响到虚拟机的空间分配担保策略,观察 OpenJDK 的源码变化,虽然源码中还定义了 HandlePromotionFailure 参数,但是在代码中已经不会再使用它. JDK6 Update24 之后规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC, 否则将进行 Full GC
# 代码优化
使用逃逸分析,编译器可以对代码做如下优化 :
- 栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
- 同步省略。如果一个对象被发现只能从一个线程
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分 (或全部) 可以不储存在内存,而是存储在 CPU 寄存器中
- -server: 启动 server 模式,因为在 server 模式下,才可以启动逃逸分析
- -XX: +DoEscapeAnalysis: 启用逃逸分析
- -Xmx10m: 指定了堆空间最大为 10m
- -XX: +PrintGC: 将打印 GC 日志
- -XX: +EliminateAllocations: 开启了标量替换 (默认打开), 允许将对象打散分配在栈上,比如对象拥有 id 和 name 两个字段,那么这两个字段将会被是为两个独立的局部变量进行分配
