一、垃圾收集器
JVM 在进行 GC 垃圾回收时,并非每次都对上面三个内存(新生代、老年代、元空间)区域一起回收的
,大部分时候 回收的都是指新生代
。 针对 Hotspot VM 的实现,它里面的 垃圾收集器
按照回收区域又分为两大种类型:
Partial GC 部分收集
新生代收集 (Minor GC / Young GC)
- 只针对
新生代(Eden,s0,s1)区
堆内存进行垃圾回收。
老年代收集 (Major GC / Old GC)
- 只是
老年代
的堆内存进行垃圾回收。 - 目前,只有 CMS 收集器,会有单独收集老年代的垃圾回收。
混合收集(Mixed GC)
- Mixed GC 收集器
- 收集整个
新生代
以及部分老年代
的堆内存垃圾收集器。 - 目前,只有
G1
会有这种行为。
触发条件:
- 当
新生代
空间不足时,就会触发Minor GC
(这里的年轻代
满指的是Eden 区
满), 会顺带触发S0 区
的 GC,也就是被动触发 GC(每次 Minor GC 会清理年轻代的内存)
。- 因为 Java 对象大多都具备
朝生夕灭
的特性,所以Minor GC
非常频繁,一般回收速度也比较快。Minor GC
会引发STW(Stop The World)
,暂停其它用户的线程
,等垃圾回收结束,用户线程
才恢复运行。
package com.calvin.jvm.heap.gc.example;
/**
* 新生代空间不足(Eden 区)触发 MinorGC
*
* @author Calvin
* @date 2023/8/24
* @since v1.0.0
*/
public class MinorGcTrigger {
/**
* 触发条件: Eden区(75MB), 程序启动和对象占用超出了75MB, 触发 Minor GC
*/
public static void eDenSpaceFull() throws InterruptedException {
// A对象 => 占用25MB
byte[] a = new byte[1024 * 1024 * 25];
System.out.println("A对象: " + a.length / 1024 /1024 + "MB") ;
// 停顿 5 秒
Thread.sleep(5000);
// B对象 => 占用25MB
byte[] b = new byte[1024 * 1024 * 25];
System.out.println("B对象: " + b.length / 1024 /1024 + "MB");
// C对象 => 占用25MB
byte[] c = new byte[1024 * 1024 * 25];
System.out.println("C对象: " + c.length / 1024 /1024 + "MB");
// 停顿 500 秒
Thread.sleep(5000 * 100);
}
/**
* 主方法: -Xms300m -Xmx300m -XX:+PrintGCDetails
*
* @param args 参数
* @throws InterruptedException 中断异常
*/
public static void main(String[] args) throws InterruptedException {
eDenSpaceFull();
}
}
当前执行程序: -Xmx300m 最大内存为 300MB, (默认 NewRatio=2 => 新生代 1/3 老年代 2/3)
新生代占比 300MB _ 1/3 = 100MB, 老年代占比: 300MB _ 2/3 = 200MB
新生代: (默认 SurvivorRatio=8 => 8:1:1)
- eden 区占比: 100MB * 6/8 = 75MB
- SO 区占比: 100MB * 1/8 = 12.5MB
- S1 区占比: 100MB * 1/8 = 12.5MB
执行结果
A对象: 25MB
B对象: 25MB
## 当前执行了一次 Moinr GC
[GC (Allocation Failure) [PSYoungGen: 58982K->880K(90624K)] 58982K->52088K(298496K), 0.0222460 secs] [Times: user=0.08 sys=0.01, real=0.02 secs]
C对象: 25MB
Heap
PSYoungGen total 90624K, used 28453K [0x00000007b9b00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 77824K, 35% used [0x00000007b9b00000,0x00000007bb5ed408,0x00000007be700000)
from space 12800K, 6% used [0x00000007be700000,0x00000007be7dc010,0x00000007bf380000)
to space 12800K, 0% used [0x00000007bf380000,0x00000007bf380000,0x00000007c0000000)
ParOldGen total 207872K, used 51208K [0x00000007ad000000, 0x00000007b9b00000, 0x00000007b9b00000)
object space 207872K, 24% used [0x00000007ad000000,0x00000007b0202020,0x00000007b9b00000)
Metaspace used 3710K, capacity 4536K, committed 4864K, reserved 1056768K
class space used 410K, capacity 428K, committed 512K, reserved 1048576K
Full GC 全部收集
- 收集整个
新生代(Young Gen)、老年代(Old Gen)、元空间(Metaspace)
的垃圾收集器。
触发条件:
- 老年代、方法区空间不足时,通过
Minor GC
后进入老年代的平均大小
>老年代的可用内存
- 由
Eden区
、S0
=>S1
复制时,对象大小 >S1
可用内存,则把该对象转存到老年代
,且老年代的可用内存
<对象大小
。 Full GC
是开发或调优中尽量要避免的, 这样STW
时间会短一些。
- 老年代、方法区空间不足时,通过
案例: 大对象直接晋升老年代
package com.calvin.jvm.heap.gc.example;
/**
* 老年代 Major GC / Full GC 触发
*
* @author Calvin
* @date 2023/9/5
* @since v1.0.0
*/
public class MajorGcOrFullGcTrigger {
/**
* Full GC
* <p>
* 当前执行程序: -Xmx300m => 最大内存为300MB
* 触发机制:
* - 调用System.gc()时,系统建议执行Full GC,但是不必然执行.
* - 老年代空间不足.
* - 方法区空间不足.
* - 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
* - 由Eden区、survivor space0(From Space)区向 survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小.
* - full gc 是开发或调优中尽量要避免的。这样暂停时间会短一些。
*
* @throws InterruptedException
*/
public static void fullGc() throws InterruptedException {
// A对象 => 占用80MB
byte[] a = new byte[1024 * 1024 * 80];
System.out.println("A对象: " + a.length / 1024 / 1024 + "MB");
// B对象 => 占用220MB
byte[] b = new byte[1024 * 1024 * 220];
System.out.println("B对象: " + b.length / 1024 / 1024 + "MB");
Thread.sleep(1000 * 10);
}
/**
* 主方法
*
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
fullGc();
}
}
- 当前执行程序: -Xmx300m 最大内存为 300MB, (默认 NewRatio=2 => 新生代 1/3 老年代 2/3)
- 新生代占比 300MB _ 1/3 = 100MB, 老年代占比: 300MB _ 2/3 = 200MB
- 新生代: (默认 SurvivorRatio=8 => 8:1:1)
- eden 区占比: 100MB * 6/8 = 75MB
- SO 区占比: 100MB * 1/8 = 12.5MB
- S1 区占比: 100MB * 1/8 = 12.5MB
- 老年代: 200MB
- 当前对象内存使用已使用 300MB + 启动程序加载包对象内存,已超出了 300MB, 所以对象内存溢出 OOM.
执行结果
************A对象: 80MB
####### 执行2次 MinorGC #######
[GC (Allocation Failure) [PSYoungGen: 4669K->512K(90624K)] 86589K->82440K(298496K), 0.0007327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 512K->480K(90624K)] 82440K->82408K(298496K), 0.0023379 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
####### 执行一次 FullGC 回收失败 #######
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(90624K)] [ParOldGen: 81928K->82285K(207872K)] 82408K->82285K(298496K), [Metaspace: 3168K->3168K(1056768K)], 0.0027795 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
####### 再一次执行 MinorGC 回收失败 #######
[GC (Allocation Failure) [PSYoungGen: 0K->0K(90624K)] 82285K->82285K(298496K), 0.0009091 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
####### 再一次执行 FullGC 回收失败 #######
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(90624K)] [ParOldGen: 82285K->82268K(207872K)] 82285K->82268K(298496K), [Metaspace: 3168K->3168K(1056768K)], 0.0021149 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
####### OOM 内存溢出 #######
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.calvin.jvm.heap.gc.example.MajorGcOrFullGcTrigger.fullGc(MajorGcOrFullGcTrigger.java:51)
at com.calvin.jvm.heap.gc.example.MajorGcOrFullGcTrigger.main(MajorGcOrFullGcTrigger.java:66)
####### 打印GC使用详情 #######
Heap
PSYoungGen total 90624K, used 2335K [0x00000007b9b00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 77824K, 3% used [0x00000007b9b00000,0x00000007b9d47c68,0x00000007be700000)
from space 12800K, 0% used [0x00000007be700000,0x00000007be700000,0x00000007bf380000)
to space 12800K, 0% used [0x00000007bf380000,0x00000007bf380000,0x00000007c0000000)
ParOldGen total 207872K, used 82268K [0x00000007ad000000, 0x00000007b9b00000, 0x00000007b9b00000)
object space 207872K, 39% used [0x00000007ad000000,0x00000007b2057198,0x00000007b9b00000)
Metaspace used 3200K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
- 简称
STW
。 - 即在
执行垃圾收集算法
时, Java 应用程序的所有的线程(除了垃圾收集收集器线程之外)都被挂起。 - 此时,系统
只能允许GC线程
进行运行,其他线程则会全部暂停
,等待GC线程执行完毕后
才能再次运行。
二、【新生代】收集器
Serial 串行-收集器
- 是一个
单线程
垃圾收集器 - 使用
copy 复制算法
- 使用场景:
内存少(5MB ~ 20MB)
, 主要应用于客户端
- 会产生
STW
# 选择Serial收集器参数:
java -XX: +UseSerialGC xxx.jar
ParNew 多线程-收集器
- 是一个
多线程
垃圾收集器 - 使用
copy 复制算法
- 使用场景: 内存上升(20MB ~ 1G), 主要应用于
服务端
- 协助收集器: 可以和
CMS (收集老年代)
更好使用 - 会产生
STW
# 选择ParNew收集器参数:
java -XX: +UseParNewGC xxx.jar
Parallel Scavenge 并行清理-收集器
吞吐量: 就是处理器用于运行用户代码的时间与处理器总消耗时间的比值
(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))
高吞吐量: 可以最高效率的利用处理器资源
,吞吐量高,就意味着垃圾收集时间短
,而更多的精力投入到程序的主要运算任务上。
- 是一个
并行多线程
垃圾收集器 - 使用
copy
复制算法 - 使用场景: 内存上升(20MB ~ 1G) 主要应用于
服务端
- CMS 和 Parallel Scavenge 区别:
- CMS: 关注点是尽可能地
缩短
垃圾收集时用户线程的停顿时间
- Parallel Scavenge: 目标则是达到一个
可控制的吞吐量(Throughput)
。
- CMS: 关注点是尽可能地
- 优点: 还具有
自适应的调节策略
(通过-XX: +UseAdaptiveSizePolicy
参数控制)- 当该参数激活后,将
不需要人工指定新生代(-Xmn)、Eden 与 Survivor 的比例(-XX: SurvivorRatio)
, - 虚拟机会根据当前系统的运行情况收集性能监控信息,
动态的调整这些参数以提供最合适的停顿时间或者最大吞吐量
。
- 当该参数激活后,将
- JDK 1.8 默认垃圾收集器:
Parallel Scavenge + Parallel Old
组合, 使用命令-XX: +PrintCommandLineFlags -version
# Paraller Scavenge 控制参数:
-XX: +UseParallelGC
# 吞吐量大小,是垃圾收集时间占总时间的百分比
-XX: GCTimeRatio
# 最大垃圾收集停顿时间,收集器尽力保证内存回收花费的时间不超过设定的值
-XX: MaxGCPauseMillis
三、【老年代】收集器
Serial Old 串行-收集器
- 是一个
单线程
垃圾收集器 - 使用
Mark-sweep-compact 标记整理算法
- 会产生
STW
# 选择Serial Old收集器参数:
java -XX: +UseSerialOldGC xxx.jar
Parallel Old 并行-收集器
- 是一个
并行多线程
垃圾收集器 - 使用
Mark-sweep-compact 标记整理算法
。 - 产生问题: 单线程,导致用户线程停顿过久,所以为了解决单线程问题,使用多线程,而产生垃圾收集器。
# 选择Parallel Old收集器参数:
java -XX: +UseParallelOldGC xxx.jar
CMS (concurrent mark sweep)并发标记-收集器
是一个
并发标记垃圾收集器
主要工作在
老年代
,为了解决STW
CMS 新特性:
垃圾线程和用户线程同时进行
, 在最耗时的并发标记
与并发清除
阶段,CMS 无需暂停用户线程,CMS 收集器的内存回收过程是可以与用户线程一起并发执行的。场景: 应用于
B/S 系统
的服务端上。是基于
标记-清除算法
实现的,运作过程分为四步:其中,初始标记
和重新标记
这两个步骤仍然需要暂停用户线程
。- 1.
初始标记
:仅仅只标记一下 GC ROOTS 直接关联的对象,速度很快。 - 2.
并发标记
:从 GC ROOTS 的直接关联对象开始遍历整个对象图
,耗时虽然长
,但是不需要停顿用户线程
。 - 3.
重新标记
:其目的是为了修正并发标记期间
,因用户程序继续运行而导致标记变动的那一部分对象
。 - 4.
并发清除
:清理标记阶段判定为死亡的对象
。
- 1.
CMS 的优点也为 CMS 带来了部分负面的影响:
【浮动垃圾】
- 由于 CMS
并发标记
和并发清理阶段
是和用户线程
同时进行的,程序难免会在运行中产生新的垃圾对象,如果这部分的浮动垃圾
产生在并发标记结束后,那么 CMS 无法在当次收集中处理掉。 - CMS 面临的另一问题是,并发标记虽然不会停止用户线程,但是会
占用一部分CPU资源
从而导致应用程序变慢。 - CMS 默认启动的
回收线程数是(处理器核心数量+3)/ 4
。当处理器核心数量不足4个时,CMS对程序的影响会变大
。 - CMS 收集结束后会产生打量的
内存空间碎片
,可能会导致大对象因无法找到连续的内存空间而引发一次Full GC
。
- 由于 CMS
由于 CMS 与用户线程同时运行的情况,CMS
必须预留足够的内存空间
给用户线程使用
,因此 CMS 无法像其他垃圾回收器一样等着老年代几乎被填满时再收集。JDK5 默认
CMS 收集器当老年代使用了 68%的空间后被激活
,而这一阈值在JDK6 时提升到了 92%
,无论何种阈值,CMS 都面临着“并发失败”的情况
,- 即 CMS 预留空间不足以让程序运行产生的新对象的需要。当出现并发失败的情况时,虚拟机将采用预案:冻结用户线程,启用 Serial Old 收集器来重新进入老年代回收垃圾,Serial Old 的特性造就了停顿时间更长的情况.
# 调整CMS触发的百分比, 使用 CMS
java -XX: CMSInitiatingOccupancyFraction -XX:+UseConcMarkSweepGC xxx.jar
参考: 三色标记
黑色
:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
,它是安全存活的
。- 如果有
其他对象
引用指向了黑色对象
,无须重新扫描一遍
。 黑色对象
不可能直接(不经过灰色对象)指向某个白色对象
。
- 如果有
灰色
:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过
。白色
:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
四、【不分代】收集器
G1 (Garbage First) 收集器
G1收集器专门针对以下应用场景设计:
- 可以像CMS收集器一样可以和应用并发运行
- 压缩空闲的内存碎片,却不需要冗长的GC停顿
- 对GC停顿可以做更好的预测
- 不想牺牲大量的吞吐量性能
- 不需要更大的Java 堆空间
G1 与 CMS 区别:
- G1 会
压缩空闲内存
使之足够紧凑,做法是用regions
代替细粒度的空闲列表进行分配,减少内存碎片的产生。 - G1 的
STW更可控
,G1在停顿时间上添加了预测机制
,用户可以指定期望停顿时间
。
- G1 会
在G1中,
堆
被分成一块块大小相等的heap region
,一般有默认2048个
,默认一个为1M
,这些region在逻辑上是连续的。- 每块region都可以作为
独立的新生代,幸存区和老年代
。 Region
来表示Eden 区、Survivor(S0/S1)区、Old 区、Humongous (H)区
等。- Eden 区 : 新生代
- Survivor 区: 新生代
- Old 区: 老年代
- Humongous 区: 主要存放一些比
较大的对象
连续的,一个对象大于region的一半时,称之为巨型对象
,G1不会对巨型对象进行拷贝,回收时会考虑优先回收。
- 每块region都可以作为
G1中的GC收集:
- G1保留了YGC并
加上了一种全新的MIXGC用于收集老年代
。 - G1中没有Full GC,
G1中的Full GC是采用serial old Full GC
。
- G1保留了YGC并
- 运作过程大致可划分为以下四个步骤:
初始标记(Initial Marking)
:- 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的
Region中分配新对象
。 - 这个阶段需要
停顿线程,但耗时很短
,而且是借用进行Minor GC
的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的
并发标记(Concurrent Marking)
:- 从GC Root开始
对堆中对象进行可达性分析
,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长
,但可与用户程序并发执行。 - 当对象图扫描完成以后,还要重新处理
SATB记录下的在并发时有引用变动的对象
。
- 从GC Root开始
最终标记(Final Marking)
:对用户线程做另一个短暂的暂停
,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
。
筛选回收(Live Data Counting and Evacuation)
:- 负责更新
Region的统计数据
,对各个Region的回收价值和成本进行排序
,根据用户所期望的停顿时间来制定回收计划
,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。 - 这里的操作涉及
存活对象的移动
,是必须暂停用户线程
,由多条收集器线程并行完成的。
- 负责更新
当发生 YGC 时:
- 会对
Eden
区存活对象
迁移Survivor
区,然后进行回收Eden
区。 - 会对满的
Survivor
区存活对象
或达到一定阈值新生代对象
,迁移到Old
区, 然后进行回收Survivor
区。
# 使用 G1 收集器, 打印时间戳、打印GC日志详情
java -XX:+UseG1GC \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCDetails \
xxx.jar