写在前面
本文一起看下GC相关的内容。
1:GC基础内容
1.1:为什么要有GC
内存资源的稀缺性,以及内存管理的复杂性,决定了需要有垃圾回收器这样的角色来帮助我们管理内存资源,避免手动管理带来的内存不能得到正常释放,内存资源异常耗尽等问题。
1.2:GC如何确定垃圾对象
GC想要进行垃圾回收,就需要首先确定哪些对象是垃圾对象,当前有两种方式:
1:引用计数
2:引用追踪
1.2.1:引用计数
这种方式是统计计算对象被应用的次数,来确定对象是否是垃圾对象,如下图:
但是引用计数的方式存在循环引用的问题,如下图:
此时对象虽然已经成为事实上的垃圾对象,但是确不能被发现,进而导致内存泄漏,最终导致内存溢出。但是这种方式简单,只需要维护每个对象被应用的次数就行,但因为其可能会造成内存泄漏,所以并没有得到广泛的应用。
1.2.2:引用追踪
这种方式是通过一组对象出发来标记对象,最终没有被标记的对象就是可回收的垃圾对象,这用来标记对象的一组对象我们叫做GC ROOT
,GC ROOT包括如下几类对象:
1:运行中方法的对象
即虚拟机栈栈帧中本地变量表中的对象引用
2:类的静态成员变量
3:本地方法栈JNI引用的对象
引用追踪通过GC ROOT标记对象的过程需要需要STW,此时程序将会暂停执行,STW的原因是让对象进入一个稳定的状态,这样才能进行正常的标记,避免将正常的对象最终标记为垃圾对象,比如如下程序:
package a.b;
public class WhyStw {
public static void main(String[] args) {
Object a = new Object();
}
}
然后如下编译并查看其字节码:
D:\test>javac -d . WhyStw.java
D:\test>javap -c a.b.WhyStw
Compiled from "WhyStw.java"
public class a.b.WhyStw {
public a.b.WhyStw();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
}
其中代码Object a = new Object();
创建对象并赋值到变量a的过程指令如下:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
这里的a
就是GC ROOT,因为其是运行中方法的一个对象,但是需要注意这些指令并不是原子的,如果不STW,考虑一种情况,当程序执行到4: invokespecial #1
此时只是对象在堆中分配完毕了,但是并没有赋值到局部变量表中的a,因此就有可能被标记为垃圾对象,因此当程序执行7: astore_1
时就有可能出现了,当然这只是STW的其中一个原因,应该还有其他原因,欢迎看本文的你在留言中给出自己的理解。
1.2:常见GC算法
常见的GC算法有如下3种:
1:清除算法
2:复制算法
3:整理算法
-
清除算法
先标记存活对象,然后删除垃圾对象,如下图:
-
复制算法
先标记存活对象,然后将对象直接复制到一个新的内存区域,如下图:
-
整理算法
先标记存活对象,并清除垃圾对象,然后将存活对象移动到内存的一端,如下图:
2:常见的GC方式
测试使用的jar包从这里 下载。
2.1:串行GC
即serial GC,使用参数-XX:+UseSerialGC
来配置使用串行GC,对于老年代使用的GC收集器是serial old,使用复制算法,对于老年代使用的GC收集器是serial old,采用整理算法,这两种垃圾收集器都是单线程串行方式的,会造成全线的STW,CPU的利用率低,GC时间长,严重降低系统的吞吐量,适用于几百兆小内存的单核机器。
接下来我们实际看下配置串行GC的参数以及其效果,首先运行:
D:\test>java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseSerialGC -jar test.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.4.RELEASE)
查看GC配置:
C:\WINDOWS\system32>D:\\program_files\\jdk-17.0.1\\bin\\jps
11280 Launcher
19376 test.jar
使用jmap查看堆快照
C:\WINDOWS\system32>jmap -heap 18160
Attaching to process ID 18160, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.202-b08
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1073741824 (1024.0MB) -- 最大堆大小1g,即-Xmx1g配置
NewSize = 357892096 (341.3125MB) -- young初始341m
MaxNewSize = 357892096 (341.3125MB) -- young最大341m
OldSize = 715849728 (682.6875MB) -- old682m
NewRatio = 2 -- young:old = 1:2
SurvivorRatio = 8 -- eden:from:to=8:1:1
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
2.2:并行GC
即parallel GC,这种GC方式会以火力全开的方式来进行垃圾回收,这里火力全开就是用所有的CPU资源,因此这是一种吞吐量优先的GC算法,可以使用如下三种等价的方式的来配置使用parallel GC:
1:-XX:+UseParallelGC
2:-XX:+UseParallelOldGC
3:-XX:+UseParallelGC -XX:+UseParallelOldGC
同样的,并行GC对于年轻代和老年代使用垃圾收集器也不同,对于年轻代使用的垃圾收集器是parallel scavenge,采用复制算法,对于老年代使用的垃圾收集器是parallel old,如果是系统对于吞吐量的要求比较高并且对于相应延迟不敏感,则可以才用并行GC的方式,特别的,如一些后台定时任务则可以优先采用并行GC,尽可能的增加单位时间的吞吐量。
参数-XX:ParallelGCThreads=N
来配置执行GC的线程数,默认和核数相等,所以一般该参数可以不用配置。
带来更高的吞吐量,更少的总GC暂停时间,但单次GC暂停时长较长。即GC的总次数少,总时间少,所以吞吐量高,但单次的GC时长长,所以GC时相应延迟大。
接下来我们实际看下配置串行GC的参数以及其效果,首先运行:
D:\test>java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseParallelGC -jar test.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.4.RELEASE)
查看堆快照:
C:\WINDOWS\system32>jmap -heap 20496
Attaching to process ID 20496, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.202-b08
using thread-local object allocation.
Parallel GC with 10 thread(s) -- 使用并行gc,10个线程
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1073741824 (1024.0MB)
NewSize = 357564416 (341.0MB)
MaxNewSize = 357564416 (341.0MB)
OldSize = 716177408 (683.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
2.3:CMS GC
全称是concurrent mark and sweep garbase collector,通过参数-XX:+UseConcMarkSweepGC
参数启用,是一种响应时长优先的垃圾手机策略,年轻代使用复制算法,年老代使用并发清除算法,即年老代不会整理内存空间。通过与应用线程并发的方式来执行GC,为了尽量缩短STW将整个GC过程分为如下6个阶段:
-
1:初始标记(有STW)
简单的标记GC ROOT直接引用的对象,以及被年轻代直接引用的对象,有STW,但非常短,如下图:
-
2:并发标记
从第一步标记的老年代存活对象,开始标记存货对象,此阶段同应用程序一起运行,无stw,如下图:
-
3:并发预处理
-
4:最终标记
有stw,完成老年代所有存活对象标记,因为并发标记过程中程序在运行所以会产生新的存活对象,如下图:
-
5:并发清除
无stw,与应用程序并发执行,删除标记的垃圾对象,并回收其内存空间。 -
6:并发重置
重置内部相关变量数据,为下次GC做准备。
CMS垃圾收集器的优点是尽量降低相应延迟,缺点是老年代回收采用删除算法,因此产生内存碎片,并且当堆内存比较大时可能会出现不可预估的长时间GC暂停。
接下来,我们看下CMS GC配置效果:
D:\test>java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -jar test.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.4.RELEASE)
查看堆内存快照:
C:\WINDOWS\system32>jmap -heap 18064
Attaching to process ID 18064, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.202-b08
using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC -- 使用CMS GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 1073741824 (1024.0MB) -- 最大堆内存1g
NewSize = 357892096 (341.3125MB) -- 初始年轻代内存341m
MaxNewSize = 357892096 (341.3125MB) -- 初始年轻代大小341m
OldSize = 715849728 (682.6875MB) -- 老年代大小682m
NewRatio = 2 -- young:old=1:2
SurvivorRatio = 8 -- eden:s0:s1=8:1:1
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB -- 元数据,天文数字,即不限制大小
G1HeapRegionSize = 0 (0.0MB)
3:常见的垃圾收集器
常见垃圾收集器如下图:
4:垃圾收集器的常见组合
如下图:
serial+serial old对应的配置参数为-XX:+UseSerailGC
,低延迟组合parallel new+cms对应的配置参数为-XX:+UseParNewGC
,-XX:+UseConcMarkSweepGC
,高吞吐组合parallel scavenge,parallel old对应的的参数是,-XX:UseParallelGC
,-XX:+UseParallelOldGC
。详细的可以参考这篇文章 。
写在后面
参考文章列表
如何为你JAVA程序设置最优GC参数?-垃圾回收 。