JVM—【02】认识JVM的垃圾回收算法与收集器

1. 对象存活判断

1.1. 引用计数算法 Reference Counting

  • 给对象添加一个引用计数器,每当有一个地方引用它的时候,计数器值就加一;当引用失效时,计数器值就减一;任什么时候刻计数器为0的对象就是不可能再被使用的。
  • 主流的JVM没有选用引用计数算法来管理内存,主要的缘由是它很难解决对象之间的相互循环引用的问题。

1.2. 可达性分析算法 Reachability Analysis

  • 经过一系列称为“GC-Roots”的对象做为起点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的(图论中的不可达)。
  • 可做为GC Roots的对象:

虚拟机栈(战争中的本地变量表)中引用的对象算法

方法区中类静态属性引用的对象安全

方法区中常量引用的对象数据结构

本地方法栈中JNI引用的对象多线程


1.3. 引用类型 Reference

  • 强引用:Strong Reference

指的是相似于Object object = new Object()这类引用,只要强引用存在,垃圾收集器就永远不会回收被引用对象。并发

  • 软引用:Soft Reference

描述一些还有用但并不是必要的对象。JDK提供了SoftReference来实现软引用微服务

在系统快要发生内存溢出以前,将会把这些对象列进回收范围之中进行第二次回收。若是此次回收尚未足够的内存,才会抛出内存溢出异常。布局

  • 弱引用: Weak Reference

用来描述非必须对象,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生以前。JDK提供了WeakReference类来实现弱引用。性能

当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。学习

  • 虚引用:Phantom Reference

也称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。JDK提供PhantomReference类来实现虚引用大数据

为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知


1.3. 引用类型 Reference

  • 不可达对象,会暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

    若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”。

    finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身——只要从新与引用链上的任何一个对象创建关联便可,譬如把本身(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;若是对象这时候尚未逃脱,那基本上它就真的被回收了。

    注:若是对象呗断定有必要执行finalize()方法,那么这个对象将会放置在一个叫作F-Queue队列中,并随后JVM会建立一个低优先级的Finalizer线程去执行它。JVM触发这个方法,并不确保它会执行结束,由于若是对象finalize方法若是执行缓慢或者死循环,将颇有可能会致使F-Queue队列其余对象永久等待,甚至致使整个内存回收系统奔溃。


2. 垃圾收集算

2.1. 标记-清除算法 Mark-Sweep

  • 算法分两个阶段,即标记和清除。

    1. 标记处所须要回收的对象
    1. 标记完成后统一回收全部被标记对象
  • 算法主要不足

    1. 效率问题,标记和清除两个过程效率都不高
    2. 空间问题,标记清除后悔产生大量不连续的空间

    空间碎片太多可能会致使之后分配大对象时没法找到足够连续内存存放而不得不触发另外一次垃圾收集。


2.2. 复制算法 Copying

  • 将可用的内存按照容量划分为大小相等的两块,每次使用其中一块。当前一块用完了,将还存活的对象移动到另外一块上面,而后把已使用过的内存空间一次性清理掉。这样每次都是堆整个半区进行内存回收,分配内存时也就不考虑内存碎片等复杂状况,实现简单、运行高效。代价是将内存缩小为原来的一半。

2.3. 标记-整理算法 Mark-Compact

  • 标记后不直接对可回收对象清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界觉得的内存。

2.4. 分代收集算法 Generational Collection

  • 把JVM堆内存分为新生代和老年代,对不一样的年代采起不一样的收集算法。

    在新生代中每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法。

    老年代中由于对象存活率高、没有额外空间对它进行分配担保,那就必须使用“标记-清理”或“标记-整理”算法来进行回收。


3. 垃圾收集算

3.1. 枚举根节点

  • 可达性分析从GC Roots节点找引用链操做,如今引用仅方法区就有数百兆,逐个检查里面的引用很是耗时。
  • 可达性分析对执行时间的敏感上体如今GC停顿上,这项分析工做必须在一个能确保一致性的快照中,

    这里的一致性是指在整个分析期间整个执行系统开起来像被冻结在某个时间节点上。若是这点不知足准确性就没法保证。这是致使GC进行时必须停顿全部Java执行线程的其中一个重要缘由。即便在CMS收集器(号称几乎不发生停顿)中枚举根节点也是必需要停顿的。

  • 主流的JVM都是使用的准确式GC,因此当执行系统停顿下来并不须要一个不漏检查完全部执行上下文和全局的引用位置,JVM知道哪里存放这个信息,在HotSpot使用了一组OopMap的数据结构来达到这个目的。

    在类加载完后,HotSpot吧对象内的各个偏移量上的类型计算出来,在JIT编译过程当中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。在GC扫描时,就能够直接知道这些信息。


3.2. 安全点 Safepoint

  • HotSpot在特定的位置记录栈和寄存器中哪些位置是引用,这个“特定位置”就称为“安全点”,即程序执行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

  • 安全点不能太多,也不能太少,太多增大系统负荷,太少GC等待时间太长。因此安全点的选择基本是以“是否具备让程序长时间执行的特征”为标准选定。

    由于每条指令执行时间都很是短暂,程序不太可能由于指令流长度太长而过长时间运行,因此长时间的特征就是指令序列复用循环跳转异常跳转

  • 怎样确保GC发生全部线程都跑到安全点再停顿下来,有两种方案:

    抢先式中断(Preemptive Suspension):在发生GC时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。

    主动式中断(Voluntary Suspension):当GC须要中断线程时,不对线程直接操做,仅简单设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真的时候就本身把中断挂起。轮询标志这个地方和安全点是重合的,另外再加上建立对象须要分配内存的地方。


3.3. 安全区域 Safe Region

  • 安全区域是指一段代码片断中,引用关系不会发发生变化。在这个区域中的任意地方开始GC都是安全的。
  • 在线程执行到Safe Region中的代码时,首先表示本身进入了Safe Region,这这段时间里,JVM要发起GC时,就不用管标识本身为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),若是完成了,那线程就继续执行,不然它就必须等待知道收到能够安全离开Safe Region的信号为止。

4. 垃圾收集器

4.1. Serial收集器

  • 是一个单线程垃圾收集器,它只会使用一个CPU或者一条收集线程去完成垃圾收集工做。
  • 它在进行垃圾收集时,必须暂停其余全部的工做线程,知道收集结束。
  • 适用于Client。
  • 新生代使用复制算法,暂停全部线程;老年代使用标记-整理算法,暂停全部线程。

4.2. ParNew收集器

  • Serial的多线程版本,除了使用多条线程进行垃圾收集以外,其他行为包括Serial收集器的可用参数、收集算法、Stop The World、对象分配规则、回收策略都与Serial收集器彻底同样

  • 除了Serial收集器外,目前只有它能与CMS收集器配合工做。

    ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可使用-XX:+UseParNewGC选项强制指定。

    ParNew在单核下不会比Serial收集器效果好

    可使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。


4.3. Parallel Scavenge收集器

  • 他是一个新生代处理器,也是使用复制算法的收集器,也是并行的多线程处理器。

  • Parallel Scavenge收集器的目的是达到一个可控制的吞吐量。

    吞吐量 = 运行用户代码的时间 / (运行用户代码时间 + 垃圾收集时间)

  • 它提供了两个参数控制吞吐量:控制最大垃圾收集停顿的时间-XX:MaxGCPauseMillis,直接设置吞吐量大小-XX:GCTimeRatio

    -XX:MaxGCPauseMillis:容许的值是一个大于0的毫秒数,收集器将尽量地保证内存回收花费不超过设定值,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。

    -XX:GCTimeRatio:参数的值是大于0小于100的整数,就是垃圾收集时间占总时间的比率,如:19,容许最大的时间就是1/(1+19);99,容许最大的时间就是1/(1+99)

  • Parallel Scavenge参数:-XX:UseAdaptiveSizePolicy

    -XX:UseAdaptiveSizePolicy 打开这个参数,就不须要手工指定新生代大小、Eden与Survivor区的比列、晋升老年代对象大小等细节参数。虚拟机会根据当前系统的运行状况收集性能监控,动态调整这些参数以提供最适合的停顿时间和最大吞吐量,这种调节方式称为GC自适应调整策略(GC Ergonomics)


4.4. Serial Old收集器

  • Serial收集器的老年代版,单线程,使用“标记-整理”算法
  • 做为CMS收集器的后背元,在并发收集发生Concurrent Mode Failure时使用。

4.5. Parallel Old收集器

  • 是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。
  • 在注重吞吐量以及CPU资源敏感的场景,能够优先考虑Paralled Scavenge+Parallel Old收集器。

4.6 CMS(Concurrent Mark Swap) 收集器

  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适用于互联网站和B/S系统的服务端上。并发收集、低停顿。

  • CMS收集器是基于“标记-清除”算法实现,过程分为4步:

    初始标记(CMS initial mark):仅仅是标记一下GC Roots能直接关联到的对象,速度很快。

    并发标记(CMS concurrent mark):进行GC Roots Tracing的过程。

    从新标记(CMS remark):是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录。停顿时间通常比初始标记更长,远比并发标记时间短。

    并发清除(CMS concurrent sweep)

    其中初始标记从新标记这两个步骤仍然须要“stop the world”

  • CMS的几个缺点:

    对CPU资源很是敏感:它虽然不会致使用户线程停顿,可是启用线程,消耗CPU运算资源,会致使引用程序变慢,总吞吐量下降。CMS的默认启用回收的线程数是(CPU数量 + 3)/ 4.也就是说,CPU越少,占用性能越多,对程序的影响就越大。为了应对这种情况,JVM提供了“增量式并发收集器”(Incremental Concurrent Mark Swap/i-CMS),使用抢占式来模拟多任务机制,在并发标记和清理的时候让GC线程、用户线程交替运行。尽可能减小GC线程独占资源的时间,这样整个垃圾收集时间过程会更长,可是对用户的影响就显得更少。

    CMS没法处理“浮动垃圾(Floating Garbage)”,可能出现“Concurrent Mode Failure”失败而致使另外一次Full GC的产生。浮动垃圾即在CMS并发清理时用户线程还在运行产生的心垃圾,这部分垃圾出如今标记事后,没法再当次处理。正由于用户线程还在运行,就须要预留一部份内存给用户线程使用,因此CMS能够设置触发百分比:-XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSInitiatingOccupancyOnly 前者设置百分比,后者设置只用设置的百分比,不让JVM自动调整,若是不设置后面的,第一次会使用70,随后就会随JVM自动调整了。若是CMS运行时,预留内存没法知足须要,就会出现“Concurrent Mode Failure”,这是JVM就会启用后后备方案使用Serial Old来从新进行老年代收集。因此比例不能设置过高,否则就会容易引发Concurrent Mode Failure,性能反而下降。

    CMS是基于“标记-清除”算法实现的,因此收集结束后会有大量的空间碎片产生。虽然空间不少,可是没法给大对象找到一片连续的空间,从而不得不触发一次Full GC。为了解决这个问题,CMS提供了一个-XX:+UseCMSCompactAtFullCollection,用于在CMS要进行Full GC的时候开启内存碎片合并整理,这个过程没法并发进行,空间碎片问题解决,可是停顿时间变长。CMS还有一个-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的,为0是表示每次进入Full GC 都压缩。


4.7 G1(Garbage-First)收集器

  • G1是一款面向服务端应用的垃圾收集器。HotSpot开发来替代CMS的,特色以下:

    并行与并发: G1能充分利用多CPU、多核环境下的硬件优点,使用多个CPU来缩短Stop-The-World停顿的时间,部分其余收集器本来须要停顿Java线程执行的GC动做,G1收集器仍然能够经过并发的方式让Java程序继续执行。

    分代收集: 分代概念在G1中依然得以保留。G1能够不须要其余收集器配合就能独立管理整个GC堆,它可以采用不一样的方式去处理新建立的对象和已经存活了一段时间、熬过屡次GC的旧对象以获取更好的收集效果。G1能够本身管理新生代和老年代。

    可预测的停顿: 下降停顿时间是G1和CMS共同的关注点,G1除了追求低停顿外,还创建可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已是实时Java(RTSJ)的垃圾收集器的特征了。G1能够有计划的避免在整个JVM堆中进行垃圾收集,能够对每一个region里的回收对象价值(回收该区域的时间消耗和能获得的内存比值)进行分析,在最后筛选回收阶段,对每一个region里的回收对象价值(回收该区域的时间消耗和能获得的内存比值)最后进行排序,用户能够自定义停顿时间,那么G1就能够对部分的region进行回收!这使得停顿时间是用户本身能够控制的!

    空间整合,没有内存碎片产生:因为G1使用了独立区域(Region)概念,G1从总体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但不管如何,这两种算法都意味着G1运做期间不会产生内存空间碎片。

  • 在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。

  • G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内能够获取尽量高的收集效率。

  • G1收集器中,Region之间的对象引用以及其余收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每一个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference引用的对象是否处于不一样的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),若是是,便经过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set便可保证不对全堆扫描也不会有遗漏。

  • 不计算维护Remembered Set的操做,G1收集器的运做大体可划分为如下几个步骤:

    初始标记(Initial Marking)

    并发标记(Concurrent Marking)

    最终标记(Final Marking)

    筛选回收(Live Data Counting and Evacuation)


关于我

  • 坐标杭州,普通本科在读,计算机科学与技术专业,20年毕业,目前处于实习阶段。
  • 主要作Java开发,会写点Golang、Shell。对微服务、大数据比较感兴趣,预备作这个方向。
  • 目前处于菜鸟阶段,各位大佬轻喷,小弟正在疯狂学习。
  • 欢迎你们和我交流鸭!!!
相关文章
相关标签/搜索