JVM在进行GC时,并非每次都对新生代、老年代、方法区(元空间)三个区域一起回收,大部分时间回收的都是新生代
针对Hotspot VM的实现,它里面的GC按照回收区域分两大类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
- 部分收集:不是完整收集整个Java堆的垃圾收集
- 新生代收集(Minor GC/Young GC)只回收新生代(Eden、S0,S1)
- 老年代收集(Major GC/Old GC)只回收老年代
- 目前只有CMS GC会有单独收集老年代的行为
- 注:很多时候Major GC和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集:收集整个新生代和部分老年代的垃圾收集
- 目前,只有G1 GC有这种行为
- 整堆收集:收集整个java堆和方法区的垃圾收集
年轻代GC(Minor GC)触发机制
- 当年轻代空间不足时,就会触发Minor GC,年轻代满指的是Eden区满,Survior满不会引发GC(每次Minor GC会清理年轻代的内存)
- 因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度比较快
- Minor GC会引发STW(Stop The World),暂停其它用户线程,等垃圾回收结束,用户线程才恢复执行
老年代GC(Major GC/Full GC)触发机制
- 指发生在老年代的GC,对象从老年代消失时,Major GC或Full GC发生
- 出现了Major GC,经常会伴随至少一次的Minor GC(非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
- 老年代空间不足时,会先触发Minor GC,如果之后空间还不足,则触发Major GC
- Major GC的速度一般比Minor GC慢10倍以上,STW的时间更长
- 如果Major GC后,内存还不足,报OOM
Full GC触发机制
- 调用System.gc()时,系统建议执行Full GC,不必须执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、survivor space0(From space)区向survivor space1(To space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
注:Full GC是开发或调优中尽量避免的
/**
* Administrator
* 2024/5/18
* 测试Minor GC,Major GC,Full GC
* 执行参数 -Xms512m -Xmx512m -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String str = "lotus.com";
while (true) {
list.add(str);
str += str;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍历次数:" + i);
}
}
}
//执行结果
[GC (Allocation Failure) [PSYoungGen: 117623K->20420K(153088K)] 117623K->74572K(502784K), 0.0395143 secs] [Times: user=0.03 sys=0.03, real=0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 133555K->1908K(153088K)] 408891K->314108K(502784K), 0.0160892 secs] [Times: user=0.08 sys=0.00, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 1908K->0K(153088K)] [ParOldGen: 312200K->221844K(349696K)] 314108K->221844K(502784K), [Metaspace: 3492K->3492K(1056768K)], 0.0218373 secs] [Times: user=0.16 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(153088K)] 221844K->221844K(502784K), 0.0010237 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(153088K)] [ParOldGen: 221844K->221826K(349696K)] 221844K->221826K(502784K), [Metaspace: 3492K->3492K(1056768K)], 0.0046937 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
遍历次数:22
Heap
PSYoungGen total 153088K, used 6522K [0x00000000f5580000, 0x0000000100000000, 0x0000000100000000)
eden space 131584K, 4% used [0x00000000f5580000,0x00000000f5bde8c0,0x00000000fd600000)
from space 21504K, 0% used [0x00000000fd600000,0x00000000fd600000,0x00000000feb00000)
to space 21504K, 0% used [0x00000000feb00000,0x00000000feb00000,0x0000000100000000)
ParOldGen total 349696K, used 221826K [0x00000000e0000000, 0x00000000f5580000, 0x00000000f5580000)
object space 349696K, 63% used [0x00000000e0000000,0x00000000ed8a08f0,0x00000000f5580000)
Metaspace used 3525K, capacity 4502K, committed 4864K, reserved 1056768K
class space used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.chapter06.GCTest.main(GCTest.java:19)
堆空间分代思想
经研究,不同对象的生命周期不同,70%-99%对象是临时对象
- 新生代:有Eden、两块大小相同的Survivor(又称为from/to,s0/s1)构成,to总为空
- 老年代:存放新生代中经历多次GC仍然存活的对象
为什么需要把Java堆分代?不分代不能正常工作吗?
- 其实不分代完全可以,分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,就如同把学校的所有人关在一个教室,GC的时候要找到哪些对象没用,这样就会对全堆所有区域进行扫描,而很多对象朝生夕死,如果分代的话,新创建的对象放在某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,就会腾出很大空间
内存分配策略
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survior容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor区中每熬过一次MinorGC,年龄就加1岁,当它的年龄增加到一定程度(默认15岁,其实每个JVM,每个GC都有所不同)时,会被晋升到老年代中
- 对象晋升老年代的年龄阈值,可以通过选项 -XX:MaxTenuringThreshold来设置
针对不同年龄对象分配原则如下:
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
- 空间分配担保
- -XX:HandlePromotionFailure
//测试大对象直接进入老年代
/**
* Administrator
* 2024/5/18
* -Xms60m -Xmx60m -XX:+PrintGCDetails -XX:NewRatio=2 -XX:SurvivorRatio=8
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
byte[] buffer = new byte[1024*1024*20];
}
}
//执行结果
Heap
PSYoungGen total 18432K, used 2624K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
eden space 16384K, 16% used [0x00000000fec00000,0x00000000fee90218,0x00000000ffc00000)
from space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
to space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)
ParOldGen total 40960K, used 20480K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)
object space 40960K, 50% used [0x00000000fc400000,0x00000000fd800010,0x00000000fec00000)
Metaspace used 3496K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
对象分配过程(TLAB)
- 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内
- 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
- 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计
为什么要有TLAB(Thread Local Allocation Buffer)
- 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
- 尽管不是所有的对象实例都能够在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:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
- -XX:+PrintGCDetails:输出详细的GC处理日志
- 打印gc简要信息:-XX:+PrintGC | -verbose:gc
- -XX:HandlePromotionFailure:是否设置空间分配担保