深入学习GC机制
写在文章之前
Java的GC机制无处不在,但平时敲代码的时候都不会去注意它,这次就来揭开它的真面目。
GC的基础知识
1.什么是垃圾
C语言申请内存:malloc free
C++: new/delete
Java: new
自动内存回收,编程上简单,系统不容易出错,手动释放内存,容易出两种类型的问题:
- 忘记回收
- 多次回收
没有任何引用指向的一个对象或者多个对象(循环引用)
2.如何定位垃圾
- 引用计数
- 根可达算法
3.常见的垃圾回收算法
- 标记清除 - 位置不连续 产生碎片
- 拷贝算法 - 没有碎片,浪费空间
- 标记压缩 - 没有碎片,效率偏低
4.JVM内存分代模型(用于分代垃圾回收算法)
这里所image说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行。Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。如下图
年轻代(Young Generation)
对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在年轻代上的GC。年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区和两个存活区(Survivor 1 、Survivor 2)。
绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor1(此时,Survivor2是空白的,两个Survivor总有一个是空白的);此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor1;当Survivor1也满的时候,将其中仍然活着的对象直接复制到Survivor2,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor2(此时,Survivor0是空白的)。当两个存活区切换了几次(HotSpot虚拟机默认15次,CMS 6次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是复制算法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中)。
年老代(Old Generation)
对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。
可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
永久代(Permanent Generation方法区)
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
GC机制
1.概述
由上图可知,Serial,ParNew,Parallel Scavenge主要负责Young generation区域的垃圾回收,CMS,Serial Odl, Parallel Old主要负责Tenured generation区域的垃圾回收,G1在Young generation以及Tenured generation区域均可以使用。
前六种垃圾回收器介绍:
“Serial”是一个stop-the-world,使用单个GC线程复制收集器。
“ParNew”是一个stop-the-world,复制使用多个GC线程的收集器。不同的是从“并行清除”中可以看出,它有增强功能,使其可用CMS。例如,“ParNew”可以需要同步,以便在CMS的并发阶段。
“Parallel Scavenge”是一个stop-the-world,复制收集器,它使用多个GC线程。
“Serial Old”是stop-the-world,标记使用单个GC线程的扫描压缩收集器。
“CMS”是一个多并发、低暂停的收集器。
“Parallel Old”是一个使用多个GC线程的压缩收集器。为jdk6的收集器使用-XX标志。
注:Stop-The-World
在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
前六种垃圾回收器联系(图中虚线):
UseSerialGC是“Serial”+“Serial Old”
UseParNewGC是“ParNew”+“Serial Old”
UseParallelGC 是“Parallel Scavene”+“Serial Old”
UseParallelOldGC是“Parallel Scavenge”+“Parallel Old”
UseConcMarkSweepGC是“ParNew”+“CMS”+“Serial Old”。“CMS”是用来收集大部分时间的终身一代,当并发模式发生故障时,使用“Serial Old”。
2.典型的垃圾收集算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。
1.Mark-Sweep “标记-清除”算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2.Copying ”复制“算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
3.Mark-Compact ”标记-整理“算法(压缩法)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
3.垃圾收集器概述
jdk提供了多重垃圾收集器,下文会提供主流的垃圾收集器搭配组合,各种组合按照特点分为以下三类:
- 串行收集器:Serial + Serial Old;
- 并行收集器: Parallel Scavenge + Parallel Old,专注于应用吞吐量;( JDK1.8默认的垃圾回收)
- 并发收集器:CMS,G1,专注于响应时间。
3.1 Serial收集器
Serial收集器(Serial + Serial Old)的主要特点是单线程回收资源。当需要执行垃圾回收时,程序会暂停一切工作(又称为Stop The World,STW),使用复制算法完成垃圾清理工作。
优点:
- 简单高效,是Client模式下默认的垃圾收集器;
- 对于资源受限的环境,比如单核(例如Docker中设置单核),单线程效率较高;
- 内存小于一两百兆的桌面程序中,交互有限,则有限的STW是可以接受的。
缺点:
- 垃圾回收速度较慢且回收能力有限,频繁的STW会导致较差的使用体验。
ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集工作,其他的控制参数,收集算法,对象分配规则等均与Serial收集器一致。ParNew收集器在单核/双核环境下,效率未必有Serial收集器工作效率高(多线程切换开销等因素限制),当然随着核数的增加,其性能也会得到较大的提升。
3.2 Parallel收集器
Parallel收集器(Parallel Scavenge + Parallel Old)相比于Serial收集器的主要特点是,其是通过多线程完成垃圾的清理工作。其中Parallel Scavenge使用复制算法完成垃圾收集(Parallel Old使用标记整理算法),如果从这一点看其与ParNew相似,但实际上两者的出发点存在区别,区别如下所示:
- ParNew出发点在于加速资源回收的速度,以减少应用的STW时间;
- Parallel Scavenge出发点在于资源回收的吞吐量(吞吐量:用户线程时间/(用户线程时间 + GC线程时间)).
高吞吐量适合于交互较少的后台应用程序(诸如科学计算应用),能够更加充分的压榨CPU。开发者可以根据应用的实际情况,通过调整以下两个参数追求最优性能:
- 最大停顿时间:垃圾收集器在执行垃圾回收时终端应用执行的最大时间间隔,-XX:MaxGCPauseMills;
- 吞吐量:执行垃圾收集的时间与执行应用的时间占比,-XX:GCTimeRatio=1/(1+N)。
3.3 CMS收集器
CMS(Concurrent Mark Sweep)收集器是jdk 1.5推出的第一款真正意义上的并发收集器(针对老年代),实现了让垃圾收集器与用户线程(近似)同时工作,其具有以下特点:
- 基于"标记-清除"算法;
- 以获取最短回收停顿时间为目标;
- 并发收集,停顿时间短。
以上步骤中,最为耗费时间的并发标记与并发清除阶段,不需要应用程序暂停执行,所以垃圾回收的停顿时间较短。
160; CMS的垃圾收集过程比较复杂,主要步骤如下所示:
(1) CMS Initial Mark:初始标记Root(会STW,单线程执行,不过因为仅仅把GC Roots的直接可达对象标记一下,所以速度较快);
(2) CMS Concurrent Mark:并发标记;
(3) CMS Concurrent Preclean: 并发预清理;
(4) CMS Remark: 并发标记(会STW,此步骤是因为在并发标记的过程中可能会产生新的垃圾,需要重新标记新产生的垃圾);——使用Incremental Update算法。
(5) CMS Concurrent Sweep: 并发清除;
(6) CMS Concurrent Reset: 并发重置。
标记算法:三色标记算法(CMS,G1)
ZGC ,Shenandoah方案: 颜色指针(Colored Pointers)
缺点:
- 对CPU资源敏感:并发收集虽然不会暂停应用程序,但是会占用CPU资源从而降低应用程序的执行效率(CMS默认收集线程数量=(CPU数量 + 3) / 4);
- 产生浮动垃圾:在并发清除时,用户线程会产生新的垃圾,称之为浮动垃圾(并发清除时需要预留内存空间,不能像其他收集器在老年代几乎填满之后再进行收集工作)。
- 产生空间碎片:使用"标记-清除"算法,会产生大量不连续的内存碎片,从而导致在分配大内存对象时,无法找到足够的连续内存,从而需要提前触发一次Full GC操作。
针对以上缺点,可以从如下参数进行改进:
- -XX:ConcGCThreads:并发的GC线程数,从而降低CPU敏感度;
- -XX:CMSInitiatingOccupancyFraction:合理设置CMS的预留内存空间;
- -XX:+UseCMSCompactAtFullGCCollection: FullGC之后执行压缩操作,消减内存碎片;
- -XX:CMSFullGCBeforeCompaction: 执行多次FullGC之后执行压缩操作,消减内存碎片。
3.4 G1收集器
需要注意的是G1垃圾收集器在新生代以及老年代都能进行工作,这是因为相比于前面所介绍的垃圾收集器,它具有不同的堆内存结构。以前的垃圾收集器分代是划分为新生代、老牛代、持久带等。
G1将内存划分为多个大小相同的Region(1-32M,上限2048个),每个Region均拥有自己的分代属性,这些分代不需要连续。通过划分Region,G1可以根据计算老年代对象的效益率,优先回收具有最高效益率的对象(分代的内存不连续,GC搜索垃圾时需要全盘扫描找出对象引用情况,G1通过在每个Region中维护一个Remembered Set记录对象引用情况解决此问题)。具体如下图所示:
G1提供了两种GC模式,Young GC以及Mixed GC,两种GC都会STW。
3.4.1 Young GC
选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
3.4.2 Mixed GC
选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region,在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
Mixed GC不是Full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap(此时效率就会很低下)。所以我们可以知道,G1是不提供Full GC的。
在执行Mixed GC之前需要进行并发标记过程(Global Concurrent Marking),具体步骤如下图所示:
- Initial marking phase: 标记GCRoots(会STW);
- Root region scanning phase: 标记存活Region;
- Concurrent marking phase:标记存活的对象;
- Remark phase:重新标记(会STW);——使用STAB算法。
- Cleanup phase: 回收内存。
需要注意,Mixed GC并不是一次性执行完,其会分为多个步骤执行(具体可见下一篇关于GC日志的文章)。在每次执行时,G1会计算每个Region中垃圾占内存分段比例,如果超过了-XX:G1MixedGCLiveThresholdPercent,则进行回收操作。此外,G1中可以设置堆内存中有多少空间允许浪费,即-XX:G1HeapWastePercent,在并发标记结束后,可以知道有多少空间要被回收,在每次Young GC和发生Mixed GC之前,会检查垃圾占比是否到达了此阈值,只有到达了,才会发生Mixed GC。
常见问题
1) UseParNew和UseParallelGC都使用多个GC线程。哪个更快?
大多数情况下他们的表现都一样好,但在不同的情况下比其他人做得更好。如果你想用GC人机工程学,仅由UseParallelGC(和UseParallelOldGC)支持。
2) 如何将“CMS”与“Serial”一起使用?
-XX:+UseConcMarkSweepGC -XX:-UseParNewGC.
不要使用-XX:+UseConcMarkSweepGC和-XX:+UseSerialGC。尽管看起来一个逻辑组合,它将产生一条消息,说明冲突的收集器组合和JVM不会启动。
JVM调优第一步,了解生产环境下的垃圾回收器组合
-
JVM的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-
JVM参数分类
标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消
-XX:+PrintCommandLineFlags
-XX:+PrintFlagsFinal 最终参数值
-XX:+PrintFlagsInitial 默认参数值
参考资料
- https://blogs.oracle.com/jonthecollector/our-collectors
- https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
- https://blog.****.net/yrwan95/article/details/82829186
- https://www.cnblogs.com/jason1990/archive/2019/10/24/11732261.html