【JVM基础篇】Java垃圾回收器介绍
本文档详细解析了Java虚拟机(JVM)中垃圾回收器的原理与实践,涵盖Serial、ParNew、CMS、Parallel Scavenge与Parallel Old、G1等主流垃圾回收器的工作机制、优缺点及适用场景。特别聚焦于G1垃圾回收器,阐述其创新的Region分区管理、年轻代与混合回收策略,以及如何通过参数调整实现低延迟垃圾回收。适合希望深入了解JVM内存管理和优化的开发者阅读。
垃圾回收器(垃圾回收算法实现)
垃圾回收器是垃圾回收算法的具体实现。由于垃圾回收器分为年轻代和老年代,除了G1(既能管控新生代,也可以管控老年代)之外,新生代、老年代的垃圾回收器必须按照hotspot的要求成对组合进行使用(需要根据JDK的版本来选择相应的组合)
具体的组合关系如下:
JVM底层源码中,在某些特殊情况,CMS回收器会调用Serial Old回收器
Arthas查看所使用的垃圾回收器
不能直接看到使用了什么垃圾回收器,只能根据算法来推断
年轻代-Serial垃圾回收器
Serial是一种单线程串行回收年轻代的垃圾回收器。
回收年代和算法
- 年轻代
- 复制算法
优点
单CPU处理器下吞吐量非常出色
缺点
多CPU下吞吐量不如其他垃圾回收器(单线程,只使用了一个CPU),堆如果偏大会让用户线程处于长时间的等待
适用场景
Java编写的客户端程序或者硬件配置有限(CPU核数不多)的场景
如何使用
-XX:+UseSerialGC
:新生代、老年代都使用串行回收器。
老年代-SerialOld垃圾回收器
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
回收年代和算法
- 老年代
- 标记-整理算法
优点
单CPU处理器下吞吐量非常出色
缺点
多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
适用场景
与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
如何使用
-XX:+UseSerialGC
:新生代、老年代都使用串行回收器。
年轻代-ParNew垃圾回收器
ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收
回收年代和算法:
- 年轻代
- 复制算法
优点
- 多CPU处理器下停顿时间较短
缺点
- 吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用
适用场景
- JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
如何使用
-XX:+UseParNewGC
:新生代使用ParNew回收器, 老年代使用串行回收器
老年代-CMS(Concurrent Mark Sweep)垃圾回收器
CMS垃圾回收器关注的是系统的暂停时间(尽量减少STW,优化用户体验),允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
回收年代和算法
- 老年代
- 标记清除算法
优点
- 系统由于垃圾回收出现的停顿时间较短,用户体验好
缺点
1、内存碎片问题
2、退化问题(在某些特定情况,会退化为SerialOld这种单线程回收器)
3、浮动垃圾问题(回收过程,有的垃圾回收不掉)
适用场景
大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
使用
XX:+UseConcMarkSweepGC
,可以分别设置年轻代和老年代的回收器
CMS执行步骤
- 初始标记,用极短的时间标记出GC Roots能直接关联到的对象。
- 并发标记, 标记所有的对象,用户线程不需要暂停。(虽然并发标记是和用户线程一起执行,但是如果并发标记占用的资源较高,也会影响用户线程)
- 重新标记(并发),由于并发标记阶段有些对象会发生了变化,存在错标(用的对象本来是存活的,标记之后,用户线程把它变得不存活了,导致错标)、漏标(因为是并发的,可能有的对象是用户线程刚刚创建出来的,就会导致漏标)等情况,需要重新标记。
- 并发清理,清理死亡的对象,用户线程不需要暂停。
注:只有初始标记、重新标记阶段会出现STW
缺点:
1、CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,为了不影响对象的分配,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停,可以使用-XX:CMSFullGCsBeforeCompaction=N 参数(默认0)调整N次Full GC之后再整理。
2.、无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收(在本次清理的过程中,用户线程并发创建了一些对象,但是很快没有再使用,这些对象在这次清理中没有得到回收,需要等到下一次清理,所以称这些为浮动垃圾)。
3、如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代。
并发线程数:
在CMS中并发阶段运行时的线程数可以通过-XX:ConcGCThreads
参数设置,由系统计算得出,计算公式为(-XX:ParallelGCThreads定义的线程数 + 3) / 4
, ParallelGCThreads是STW停顿之后的并行线程数
ParallelGCThreads是由处理器核数决定的:
1、当cpu核数小于8时,ParallelGCThreads = CPU核数
2、否则 ParallelGCThreads = 8 + (CPU核数 – 8 )*5/8
我的电脑上逻辑处理器有12个,所以ParallelGCThreads = 8 + (12 - 8)* 5/8 = 10,ConcGCThreads = (-XX:ParallelGCThreads定义的线程数 + 3) / 4 = (10 + 3) / 4 = 3
最终可以得到这张图:
并发标记和并发清理阶段,会使用3个线程并行处理。重新标记阶段会使用10个线程处理。由于CPU的核心数有限,并发阶段会影响用户线程执行的性能。
年轻代-Parallel Scavenge垃圾回收器
Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。为了拉高吞吐量,PS会自动调整堆内存大小(调整新生代、老年代内存大小、晋升的阈值)。
回收年代和算法
- 年轻代
- 复制算法
优点
- 吞吐量高,而且支持手动设置参数控制吞吐量。为了提高吞吐量,虚拟机会动态调整堆的参数(用户只需要设置吞吐量,不需要设置内存大小等其他参数)
缺点
- 不能保证单次的停顿时间,但是支持设置STW时间
适用场景
- 后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据的处理,大文件导出
常用参数
Parallel Scavenge允许手动设置最大暂停时间和吞吐量。Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
- 最大暂停时间,
-XX:MaxGCPauseMillis=n
设置每次垃圾回收时的最大停顿毫秒数 - 吞吐量,
-XX:GCTimeRatio=n
设置吞吐量为n(用户线程执行时间 = n/(n + 1)) - 自动调整内存大小,
-XX:+UseAdaptiveSizePolicy
设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小,这个参数默认是开启的(Oracle建议使用PS组合的时候,不要设置堆内存的最大值,让垃圾回收器自动调整)
注:最大暂停时间和吞吐量这两个指标是冲突的,垃圾回收器会尽量满足最大暂停时间(有时候设置得太小,是没办法满足的,会超出所设置得最大暂停时间),牺牲吞吐量。如果要同时设置最大暂停时间和吞吐量,要多做测试,让它们比较协调
老年代-Parallel Old垃圾回收器
Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。
回收年代和算法
- 老年代
- 标记-整理算法(其实是标记+清除+整理)
优点
- 并发收集,在多核CPU下效率较高
缺点
- 暂停时间会比较长
适用场景
- 与Parallel Scavenge配套使用
如何使用
JDK8设置参数,就是默认使用该回收器
参数:-XX:+UseParallelGC
或 -XX:+UseParallelOldGC
可以使用Parallel Scavenge + Parallel Old这种组合。
测试
-XX:+PrintFlagsFinal
:可以在程序启动的时候打印所有配置项的最终值,可以看看自动调整功能有没有开启
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 垃圾回收器案例3
*/
//-XX:+UseSerialGC -Xmn1g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc -XX:+PrintFlagsFinal
//-XX:+UseParNewGC -Xmn1g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc
//-XX:+UseConcMarkSweepGC
//-XX:+UseG1GC -Xmn8g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc MaxGCPauseMillis
//-XX:+PrintFlagsFinal -XX:GCTimeRatio = 19 -XX:MaxGCPauseMillis=10 -XX:+UseAdaptiveSizePolicy
public class GcDemo2 {
public static void main(String[] args) throws IOException {
int count = 0;
List<Object> list = new ArrayList<>();
while (true){
//System.out.println(++count);
if(count++ % 10240 == 0){
list.clear();
}
// byte[] bytes = new byte[1024 * 1024 * 1];
list.add(new byte[1024 * 1024 * 1 / 2]);
// System.gc();
}
}
}
【测试最大暂停时间=10】
内存大小如下:
【测试最大暂停时间=1】
结论:堆内存越小,垃圾回收暂停时间越短
G1(Garbage First)垃圾回收器(极力推荐)
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
- Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小。
- CMS关注暂停时间,但是吞吐量方面会下降。
- 而G1设计目标就是将上述两种垃圾回收器的优点融合:
JDK9之后强烈建议使用G1垃圾回收器。(在JDK7、8上,G1可能不是很成熟,要慎重考虑)
G1出现之前的垃圾回收器,年轻代和老年代一般是连续的,如下图:
G1内存结构发生了变化:G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m
指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
G1垃圾回收有两种方式
1、年轻代回收(Young GC)
2、混合回收(Mixed GC)
年轻代回收(Young GC)
只回收年轻代,回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数
-XX:MaxGCPauseMillis=n
(默认200) 设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。
【Young GC步骤】
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%,如果年轻代的内存占了总堆的60%以上,就要Young GC),无法分配对象时需要回收时会执行Young GC。
2、标记出Eden和Survivor区域中的存活对象,
3、根据配置的**最大暂停时间选择某些区域(和其他回收器的区别:其他回收器会回收负责的所有区域)**将存活对象复制到一个新的Survivor区中(年龄+1)(使用复制算法,不会产生内存碎片),清空这些区域。
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。比如-XX:MaxGCPauseMillis=n
(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。原来的Survivor中年龄没有到达15的对象,还是会迁移到新的Survivor中。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous(翻译:巨大的)区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region(一个Region放不完)。
7、多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时
(-XX:InitiatingHeapOccupancyPercent
默认45%)会触发混合回收Mixed GC,回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。
混合回收(Mixed GC)
混合回收分为如下步骤,看起来和CMS差不多,但是步骤里面都是有区别的:
- 初始标记(initial mark)
- 并发标记(concurrent mark)
- 最终标记(remark或者Finalize Marking):只管漏标,不管新创建、不再关联的对象。这里使用的算法远比CMS的快
- 并发清理(cleanup):G1对老年代的清理会选择存活度最低的区域来进行回收(A区:100个对象只存活1个;B区:100个对象存活99个;G1选择回收A区),这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来。最后清理阶段使用复制算法,不会产生内存碎片。
注意:**如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC,**单线程执行标记-整理算法,此时会导致用户线程的暂停,影响用户的正常使用。所以尽量保证应该用的堆内存有一定多余的空间,如果堆内存的占用比较高,就要考虑优化。
如何使用G1垃圾回收器
参数1: -XX:+UseG1GC
打开G1的开关,JDK9之后默认不需要打开
参数2:-XX:MaxGCPauseMillis=毫秒值
最大暂停的时
回收年代和算法
- 年轻代+老年代
- 复制算法
优点
- 支持巨大的堆空间回收,并有较高的吞吐量。对比较大的堆延迟可控,如超过6G的堆回收时
- 不会产生内存碎片
- 并发标记的SATB算法效率高,比CMS的算法效率高
- 支持多CPU并行垃圾回收
- 允许用户设置最大暂停时间
缺点
- JDK8的早期版本还不够成熟
适用场景
- JDK8最新版本、JDK9之后建议默认使用
测试
使用以下代码测试g1垃圾回收器,打印出每个阶段的时间:
package chapter04.gc;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 垃圾回收器案例3
*/
//-XX:+UseG1GC -Xmn8g -Xmx16g -XX:SurvivorRatio=8 -XX:+PrintGCDetails -verbose:gc
public class GcDemo2 {
public static void main(String[] args) throws IOException {
int count = 0;
List<Object> list = new ArrayList<>();
while (true){
//System.out.println(++count);
if(count++ % 10240 == 0){
list.clear();
}
// byte[] bytes = new byte[1024 * 1024 * 1];
list.add(new byte[1024 * 1024 * 1 / 2]);
// System.gc();
}
}
}
每个region大小为2m,一共有84个young区,26个幸存者区。
初始标记花了0.0478秒,初始标记是伴随着年轻代回收的,0.0478秒是两个操作的总时间
并发标记总共耗时10ms,不会产生STW。
最终标记,效率较高
清理阶段
垃圾回收器总结
垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选择如下:
- JDK8及之前:
- ParNew + CMS(关注暂停时间)
- Parallel Scavenge + Parallel Old (关注吞吐量)
- G1(JDK8之前不建议,较大堆并且关注暂停时间)
- JDK9之后:G1(默认)。从JDK9之后,由于G1日趋成熟,JDK默认的垃圾回收器已经修改为G1,所以强烈建议在生产环境上使用G1。G1的实现原理将在《原理篇》中介绍,更多前沿技术ZGC(最新的垃圾回收器)、GraalVM将在《高级篇》中介绍。
几个问题
Java中有哪几块内存需要垃圾回收?
方法区回收,Java程序员少关注
有哪几种常见的引用类型?
- 强引用,最常见的引用方式,由可达性分析算法来判断
- 软引用,对象在没有强引用情况下,内存不定时会回收
- 弱引用,对象在没有强引用情况下,会直接回收
- 虚引用,通过虚引用知道对象被回收了
- 终结器引用,对象回收时可以自救,不建议使用
有哪几种常见的垃圾回收算法?
常见的垃圾回收器有哪些?
Serial+Serial Old
:单线程回收,适用于单核CPU场景ParNew+CMS
:暂停时间较短,适用于大型互联网应用中与用户交互的部分Paraller Scavenge+Parallel Old
:吞吐量高,适用于后台进行大量数据操作G1
:适用于较大堆,具有可控暂停时间
文章说明
该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。
更多推荐
所有评论(0)