目录
1 概述
2 八大垃圾收集器
2.1 新生代垃圾收集器
(1)Serial收集器(新生代串行GC/Servial Copying)
(2)ParNew(新生代并行GC)
(3)Parallel(并行回收GC/Parallel Scavenge)
2.2 老年代垃圾收集器
(1)Parallel Old收集器
(2)CMS收集器(并发标记清除GC)
(3)Serial Old
2.3 G1收集器
(1)上述收集器的特点
(2)G1是什么?
(3)特点
(4)G1的底层原理
(5)案例
(6)常用配置参数
(7)G1相比CMS的优势
(8)JVMGC结合SpringBoot微服务的调参优化
2.4 ZGC收集器
3.总结
1 概述
- 问题1:垃圾回收算法和垃圾回收器有什么关系?
- 垃圾回收算法是垃圾回收的方法论,垃圾收集器是垃圾回收算法的具体实现
- 问题2:为什么有这么多种垃圾回收器?
- Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,
- 因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都有可能有很大差别
- 目前为止,还没有完美的收集器出现,Java的应用场景很多,没有万能的收集器能解决所有应用场景,只是针对具体应用选择最合适的收集器,进行分代收集
- 问题3:如何查看默认的垃圾回收器?
- java -XX:+PrintCommandLineFlags -version
垃圾收集器的底层配置代码:
2 八大垃圾收集器
代码后结果的参数提前说明:
- DefNew:Defalut New Generation,默认新生代
- Tenured:Old,老年代
- ParNew:Parallel New Generation,在新生代用并行回收
- PSYoungGen:Parallel Scavenge
- ParOldGen:Parallel Old Generation,在老年代用并行回收
JVM中Server/Client分别是什么意思:
- 适用范围:只需要掌握Server模式,Client基本不会用
- 操作系统:
- 32位Window操作系统,不论硬件如何都默认使用Client的JVM模式
- 32位其他操作系统,2G内存同时有2个CPU以上使用的是Server模式,低于该配置还是Client模式
- 64位的操作系统都是Server模式
2.1 新生代垃圾收集器
(1)Serial收集器(新生代串行GC/Servial Copying)
- 它是最早使用的一个收集器,在JDK1.3.1之前是唯一的选择
- 一个单线程的收集器:即在垃圾回收的时候只会使用一个CPU或一个收集线程完成垃圾回收工作
- 在进行垃圾收集的时候,必须暂停其他所有的工作线程直到它收集结束
- 如图:
- 一对一:新生代和老年代都为单线程
- 说明:STW即为Stop The World,即暂停所有应用程序线程
优点:
- 简单而高效
- 对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率
缺点:
应用场景:
- Java虚拟机运行在Client模式下默认的新生代垃圾收集器
对应JVM参数是:-XX:+UseSerialGC
- 当我们使用此参数开启Serial,老年代默认会开启Serial Old
- 即开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合
- 表示新生代和老年代都会使用串行回收收集器
- 新生代使用复制算法,老年代使用标记-整理算法
示例代码演示:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
结论:DefNew+Tenured
(2)ParNew(新生代并行GC)
- 使用多线程进行垃圾回收
- 在进行垃圾收集的时候,必须暂停其他所有的工作线程直到它收集结束
- 如图:
- ParNew收集器其实是Serial收集器新生代的并行多线程版本
应用场景:
- 最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样
- 它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器
对应JVM参数是:-XX:+UseParNewGC
- 启用ParNewGC收集器,只影响新生代的收集,不影响老年代
- 开启后会使用:ParNew(Young区用)+Serial Old(Old区用)的收集器组合
- 新生代使用复制算法,老年代使用标记-整理算法
备注:
- -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数
示例代码演示:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
结论:ParNew+Tenured,但是这种组合已经不再推荐被使用
(3)Parallel(并行回收GC/Parallel Scavenge)
- Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器
- 使用复制算法
- 也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器
- 多对多:新生代和老年代均为多线程
有了ParNew为什么还要Parallel?
- Parallel重点关注的是:可控制的吞吐量
- 吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
- 即比如程序运行100分钟,垃圾收集时间位1分钟,吞吐量为99%
- 高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务
- 比如你在前台下个单,它停顿来算,交互性差,而对于科学计算,它自己在后台计算,停顿一会我们也不知道,但是它高效利用了CPU
- 自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
- 自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量
对应JVM参数是:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活,即配置其中一个,另一个会自动连带激活)
备注:
- -XX:ParallelGCThreads=数字N 表示启动多少个GC线程
- cpu>8: N=5/8
- cpu<8: N=实际个数
示例代码演示:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
结论:PSYoungGen+ParOldGen
2.2 老年代垃圾收集器
(1)Parallel Old收集器
- Parallel Old收集器是Parallel Scavenge的老年代版本
- 使用多线程的标记-整理算法
- Parallel Old收集器在JDK1.6才开始提供
- 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量
- Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求较高,JDK1.8后可以优先考虑新生代Parallel Scavenge和老年代Parallel Old收集器的搭配策略
对应JVM参数是:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活,即配置其中一个,另一个会自动连带激活)
示例代码演示1:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
结论:PSYoungGen+ParOldGen
示例代码演示2:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
- 不配置垃圾回收器的任何参数
结论:系统默认是PSYoungGen+ParOldGen
(2)CMS收集器(并发标记清除GC)
- CMS收集器(Concurrent Mark Sweep:并发标记清除)
- 是一种以获取最短回收停顿时间为目标的收集器
- 从图中可以看出在Initial Mark和Remark阶段还是有短暂的停顿的,
- 在Concurrent Mark和Concurrent Sweep阶段,与用户线程一起执行
四个步骤:
- 初始标记(CMS initial mark):
- 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
- 并发标记(CMS concurrent mark):
- 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程,主要标记过程,标记全部对象
- 重新标记(CMS remark):
- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程,由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
- 并发清除(CMS concurrent sweep):
- 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,基于标记结果,直接清理对象
- 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程在一起工作,所以总体上来看CMS,收集器的内存回收和用户线程是一起并发地执行
优点:
缺点:
- 并发执行对CPU的资源压力大
- 由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间
- 采用的标记-清除算法会导致大量碎片
- 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后不得不通过担保机制对堆内存进行压缩,CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC
应用场景:
- 适合应用在互联网站或者B/S系统的服务器上
- 这类应用尤其重视服务器的响应速度,希望系统停顿时间最短
- CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器
对应JVM参数是:-XX:UseConcMarkSweepGC
- 开启该参数后会自动将-XX:+UseParNewGC打开
- 开启该参数后,使用ParNew(Young区用)+CMS(Old区用)+Serial Old的收集器组合(Serial Old将作为CMS出错的后备收集器)
示例代码演示:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
结论:ParNew + CMS
(3)Serial Old
- Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,
- 使用标记-整理算法,
- 这个收集器也主要是运行在Client默认的Java虚拟机默认的老年代垃圾收集器
- 现在的用处是:作为老年代版本中使用CMS收集器的后备垃圾收集方案
如何选择上述的垃圾收集器?
- 单CPU或小内存,单机程序
- 多CPU,需要最大吞吐量,如后台计算型应用
- -XX:+UseParallelGC 或者
- -XX:+UserParallelOldGC
- 多CPU,追求低停顿,需要快速响应如互联网应用
- -XX:+UseConcMarkSweepGC
- -XX:+ParNewGC
2.3 G1收集器
(1)上述收集器的特点
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden+s0+s进行复制算法
- 老年代收集必须扫描整个老年代区域
- 都是以尽可能少而快速地执行GC为设计原则
(2)G1是什么?
- G1(Garbage-First)收集器,是一款面向服务端应用的收集器
- 应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求,另外,它还具有以下特点:
- 像CMS收集器一样,能与应用程序线程并发执行
- 整理空闲空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐性能
- 不需要更大的Java堆
- G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间
CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题,于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JDK1.7发布了G1垃圾收集器
- Oracle官方在JDK9中将G1变成了默认的垃圾收集器以替代CMS
- 主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个一样的region,每个region从1M到32M不等,一个region有可能属于Eden,Survivor或者Tenured内存区域
(3)特点
- 1.G1能充分利用多CPU多核环境硬件优势,尽量缩短STW
- 2.G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
- 3.宏观上看G1之中不再区分年轻代和老年代,把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘
- 4.G1收集器里面讲的整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域
- 5.G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换
(4)G1的底层原理
Region区域化垃圾收集器:
- 最大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可
- 区域化内存划片Region,整体变为了一些不连续的内存区域,避免了全内存区的GC操作
- 核心思想:
- 将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小
- 在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地位某个代服务,可以按需在年轻代和老年代切换
- 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
- 大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器
- 这些Region的一部分包含新生代
- 新生代的垃圾收集依然采用暂停所有应用线程的方式,将存或对象拷贝到老年代或Survivor空间
- 这些Region的一部分包含老年代
- G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作
- 这意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存问题的存在了
- 在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域
- 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这种巨型对象默认直接会被分配在老年代,
- 但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象
- 如果H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储,为了能找到连续的H区,有时候不得不启动Full GC
回收步骤:
- G1收集器下的Young GC
- 针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片
- Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部分晋升到Old区
- Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区
- 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行
四步过程:
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 最终标记:修正并发标记期间,因程序导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值对大化的回收
(5)案例
示例代码:
import java.util.Random;
public class GCDemo {
public static void main(String[] args) {
System.out.println("=====GCDemo,Hello====");
try {
String str = "GCDemo";
while (true){
str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
str.intern();
}
}catch (Throwable e){
e.printStackTrace();
}
}
}
配置JVM参数:
(6)常用配置参数
- -XX:+UseG1GC:开启GC
- -XX:G1HeapRegionSize=n:设置的G1区域的大小,值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域
- -XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
- -XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
- -XX:ConcGCThreads=n:并发GC使用的线程数
- -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
开发人员仅仅需要声明以下参数即可:
- 三步归纳:开启G1+设置最大内存+设置最大停顿时间
- -XX:+UseG1GC
- -Xmx32g
- -XX:MaxGCPauseMills=100
(7)G1相比CMS的优势
- 1.G1不会产生内存碎片
- 2.是可以精确控制停顿,该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域
(8)JVMGC结合SpringBoot微服务的调参优化
待补充
2.4 ZGC收集器
- 针对大内存堆的低延迟垃圾收集器
- 既能处理几百兆的小内存,也能处理几个TB的大堆(OMG)(ZGC具有强的伸缩性,就体现在对内存的掌控上面,小内存不能体现出它的优势,大内存才能体现它的优势)
- GC暂停时间不会超过10ms
- 和G1相比,应用吞吐能力不会下降超过15%
- 只支持64位系统
- ZGC的设计目标是:支持TB级内存容量,暂停时间长(<10ms),对整个程序的吞吐量的影响小于15%,将来还可以扩展实现机制,以支持例如多层堆(即热对象(频繁使用的对象)置于DRAM和冷对象(不经常使用的对象)置于NVMe闪存),或者压缩堆
特性:
- 1.着色指针
- 64位平台上一个指针可用位是64位,ZGC限制最大支持4TB的,这样寻址只需要42位,那么剩下22位就可以用来保存额外的信息
- 着色指针技术就是利用额外信息位在指针上对对象进行着色标记,
- 2.读屏障
- ZGC通过读屏障来解决GC线程和应用线程可能并发修改对象状态的问题,而不是简单粗暴通过Stop The World来做全局的锁定
- 使用读屏障在单个对象的处理上有概率被减速,
- 3.并发处理
- 由于读屏障的使用,进行垃圾回收的大部分时间都不需要Stop The World,因此ZGC的大部分时间都是并发处理
- 虽然ZGC大部分时间是并发处理,但是还是会有短暂的停顿
- 4.基于Region
- 这与G1算法一样,不过虽然也分了Region,但是并没有进行分代
- ZGC的Region不像G1那样是固定大小,而是动态决定大小,Region可以动态创建和销毁
- 这样可以对大对象更好的分配和管理
- 5.内存压缩(整理)
- MS算法清理对象时原地回收,会存在内存碎片问题,ZGC和G1一样也会在回收后对Region进行移动合并,解决了碎片问题
- 初始状态时:整个堆空间被划分成了许多不等的Region
- 开始进行回收时:ZGC首先会进行一个短暂的Stop The World,来进行GC Roots对象的标记(这个过程非常短,因为Roots的总数量通常比较小)
- 并发标记:通过对对象指针进行着色来进行标记,结合读屏障解决单个对象的并发问题,其实这个阶段最后的时候,还会有一个非常短的Stop The World停顿,用来处理一些边缘情况
- 清理阶段:把标记为不可用的对象进行回收
- 重定位:将GC后存活的对象进行移动,来腾出大块内存空间解决碎片问题,
- Root重定位:在重定位最开始会有一个短暂的Stop The World,用来重定位集合中的Root对象,暂停时间取决于Root的数量和重定位及与对象的总活动集的比率
- 并发重定位:这个过程也是读屏障与应用线程并发进行的
总结:ZGC只会在以下阶段会发生stop-the-world
- 1. GC开始时对root set的标记时
- 2. 在标记结束的时候,由于并发的原因,需要确认所有对象已完成遍历,需要进行暂停
- 3. 在重定位root-set 中的对象时
3.总结
总结: