分析垃圾收集器,主要从以下几个方面入手:
1)什么是垃圾,什么样的对象要被回收?
2)如何定位垃圾?
3)垃圾清除算法
4)经典垃圾收集器
通俗来说:一个对象没有任何引用指向它,这样的对象就是垃圾。
1.引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
缺陷:
无法解决循环引用的问题。如下图:
A,B,C三个对象互相持有对方的引用,除此之外再没有任何引用了。这种循环引用无法解决。
2.GC根可达算法
GC根可达算法:
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GCRoots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
大白话:
从一个根出发,去遍历所有引用的对象,如果顺着根找不到的对象,就是垃圾。
可以固定作为GC根对象的对象:
1) 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2) 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3)·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。·
4) 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。·
5) Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。·
6) 所有被同步锁(synchronized关键字)持有的对象。
7) 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
扩展点,强软弱虚引用
强引用:是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()
软引用:是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
弱引用:也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
虚引用:也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
目前只有典型的三种垃圾清除算法,这三种都需要stw。分别是:
1)Mark-Sweep 标记清除。
2)复制 Copying
3) Mark-Compact (标记压缩)
1)标记清除
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2)Copying 复制
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
缺点:浪费空间,空间利用率只有一半。
优点:简单高效,无需考虑内存碎片。
3) Mark-Compact (标记压缩)
标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
优点:无碎片产生
缺点:需要移动对象,效率较低。
拓展点:
让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。
G1前面6种垃圾回收器都是分代回收模型。在G1开始,垃圾回收不再采用分代模型,G1还是在逻辑上有分代,但是物理上无分代。ZGC逻辑和物理上都无分代。
分代回收模型搭配,通常情况下:
ParNew + CMS (并行 + 并发标记清除)
Serial + Serial Old (串行)
Parallel Scavenge + Parallel Old (并行)
首先需要了解一下什么是分代(分区)模型?
将堆内存总体分为新生代与老年代,新生代分为eden去,survivior1, survivor2,新生代通常采用的是Copying(复制)算法,原因:主要是新生代的对象,生命周期普遍比较短暂,把存活的对象复制后,这样可以做到对一整块内存回收,效率也高,采用复制算法比较合适。
老年代通常使用的算法为:标记清除或者标记压缩,老年代内的一般是大对象或者长时间回收不了的对象。比较适合单独去标记对象。但是也带来了问题,就是老年代的内存碎片问题。
新生代:Eden + 2个Suvivor区,一般比例是8:1:1。对象回收被称为YGC或者MinorGC ,回收过程如下:
老年代:经过n次垃圾回收存活的对象(这个n被称为年龄阀值 ,默认是15次)。 老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域。老年代回收称之为:MajorGC或者FullGC。
对象分配整体流程图:
说明:部分小对象会直接尝试在栈上分配,如果失败,判断对象是否足够大,如果对象很大,直接接入老年代。如果不是很大,会采用TLAB(每个线程分配一块buffer,本质上是为了避免多个线程分配对象在eden区争抢),tlab也是在eden区的,如果可以用tlab分配,则使用tlab,否则直接在eden区分配。分配后,判断是否需要gc,eden区gc采用copying算法,直接把活着的对象copy到s1, s1再进行gc,判断对象年龄是否大于15,大于15直接进入老年代,否则进入s2。如此循环往复。
各种垃圾收集器详解
Serial收集器
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器。更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,主要是为了同CMS收集器搭档使用。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。是一款承上启下的,真正实现并发(同一时刻,工作线程和GC线程同时工作)收集的垃圾处理器。CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:1)初始标记(CMS initial mark)2)并发标记(CMS concurrent mark)3)重新标记(CMS remark)4)并发清除(CMS concurrent sweep)
初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
并发标记存在的问题:
1)gc线程和工作线程同时进行,假设一个对象A,一开始被标记为可回收,后面A对象又有了引用,这种就属于错标。
2)错标会在重新标记阶段,重新标记一遍,采用的算法是:三色标记法,这个比较复杂,还没搞懂。
3)还有一个问题,就是在进行并发清理的过程中,又有新的垃圾产生,这种垃圾叫做浮动垃圾,这种会在下一个阶段清理掉。
CMS存在的缺点:
1. CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。
2.CMS标记清除算法,会有过多的内存碎片,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore-Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。
G1收集器
G1是一款主要面向服务端应用的垃圾收集器,hotspot团队对他的期望是替换掉CMS。
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
G1也更有发展潜力。与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
1)什么是垃圾,没有任何引用执行的对象即垃圾,垃圾对象如何定位?传统有两种,引用计数和gc根可达算法,hotspot内采用的是gc根可达算法。
2)垃圾收集的常见算法:1)标记-清除,速度快,简单,容易产生内存碎片。2)复制算法。无内存碎片,但是空间利用率低。3)标记-压缩,标记后,对内存空间进行压缩,可以避免内存碎片,但是需要移动对象,性能偏低。
3)常见的垃圾收集器:1)serial系列,单线程收集。2)paralleel系列,多线程收集。3)Parnew + cms,年轻带多线程收集,老年代并发收集。4)G1 全新一代收集器,无明确的新生代老年代的概念。基于Region的堆内存布局。