JVM垃圾回收

垃圾回收与内存分配策略

“垃圾”的定义

对象是否为“垃圾”

判断对象是否已成为“垃圾”的两种方法:引用计数法可达性分析算法java

  • 引用计数法

若是一个对象被引用一次,则加1,若是没人引用则被回收;存在问题:若是两个对象循环引用,可是没有任何外部对象引用他们俩,则那两个对象没法被回收。算法

  • 可达性分析算法(主流JVM采用)

没有被根对象(GC ROOT)直接或简介引用的对象则会被回收
根对象--确定不能对回收的对象
GC ROOT对象:system class、同步锁、线程类、本地方法类数据结构

何为“引用”--四种引用类型

JDK1.2之后将引用分为:强引用、软引用、弱引用和虚引用4种,强度依次减弱。多线程

  • 强引用
    被GC ROOT直接引用(等号赋值
  • 软引用
    被GC ROOT间接引用;当内存不足时被回收,内存充足时不会被回收
  • 弱引用
    没有GC ROOT直接引用,当发生垃圾回收时,无论内存是否充足都会被回收
  • 虚引用
    没有GC ROOT直接引用,虚引用使用时必须配合引用队列进行管理。

好比建立一个ByteBuffer实现类对象时,会建立个一个Cleaner对象,当ByteBuffer实现类对象没有再被引用时,ByteBuffer实现类对象会被回收,Cleaner对象则会进入引用队列,这时候一个referencehandles线程会查找引用队列中是否存在cleaner对象,若是有则调用Cleaner.clean方法,clean方法则根据记录的直接内存的地址,调用unsafe.freememory方法释放直接内存并发

  • 补充:引用队列

软引用、弱引用自己也要占用必定内存,当软引用、弱引用的引用对象都被回收时,则进入引用队列,会对引用队列进行后续管理;虚引用引用的对象被释放后,虚引用会进入引用队列异步

最后的挣扎--finalize()方法

即便可达性分析后,对象被断定为“垃圾”,也并不是非死不可。一个对象的死亡至少须要两次标记:函数

没有与GC Root的引用链,标记一次
对象没有重写finalize()方法,或finalize()重写但已被调用过一次,标记第二次布局

若是重写了finalize()方法,且尚未被调用,那么对象会被放置在F-Queue的队列中,会有一条虚拟机自建的、优先度较低的线程Finalizer线程去执行对象的finalize()方法,但为了防止finalize()方法出现死循环等异常,并不会保证等待finalize()方法执行结束。在此期间,若对象创建了引用链,则对象能够存活一次,不然就“死定了”。线程

不建议使用该finalize()方法设计

回收方法区

方法区的垃圾回收主要包含两部分:废弃的常量、再也不使用的类型

常量的回收相似与Java堆中的对象,当没有引用时,则容许回收
类型的回收相对比较苛刻,须要同时知足如下条件,才容许被回收

  • 该类全部实例都已被回收
  • 该类的类加载器已被回收
  • 该类对应的java.lang.Class对象没有被引用,且在任何地方都不能够经过反射访问该类方法

垃圾回收算法

从断定垃圾消亡的角度出发,垃圾回收算法能够划分为“引用计数式垃圾收集”、“追踪式垃圾收集”两类。在Java虚拟机中的讨论都在追踪式垃圾收集的范畴中。

回收的前置--分代理论

分代设计的理论创建在两个分代假说之上:

  1. 弱分代假说:新生对象都是朝生夕死
  2. 强分代假说:熬过越屡次垃圾回收的对象,就越难以消亡

    设计原则:

    垃圾收集器应该依据对象的年龄,把Java堆划分为不一样的区域。

    • 新生代
      朝生夕灭的对象集中在一个区域,每次回收只需关注少许须要存活的对象便可
    • 老年代
      难以消亡的对象集中在一个区域,可使用较低的频率去触发回收机制

    可是,在对新生代进行垃圾收集的时候,难免会出现新生代的中的对象被老年代引用的状况。因此,为了肯定新生代区域的存活对象,除了GC Root以外还须要遍历整个老年代中全部对象来得到准确的可达性分析。基于此,引入第三条经验法则:

  3. 跨代引用假说:跨代引用相对于同代引用来讲只占少数

    跨代引用通常倾向于两个对象同时生存或同时消亡的

    设计原则:

    在新生代创建全局数据结构(记忆集),把老年代分为若干小块,记录老年代中哪一块内存存在跨代引用
    此后,发生minor gc时只有包含了跨代引用的小块内存中的对象才会被加入到GC Root进行扫描

标记-清除算法(Mark Sweep)

先标记须要回收的对象,再统一清除

效率不稳定,随着对象数量增多,标记、清除两个过程的执行效率下降
内存碎片化,致使存入大对象时没法得到足够的连续内存空间,触发另外一次垃圾收集动做

标记-复制算法

将可用内存划分为两个彻底相等空间,每次只使用其中的一块。若是其中的一块内存用完,则将存活的对象彻底复制到另外一块,再对原来的空间进行统一清除回收。

  • 缺点
    内存空间的浪费
    若空间内大量对象都是存活的,复制的开销增大
  • 优势
    简单高效
    不用考虑内存空间碎片化
    PS.
    现商用Java虚拟机多在新生代中采用该方法
  • Appel式回收
    HotSpot虚拟机中的Serial、ParNew等新生代收集器均采起该策略。具体以下:
    把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配只使用Eden和一块Survivor,发生垃圾回收时,将存活的对象一次性复制给另外一块Survivor空间内,而后清理已用的空间。

    HotSpot虚拟机给Eden和Survivor默认大小比例为8:1,也就是说会有10%的空间会被浪费。当预留的10%的内存空间存不下存活的对象时,,就须要依赖其它内存空间(大多为老年代)进行内存分配。

标记-整理算法(Mark Compact)

区别与标记--清除算法,标记--整理算法,在标记后将存活的对象移向一端,而后将另外一端的空间总体回收,是一种移动式的算法。

  • 优势
    不存在碎片化内存,则无需依赖复杂的内存分配器
  • 缺点
    对象的移动操做须要触发“Stop The World”耗时较久

标记?清除:整理

标记-清除是一种非移动式算法、标记-整理是一种移动式算法,二者比较说明:

  • 吞吐量比较
    吞吐量定义:赋值器和收集器效率之和
    不移动会使得收集器效率增大,可是内存分配和访问会比垃圾回收频率高得多,因此总体吞吐量仍是下降的。

  • 举例说明
    HotSpot虚拟机中关注吞吐量的Parallel Scavenger收集器基于标记-整理算法;关注低延迟的CMS收集器基于标记-清除算法

  • 混合方案
    使虚拟机多数时间采用标记-清除算法,暂时容忍碎片的存在,等到碎片化程度开始影响对象的内存分配时,在采用标记-整理算法收集一次(CMS就采起该方式)

经典垃圾回收器

所谓“经典”垃圾回收器是指区别于实验室阶段的、已经过应用实践的垃圾回收器。

HotSpot垃圾回收器

Serial收集器

Serial:新生代:标记-复制算法
Serial Old:老年代:标记-整理算法
HotSpot虚拟机运行在客户端模式下的默认新生代收集器
简单高效、内存消耗最小

ParNew收集器

ParNew:新生代:标记-复制算法
Serial Old:老年代:标记-整理算法
激活CMS后,默认的新生代收集器
Serial的多线程版本,默认开启的线程数与CPU核心数相同

Parallel Scavenge搜集器

标记-复制算法,与ParNew类似
关注点在于达成可控制的吞吐量(吞吐量=用户代码运行时间/总时间;总时间=用户代码运行时间+垃圾回收时间)

参数说明

  • -XXMaxGCPauseMillis更关注停顿时间
    一个大于0的毫秒数,尽可能使回收时间不超过这个值
    实现原理:牺牲吞吐量和新生代空间获取,小内存新生代空间的回收速度必定因为高内存速度,可是回收频率也会增长

  • -XXGCTimeRatio更关注吞吐量
    0到100之间的整数,表明垃圾回收时间占总时间的比率,至关于吞吐量的倒数

  • -UserAdaptiveSizePolicy
    开关函数,激活后虚拟机会根据当前运行状况自动调整Eden与Survivor的内存比例、老年代内存大小等参数,已提供合适的停顿时间和最大吞吐量

Serial Old 收集器

serial 收集器的老年版本,标记-整理算法
在CMS收集器并发失败时的预备方案

Parallel Old 收集器

Parallel Scavenge 收集器的老年版本,标记-整理算法
在注重吞吐量或处理器资源稀缺时使用

CMS收集器

获取最短停顿时间的为目标,采用并发-清除算法

  • 工做步骤
    1. 初始标记
      标记GC Roots能直接关联的对象,速度很快

    2. 并发标记
      从GC Roots直接关联到的对象开始遍历整个对象图,耗时较长

    3. 从新标记
      修正并发标记期间,因用户继续运做致使标记产生变更的部分对象的标记记录

    4. 并发清除
      清除掉标记的已死亡的对象

整个过程当中,并发标记和并发清除耗时最久

  • 关键问题
    1. 并发过程当中会占用部分资源
      当处理器核心数大于4时,默认回收线程数不超过25%(处理器核心数+3)/4
      可是当处理器核心数小于4时,用户线程执行速度会大幅下降

    2. “浮动垃圾”与并发失败
      与用户程序运行并发运行就必然产生新的垃圾只有等下一次回收时才清理,这部分垃圾称为“浮动垃圾”,因此须要给用户线程预留足够空间。所以,CMS不能等老年代满了才进行收集,必须预留一部分做为并发时使用。若是CMS运行期间预留的内存没法知足程序分配新对象的需求,就会出现“并发失败”,这时候须要STW,临时启用Serial Old收集器对老年代的垃圾进行收集

    3. 内存碎片
      基于标记-清除算法必然产生内存碎片,致使大对象分配时出现内存不足进而触发Full GC。CMS提供-XX:UseCMSCompactAtFullCollection开关参数(默认开启),当不得不进行Full GC时进行内存碎片整合,即移动存活对象。会使得停顿时间延长

Garbage First 收集器

创建可预测的停顿时间模型,开创了面向局部收集的内存设计思路,基于Region的内存布局形式。默认停顿时间为200毫秒

  • 基于Region的内存布局
    把连续的Java堆内存划分为多个大小相等的独立空间,每一个空间均可以扮演Eden、Survivor空间或者老年代空间,其中Humongous区域转为收集大对象(大小超过了一个Region的对象,Region的大小可经过参数调整),G1大多会把Humongous当作老年代看待。收集器能够根据不一样的角色采起不一样的收集策略。

  • 局部收集思想
    Region做为每次回收的最小内存单位,每次收集到的空间都是Region的整倍数,G1会跟踪Region堆积的“价值”大小(回收所获空间/回收所需时间的经验值),再后台维护一个优先级列表,优先回收价值大的Region

  • 工做步骤
    1. 初始标记
      标记GC Roots能直接关联的对象,并修改TAMS指针的值,是借助Minor GC完成,因此不会形成额外的时间成本。
    2. 并发标记
      从GC Roots开始对堆中对象进行可达性分析,可并发执行,扫描完成时从新处理SATB记录的引用变更
    3. 最终标记
      处理并发标记时的发生变更的对象,STW,并发完成
    4. 筛选回收
      更新Region的统计数据,根据用户指望的停顿时间结合回收价值,肯定须要回收Region集合。把须要回收的Region中存活的对象复制到空Region中,再清理须要回收的所有Region区域。
  • 关键问题
    1. 跨Region引用的处理办法
      每一个Region都维护一张本身的记忆集,记录别的Region指向本身的指针,并标记这些指针在哪些卡页范围以内。其存储结构本质上是一种哈希表,key是别的Region的起始地址,value是一个集合,存储卡表的索引号。G1要耗费大越10%到20%的额外内存来维持收集器的工做。

    2. 并发干扰问题
      CMS在并发标记时采用增量更新的算法实现,而G1则经过原始快照(SATB)算法实现。此外,G1在回收过程当中建立新对象的内存分配上也作了改动,G1为每一个Region设计了两个名为TAMS(Top At Mark Start)的指针,并发标记中新分配的对象都要在这两个指针位置以上。G1收集器默认这部分对象是隐式标记过的,默认为存活

    3. 可靠地停顿预测
      -XX:MaxGCPauseMillis参数指用户指望的停顿时间,具体实现是以“衰减均值”为理论基础:在垃圾回收过程当中,会记录每一个Region的回收耗时、记忆集中里的脏卡数量等各个可测量的步骤所花费的成本。“衰减均值”更能体现“最近”一段时间的平均状态,更能在当下使回收不超过预期。(有点活在当下的感受)

  • G1与CMS
    • 优势:
      能够指定最大停顿时间、分Region的内存布局、按收益动态回收、不会产生内存碎片、回收完成后可提供规整的可用内存
    • 缺点:
      内存占用、程序执行的额外负载都较高
      G1的卡表更为复杂;运行负载方面,CMS使用写后屏障来更细维护卡表,而G1为了实现原始搜索(SATB)快照算法,还须要写前屏障来跟踪并发时的指针变化状况,G1能减小并发标记和从新标记的消耗,避免像CMS那样在最终标记阶段停顿时间过长。CMS直接同步处理,而G1异步处理
  • 总结
    小内存上使用CMS有优点,而大内存状态下使用G1有更多优点,而Java堆内存容量平衡点大约在6-8GB之间(经验数据)

低延迟垃圾收集器

Shenandoah 收集器

ZGC 收集器

相关文章
相关标签/搜索