1. 堆的核心概念
- 堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM
- 进程包含多个线程,他们是共享同一堆空间的
- 堆的大小可调节 -Xms=1024m
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
2. 堆内存细分
Java 8及之后堆内存逻辑上分为三部分:新生区+老年代+元空间(方法区实现方式)
堆空间内部结构,JDK1.8之前从永久代 替换成 元空间。
2.1 常用参数
2.1.1 设置堆内存大小与OOM
- “-Xms”用于表示堆区的起始内存,等价于-xx:InitialHeapSize
- “-Xmx”则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
- “-Xmn”则用于表示新生代最大内存大小
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
默认情况下
- 初始内存大小:物理电脑内存大小/64
-
2.1.2 配置新生代与老年代在堆结构的占比
默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
2.1.3 配置Eden空间和Survivor 占比
在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1当然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例。比如-xx:SurvivorRatio=8
2.1.4 新生代过渡到老年代的年龄设置
可以设置参数:-Xx:MaxTenuringThreshold= N进行设置
2.2 GC 过程
Minor GC | 新生代的GC |
---|---|
Major GC | 老年代的GC |
Full GC | 整堆收集,收集整个Java堆和方法区的垃圾收集 |
我们都知道,JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW的问题
而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)。
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集
- 老年代收集(MajorGC/o1dGC):只是老年代的圾收集。
- 目前,只有CMSGC会有单独收集老年代的行为。
- 注意,很多时候Major GC会和Fu11GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
- 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC会有这种行为。
整堆收集(FullGC):收集整个java堆和方法区的垃圾收集。
2.2.1 Minor GC
何时发生Minor GC | 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。) |
---|---|
Minor GC 特性 | 频率高,回收速度快。因为大部分对象朝生夕死。 |
是否会引发STW | 会,暂停其他用户线程。但是比Major GC引发的STW影响小。 |
我们创建的对象,一般都是存放在Eden区的,当我们Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作.
- 当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,一次回收后就是1。
- 同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor From中的对象 进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1
- 我们继续不断的进行对象生成 和 垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion晋升的操作,也就是将年轻代中的对象 晋升到 老年代中。
2.2.1.1 对象分配的特殊情况
2.2.2.2 动态对象年龄判定
Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,
当累积的某个年龄大小超过了 survivor 区的一半时,
取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值
动态年龄计算的代码如下:
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
//sizes数组是每个年龄段对象大小
total += sizes[age];
if (total > desired_survivor_size) {
break;
}
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}
2.2.2 Major GC
何时发生Major GC | 也就是在老年代空间不足时,会先尝试触发MinorGc。如果之后空间还不足,则触发Major GC |
---|---|
Major GC 特性 | Major GC的速度一般会比MinorGc慢1e倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了 |
是否会引发STW | 会,后果也更严重,时间更长。 |
2.2.3 Full GC
触发FullGC执行的情况有如下五种:
- 调用System.gc()时,系统建议执行FullGC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、Survivor Space(From Space)区向Survivor Space(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
2.3 堆内存分配策略
2.3.1 对象优先在 eden 区分配
2.3.2 空间分配担保
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。
2.3.3 大对象直接进入到老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。详细原因看2.2 GC过程。
2.3.4 长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
2.3.5 动态对象年龄判定
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
2.4 为对象分配内存TLAB
Thread Local Allocation Buffer
2.4.1 为啥需要给每一个线程分配TLAB
堆区是线程共享区域,任何线程都可以访问到堆区的共享数据;
由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。
为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度;
2.4.2 TLAB 具体实现
- 从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们将这种内存分配方式成为快速分配策略
- 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
- 开发人员通过-XX:UseTLAB设置是否开启TLAB空间
- 默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%,通过-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小;
- 一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存(CAS失败重试)