JVM笔记(1.2)垃圾收集器和内存分配策略

垃圾收集器(GC)的做用相信你们都知道,它将咱们的不用的内存空间给回收,Java的垃圾收集器是"动态分配内存和垃圾收集"的。正由于它是动态的,因此不少人都忽略了它,但当出现一些内存泄漏、内存溢出的问题时,咱们必须掌握JVM才能去解决问题java

如今,咱们从GC设计者的角度来看它须要完成哪些工做:

  1. 哪些内存须要回收
  2. 何时回收
  3. 如何回收

对于第一个问题:

上文中,咱们说到程序计数器、虚拟机栈、本地方法栈这3个区域随线程生,随线程死算法

栈中的栈帧随着方法的进入和退出而出栈、入栈,每一个栈帧分配多少内存基本在类结构肯定时就是已知的(不包括JIT的优化)数组

而Java堆和方法区只在程序运行期间才会知道开辟的空间(),这部份内存分配和回收是动态的,因此垃圾收集器关注的这部份内存安全

对于第二个问题

当一段内存再也不使用(不处于存活状态)时就回收,下文会谈到哪些内存将再也不使用bash

对于第三个问题

这就是咱们下文要讲到的各类回收机制服务器

判断对象是否'存活'

首先,来看堆,堆中存放了几乎全部的实例对象,在对堆进行回收内存时,要先判断哪些对象能被回收(存活)。多线程

引用计数法

每当有一个地方引用它,计数器+1,每当一个引用失效,计数器-1;任什么时候刻计数器为0的对象是不能被使用的。并发

存在的问题

这种分析虽然简单,但有一个问题,如循环引用:布局

public class A {
    Object obj;
    
    public void testGC(){
        A a1 = new A();
        A a2 = new A();
        a1.obj = a2;
        a2.obj = a1;
        a1 = null;
        a2 = null;
        System.gc();//若是采用引用记数法则不回收
    }
}
复制代码

可达性分析算法

因此,咱们须要一种更全面的回收机制性能

思路:

经过一系列被称为“GC Roots”的对象做为起点,从这些节点开始往下搜索,搜所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,则证实此对象是不可用的

image

可做为GC Roots的对象:

  • 虚拟机栈中引用的对象
  • 方法去中类静态属性的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象

引用

垃圾收集器判断对象存活都和引用有关,下面来看引用有哪些类型

  • 强引用:广泛存在,如Object obj = new Object();只要引用在,就不会回收
  • 软引用:jdk1.2以后,SoftReference类实现软引用。在系统发出内存溢出以前,会把这些对象二次回收,若还不够,抛出异常。
  • 弱引用:jdk1.2以后,WeakReference来实现弱引用。只能生存到下一次垃圾收集发生以前
  • 虚引用:jdk1.2以后,PhantomReference类实现虚引用。这个对象被系统回收时收到一个通知

生存仍是死亡

一个对象要被宣告死亡,要经历两部:

  • 若是对象进行可达分析后没有和GC Roots相连,那她将会被第一次标记而且进行一次筛选,若是有finalize()方法则放置在F-Queue队列中执行finalize()方法,(不保证它有运行结果)
  • 若是是第二次被标记而且没有引用,那就只有被回收了

一个对象的finalize()方法只会被执行一次

在Java9中,finalize()方法已被弃用,缘由以下:

  • finalize机制可能会致使性能问题,死锁和线程挂起。
  • finalize中的错误可能致使内存泄漏;若是不在须要时,也没有办法取消垃圾回收;而且没有指定不一样执行finalize对象的执行顺序。
  • 没有办法保证finlize的执行时间。

回收方法区

永久代的方法区分为两部分:

  • 废弃常量(如String常量)
  • 无用的类(同时知足如下三种为无用的类)
    • 该类全部实例都已经被回收,Java堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问

垃圾收集算法

知道了要回收哪些东西,咱们还要知道如何回收,下面来看一下典型的垃圾回收算法

标记-清除算法

分为标记清除两个阶段:首先标记出全部须要回收的对象,在标记完成后赞成收回被标记的对象

image

不足:
  • 效率:标记和清除两个过程效率都不高
  • 空间:标记清除后会产生大量不连续的内存碎片(这会致使如有大对象但找不到连续内存时必须再触发一次垃圾收集)

复制算法

他将内存分为两块,每次只使用其中一块。当一块内存用完后,就将还存活的对象复制到另外一块上面,再将已使用的内存一次清理掉。

image

可是,咱们通常不将它对半分,而是分为一块较大的Eden和两块较小的Survivor区域,HotSpot默认Eden:Survivor比例大小为8:1,即Eden为收集前的空间,一块Survivor为收集后的大小,只浪费了10%的空间。

注:当每次回收有大于10%的对象存活时,经过分配担保机制让Survivor中剩余存不下的进入老年代

标记-整理算法

标记过程和标记-清除算法同样,清理以前,让全部存活的对象都向一端移动,而后直接清理掉边界之外的内存。

image

分带收集算法

根据对象存活周期的不一样划分为几块,通常为新生代和老年代

  • 新生代中,有大量对象死去,用复制算法
  • 老年代中,存活率高,必须使用标记-清理或者标记-整理算法来回收

HotSpot算法实现

以上为理论的垃圾收集算法,实际如HotSpot虚拟机会对算法有严格的考量。。。

枚举根节点

时间消耗:
  • 查找GC Roots节点
  • GC停顿,整个分析期间整个执行系统就像被冻结在某个时间点上,由于查找时不能出现分析时对象过程稿还在不停变化的状况

OopMap:虚拟机用它来得知哪些地方存放着对象引用,在类加载完成后,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程当中,也会在特定位置记录栈和寄存器中哪些位置是引用

安全点

程序在安全点才能暂停执行GC,因此安全点通常选定为“是否具备让程序长时间执行的特征”(如方法调用,循环跳转,异常跳转),前文“特定位置”就被称为安全点。

如何让全部线程都跑到最近安全点上停下来:

  • 抢先式中断:把全部线程中断,若是有中断线程不在安全点上,恢复线程,让它跑回安全点上(几乎没有了)
  • 主动式中断:设置一个标志,让各个线程去轮询这个标志,发现中断标志为真时就本身中断挂起,轮询标志的地方和安全点重合。

安全区域(Safe Region)

安全点保证了程序执行时在不太长的时间内就会遇到可进入的GC的安全点,例如线程处于SLeep或Blocked状态,这时线程没法响应JVM中断请求,这种状况,就须要安全区域来解决。

安全区域是指在一段代码中,引用关系不会发生变化。

当线程执行到了安全区域中的代码,标识本身进入了Safe Region,挡在这段时间里JVM要发起GC时,就不用管标识本身为Safe Region状态的线程了。在线程要离开Safe Region时,去检查是否完成根节点枚举,若是完成,线程继续执行;不然它就必须等待直到收到能够安全离开Safe Region的信号为止。

垃圾收集器

能够理解为收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

image

Serial收集器

特色:

  • 单线程收集器:在他进行垃圾收集时,必须暂停其余工做线程,直到它收集结束

image

ParNew收集器

特色:

  • Serial收集器的多线程版本,除了Serial收集器外,只有它能和CMS收集器合做
    image

Parallel Scavenge收集器

特色:

  • Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器
  • 该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它一样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

若是在Server模式下,主要两大用途:

  • (1)在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用
  • (2)做为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

image

Parallel Old收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。

image

CMS收集器

这类应用尤为重视服务器的响应速度,但愿系统停顿时间最短,以给用户带来较好的体验。CMS收集器就很是符合这类应用的需求

CMS收集器是基于“标记-清除”算法实现的。它的运做过程相对前面几种收集器来讲更复杂一些,整个过程分为4个步骤:

  • 初始标记:标记一下GC Roots能直接关联的对象,速度快
  • 并发标记:GC Roots Tracing的过程
  • 从新标记:修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,通常比初始标记阶段稍长,但比并发标记时间短
  • 并发清除:清除

其中,初始标记、从新标记这两个步骤仍然须要“Stop The World”.

CMS收集器主要优势:

  • 并发收集
  • 低停顿。

CMS三个明显的缺点:

  • CMS收集器对CPU资源很是敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种状况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所作的事情和单CPU年代PC机操做系统使用抢占式来模拟多任务机制的思想
  • CMS收集器没法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而致使另外一次Full GC的产生。"浮动垃圾"是指CMS并发清理时用户线程还在运行,伴随程序运行有新垃圾出现,这一部分垃圾在标记以后出现,因此本次没法清理。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,若是在应用中蓝年代增加不是太快,能够适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提升触发百分比,以便下降内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阀值已经提高至92%。
  • CMS是基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,可是没法找到足够大的连续空间来分配当前对象,不得不提早出发FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是没法并发的,空间碎片问题没有了,但停顿时间变长了。虚拟机设计者还提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)

G1收集器

G1收集器的优点:
  • 并行与并发 (停顿时间少)
  • 分代收集 (采用不一样的收集方式处理不一样年代的堆)
  • 空间整理 (标记整理算法,复制算法)
  • 可预测的停顿 (让使用者能控制一次收集的时间长度👏)
G1采用的堆布局:

使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(称为Region,大小为2的幂次方,如1M,2M,4M),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region的集合。

可以预测停顿的时间的缘由:

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

G1 内存“化整为零”的思路:

问:若一个对象在Region中,但他若是有其余Region中、甚至整个堆任意对象有引用关系,作可达性断定对象存活时,要扫描整个对空间吗?

答:

  1. 虚拟机经过Remembered Set避免全堆扫描,每一个Region都有与之对应的Remembered Set。
  2. 当程序对引用类型进行写操做时,会产生一个Write Barrier暂停中断写操做,检查引用的对象是否处于Region之中。是,就经过CardTable将引用信息记录到所属Region的Remember Set中。
  3. 回收时,Remembered Set就能够保证不用进行全堆扫描了。
若是不计算维护Remembered Set的操做,G1收集器的运做大体可划分为一下步骤:
  • 初始标记:标记GC Roots能直接关联的对象,修改Next Top at Mark Start的值,让下一阶段程序运行时,在正确的Region中建立对象,停顿线程,耗时短。
  • 并发标记:从GC Root对堆对象进行可达性分析,找存活对象,可与用户线程并发执行,耗时长。
  • 最终标记:修正并发标记因用户程序继续运做致使标记变更的部分,JVM将这段变化记录在Remembered Set Logs中,最终标记阶段将Remembered Set Logs数据合并到Remembered Set中。
  • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户指望GC停顿时间制定回收计划。
G1的回收模式

G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不一样的条件下被触发。

  • Young GC:通常对象(除了巨型对象)都是在eden region中分配内存,当全部eden region被耗尽没法申请内存时,就会触发一次young gc,这种触发机制和以前的young gc差很少,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。
  • 回收整个young region,还会回收一部分的old region
  • 老年代被填满,就会触发一次full gc(尽可能避免)

image

内存分配与回收策略

自动内存管理最终能够归结为自动化地解决了两个问题:

  • 给对象分配内存
  • 回收分配给对象的内存

简单来讲,对象内存分配主要是在堆中分配。可是分配的规则并非固定的,取决于使用的收集器组合以及JVM内存相关参数的设定

对象优先在Eden分配

大多数状况下,对象在Eden区分配内存

Minor GC和Full GC的区别:

  • 新生代GC(Minor GC):指发生在新生代的垃圾回收动做,频繁,回收速度也快
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,常常会伴随至少一次Minor GC(并不是绝对,在Parallel Scavenege收集器的收集策略里就有进行Major GC的策略过程选择),它的速度通常比Minor GC慢十倍。

大对象直接进入老年代

大对象是指,须要连续内存空间的Java对象,例如很长的字符串或数组

长期存活的对象进入老年代

虚拟机给每一个对象定义了一个对象年龄(Age)计数器,若是对象在Eden出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳的话,会被移动到Survivor空间中,而且对象年龄为1.每在Survivor区中渡过一次Minor GC,年龄增长1,当它的年龄增长到必定程度(默认15),就被晋升到老年代。

动态对象年龄断定

若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代

空间分配担保

伪代码解释:

//准备Minor GC:
    if (老年代中最大连续可用空间>新生代全部对象总空间){
        //开始Minor GC
    } else {
        if (容许担保失败){
            if (老年代最大连续可用空间>历次晋升老年代对象平均大小){
                //开始Minor GC
            } else {
                //开始Full GC
            }
        } else {
            //开始Full GC
        }
    }
复制代码

通常来讲,新生代只使用一个survivor空间来进行轮换时的备份,因此当出现极端状况(即新生代空间在一次minor GC后所有存活)时survivor空间有可能爆满,因此此时须要老年代进行分配担保,即survivor区没法容纳的对象都进入老年代。

在JDK 6 Updale 24 以后,Handle PromotionFailure 不会再影响到虚拟机的空间分配担保策略,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,不然将进行Full GC。

总结

最后,内存回收和垃圾收集器不少时候都是影响系统性能,并发能力的缘由,虚拟机也提供了多种收集器和大量的调节参数,由于不少时候咱们要选择本身的业务来设置相应的收集方式

相关文章
相关标签/搜索