目录
一.GC机制介绍
GC分析是为了进一步优化系统性能,性能优化是一个很大的领域,CPU、cache命中、IO各个方面都要综合进行考虑,这里我们只讲其中的一小部分,GC分析。在进行性能优化之前先要根据业务场景制定一个明确的性能需求指标,优化是一个无止境的事情,先制定好性能优化指标以便平衡投入和产出的问题。性能需求指标一般有以下几个:
应用预期的吞吐量是多少?
请求和响应之间的延迟预期是多少?
应用支持多少并发用户或并发任务?
当并发用户数或并发任务数达到最大时,可接受的吞吐量和延迟是多少?
最差情况下的延迟是多少?
要使垃圾收集引入的延迟在可容忍范围之内,垃圾回收的频率应该是多少?
二.GC判断方法
即如何判断Java对象需要被回收。
一般来说,我们把Java内存划分为以下几个区域,如图:
我们常说的垃圾回收指的是回收掉堆内存,那如何来判断堆内存里的对象需要回收呢,可以有以下两种办法:
一:引用计数算法
:
引用计数法记录着每一个对象被其它对象所持有的引用数,被引用一次就加一,引用失效就减一;引用计数器为0则说明该对象不再可用;当一个对象被回收后,被该对象所引用的其它对象的引用计数都应该相应减少,它很难解决对象之间的相互循环循环引用实例
二:可达性分析算法
从GC Root对象向下搜索其所走过的路径称为引用链,当一个对象不再被任何的GC root对象引用链相连时说明该对象不再可用,GC root对象包括四种:方法区中常量和静态变量引用的对象,虚拟机栈中变量引用的对象,本地方法栈中引用的对象; 解决循环引用是因为GC Root通常是一组特别管理的指针,这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针。
Java GC采用的是第二种方法,可达性分析算法。接下来我们来一起了解Java对象的四种引用。
1.强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :Person person = new Person(“sunny”);不管系统资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到。
2.弱引用 :通过WeakReference类实现,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收。
3.软引用 :通过SoftReference类实现,eg : SoftReference p = new SoftReference(new Person(“Rain”));内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。
4.虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。
三、垃圾回收算法
即如何回收Java对象
1、标记—清除算法
算法的执行过程与名字一样,先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:
1)标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的;
2)会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。
2、复制算法
复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。这样当垃圾收集器进行回收的时候就不用考虑空间碎片的问题,缺点在于把内存缩小为原来的一半,代价未免有点大。
当然,正是由于其缩小内存为原来的一半代价大的问题,现代的JVM并不是按照1:1划分内存空间的,二是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用其中的Eden和一块Survivor区。当回收的时候,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor中,最后把Eden和Survivor的空间清理出来。其实这里还有一个问题:就是如果垃圾回收后,存活的对象需要的空间大于剩余一块Survivor的空间怎么办?答案是需要依赖其他内存进行分配(这里主要指的是老年代)。
3、标记—整理算法
与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存
四、HostSpot垃圾回收器种类简单介绍
以垃圾回收机制分类,HostSpot垃圾回收器可被分四类,下面简单介绍下这四种回收机制。
1.Serial收集:
单线程收集,在垃圾回收时会“stop-the-world”,后面简称为STW,由于是单线程回收,停顿时间较长,比较适合Client模式的Java程序和内存使用量较小的程序。
2.Parallel收集:
多线程收集,在垃圾回收时同样也会产生STW现象,顾名思义,这种垃圾回收期会使用多个线程回收垃圾,在多核处理器上,可以大幅缩短停顿时间。
3.CMS收集:
为了尽可能减少STW对应用程序的应用,HotSpot设计者引入了CMS(concurrent mark sweep)垃圾回收期,这种方式只在初始标记阶段和重新标记阶段会发生STW,其余时间的垃圾回收操作可以和应用程序的工作线程并发执行,当然,这种方式也有其缺点,比如在年老代不满时就要进行回收、会出现许多内存碎片、为了减少每次的GC停顿时间会使总的GC停顿时间变长,影响吞吐量等等。
4.G1收集:
G1(garbage first)收集相对其他收集器有了革命性的改变,它将Java内存分成一些大小相等的区域,使得新生代内存和年老代内存可以在物理上不是连续的。
HotSpot针对上面四种垃圾收集器做了部分扩展,就形成了自己的七种垃圾回收器。
**1.Serial垃圾回收器—年轻代:**基于Serial收集机制,采用复制算法,新生的对象分配在Eden区和一个Survivor区(from区),经过一次垃圾回收后所有存活的对象被复制到另一个Survivor区(to区),当Survivor区内存不足以容纳这些存活对象时就会溢出到年老代,这对GC的伤害特别大,在实际开发中要尽量避免这种情况。使用-XX:+UseSerialGC参数开启。
**2.ParNew垃圾回收器—年轻代:**基于Parallel收集机制,采用复制算法,可以通过-XX:ParallelGCThreads=参数控制垃圾回收器所使用的线程数,使用-XX:+UseParNewGC开启。
**3.Parallel Scavenge垃圾回收器—年轻代:**基于Parallel搜集机制,采用复制算法,这个垃圾回收器与ParNew最大的不同就在于可设置的参数不同,我们可以更精确地控制GC停顿时间以及吞吐量(应用程序线程用时占程序总耗时的比例,比如应用程序运行了99s,GC垃圾回收停顿了1S,那么吞吐量就是99%),设置了GC停顿时间和吞吐量参数后,此垃圾回收器会优先满足最大停顿时间的目标,次之是吞吐量,最后才是新生代区域的最小值。另外此垃圾回收器还有一个自适应策略(-XX:UseAdaptiveSizePolicy),默认开启,这个策略可以动态调整内存区域的大小,包括晋升为年老代的年龄,建议不要轻易关闭此策略,除非你对自己的应用程序已经非常了解。设置最大停顿时间:-XX:MaxGCPauseMillis=,设置吞吐量:-XX:GCTimeRatio=,开启此垃圾回收器:-XX:+UseParallelGC。
**4.Serial Old垃圾回收器—年老代:**基于Serial收集机制,采用标记—整理算法,标记所有存活的对象,将其向内存一端移动,然后清理掉边界外的内存,是Jdk1.7的默认年老代垃圾回收器。
**5.Parallel Old垃圾回收器—年老代:**基于Parallel收集机制,采用标记—整理算法,使用-XX:+UseParallelOldGC开启。
**6.CMS垃圾回收器—年老代:**基于CMS收集机制,采用标记—清除算法,CMS垃圾回收器的处理分以下几个阶段:
• 初始标记(CMS-initial-mark) ,会导致swt;
• 并发标记(CMS-concurrent-mark),与用户线程同时运行;
• 预清理(CMS-concurrent-preclean),与用户线程同时运行;
• 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
• 重新标记(CMS-remark) ,会导致swt;
• 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
• 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
它只在初识标记和重新标记阶段STW,其他阶段可以和应用程序的程序并发执行,因此它的停顿时间是非常短暂的(低延迟应用),如果重新标记阶段暂停时间太长,可以通过-XX:+CMSParallelRemarkEnabled参数和-XX:+CMSScavengeBeforeRemark参数进行调优,前者会启用并行Remark,后者会在Remark执行之前进行一次Young gc,因为这个阶段,年轻代也是cms的Gcroot,cms会扫描年轻代指向年老代的引用,如果年轻代有大量引用需要被扫描,会使Remark阶段耗时增加。
但是此垃圾回收器的缺点也是很明显的。
(1)由于GC线程与应用程序并发执行时会抢占CPU资源,有较多的线程切换开销,因此会造成整体的吞吐量下降。
(2)采用标记—清除算法,会造成许多内存碎片的产生,有可能会在分配大对象时由于没有连续的内存空间而不得不进行一次Full GC,针对这种情况,提供了-XX:+UseCMSCompactAtFullCollection参数用于在Full GC之后进行一次碎片整理的工作,-XX:CMSFullGCsBeforeCompaction参数用于控制在进行几次全局GC后会进行一次碎片整理的工作。
(3)并发模式失败,由于CMS垃圾回收器在进行垃圾回收的同时应用程序还在运行,不断有新的垃圾产生,这部分垃圾在本次垃圾回收过程中无法处理,也是由于这个原因,CMS不能像其他垃圾回收器那样等到年老代几乎完全被填满了再去进行收集,而是需要预留一部分空间供并发收集时的程序使用,如果这部分预留的空间无法满足程序需求,就会出现Concurrent Mode Failure,这时候虚拟机会使用Serial Old垃圾回收器STW回收垃圾,我们可以通过-XX:CMSInitiatingOccupancyFraction来控制当年老代的内存占用达到多少时开始回收。使用-XX:+UseConcMarkSweepGC开启。
7.G1垃圾回收器—年轻代+年老代: 为解决CMS算法产生空间碎片和其他一系列缺陷,HotSpot提供了G1算法,通过-XX:+UseG1GC参数来启用,G1方面的改变较大,详细介绍请参见 https://tech.meituan.com/g1.html,下面节选部分入门知识介绍下。
传统的GC收集器将连续的内存空间划分为年轻代、年老代和永久代(JDK8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址是连续的,如下图所示:
而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址,如下图所示:
在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:
H-obj直接分配到了old gen,防止了反复拷贝移动。
H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。
在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
为了减少连续H-Objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size,可以通过-XX:G1HeapRegionSize参数配置,取值范围是1M到32M内2的指数。
G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。
Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
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的。
以上就是Java七种垃圾回收器的简单介绍,但年轻代的垃圾回收器和年老代的垃圾回收器并不是可以任意组合的,他们的组合关系如下图所示。