JVM-2

主要是用来介绍各种垃圾回收器的执行过程和优缺点

垃圾回收器的种类

Serial收集器

最早的垃圾回收器,是个单线程工作的收集器,但是他的单线程并不是真的垃圾会受到时候真的使用的是单线程,而是说进行垃圾回收的时候,会stop the world ,会停止所有的用户线程工作

特点:

  1. 垃圾回收的时候,会停止所有的用户线程进行工作
  2. 在资源受限的情况下,与其他收集器的单线程相比,效率依然是最高的

ParNew收集器

其实这就是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为都和Serial收集器一样

特点:

  1. 除了Serial收集器外,目前只有它能与CMS收集器配合工作
  2. 一般来说是CMS和ParNew来一起使用,CMS 主要用来回收老年代,ParNew主要用来回收新生代

Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器

特点:

  1. 它主要关注的点是达到一个可控制的吞吐量
  2. 自适应调节策略:自动进行内存的调优

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法

两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[插图],另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现

  • 在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,是基于标记-清楚算法实现的

过程分为四个步骤:

  1. 初始化标记:就是标记GC ROOTs,静态变量和方法中的成员变量都数据GCROOTS,速度很快
  2. 并发标记 :链路追踪,标记GGROOT 中引用其他的
  3. 重新标记:标记并发标记引用变动的对象
  4. 并发清理:并发清理掉可回收的内存,但是用户线程依旧在执行,所以会产生浮动垃圾

缺点:

  1. 对处理器资源比较敏感
  2. 无法处理“浮动垃圾”
  3. 由于使用的是标记-清理算法,故会产生大量的碎片空间

浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”

实际处理

-XX:CMSlnitiatingOccupancyFaction 用来设置老年代占用多少比例的时候触发CMS垃圾回收

jsk1.6默认的值是92%

如果Cms垃圾回收期间,系统程序要放入老年代的对象大于可用内存空间,会发生Concurrent Mode Failure ,就是说并发垃圾回收失败了,我一遍回收,你一遍把对象放入老年带中,内存不够了

此时也会自动启用“Seral Old”垃圾回收器,就是直接把系统程序”Stop the World” ,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生,然后一次性把垃圾对象都回收掉,完事儿了再回复系统线程

注意、

cms垃圾回收期也不是仅仅用“标记-清理”算法的,由于太多的内存随便实际上会更加频繁的full gc

cms 有一个-XX:+UseCMSCompactAtFullCollection 默认开启

这个参数的意思是full gc之后要再次进行”Stop the World”,停止工作线程,然后进行碎片整理,就是把存活的对象挪到一起,空出来大片连续的内存空间,避免内存碎片

-XX:CMSFullGCsBeforeCompaction 这个参数的意思是执行多少期Full gc之后再执行一次内存碎片的整理工作,默认是0,意思就是每次full gc之后都会进行一次内存整理

Garbage First收集器

简称G1收集器,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式

在JDK9中,,G1宣告取代Parallel Scavenge加ParallelOld组合,成为服务端模式下的默认垃圾收集器

垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的MixedGC模式

G1会将连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

垃圾回收思路:

让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,根据优先级列表来进行垃圾回收

Shenandoah收集器

低延迟收集器

Shenandoah相比起G1又有什么改进呢?

  1. 支持并发的整理算法,而不是只会支持多线程回收并行
  2. 默认不使用分代收集的,没有实现分代,主要通过“性价比”来衡量垃圾回收的优先级
  3. 记录跨Region的引用关系发生改变,不使用耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”

工作过程:

ZGC收集器

垃圾回收的类型

部分收集(Partial GC)

指目标不是完整收集整个Java堆的垃圾收集

新生代收集

(Minor GC/Young GC):指目标只是新生代的垃圾收集

老年代收集

(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集

混合收集

(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为

整堆收集

(Full GC):收集整个Java堆和方法区的垃圾收集。

垃圾回收算法

标记清除算法

顾名思义标记所有需要回收的对象,统一回收被标记的对象

两个过程:标记过程和清理过程

标记过程其实就是判断对象是否需要回收的过程,也就是对象是否已死的过程

清理过程就是清理掉所有的已死对象

关注延迟的CMS收集器则是基于标记-清除算法的

缺点:

  1. 执行效率低

如果对象需要回收的数量比较大,那么它的执行效率必然会随着数量的增加而减少

  1. 内部空间碎片化

由于被回收的对象位置不能保证是连续的,必然会产生大量的不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

标记复制算法

新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)

标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

标记-整理算法和标记复制算法区别

Stop TheWorld

定义:全程暂停用户应用程序才能进行的过程

如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行[插图],这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机设计者形象地描述为“Stop TheWorld”

  • 关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的

方法区的回收

回收类型两种:废弃的常量和不再使用的类

废弃的常量

假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池

不再使用的类

如何确定一个类是一个不再使用的类那,需要满足一下三个条件

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾回收(堆)的具体过程

开始

开始的时候,一定是stop the word的状态,就是所有的用户线程都会停止的,但是具体什么时候进行垃圾回收那,就是到达安全点的时候

安全点的选取标准是是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点

到达安全点会停顿下来,那么停顿方式是什么那?

  1. 抢占式中断
  2. 主动式中断

抢占式中断:抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件

主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象

安全区域:安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。

程序执行的时候可以使用到达安全点的方法来进行垃圾回收,程序不执行的时候,就需要采用安全区域的方来进行垃圾回收了

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止

根节点枚举

如果采用的是可达性分析性算法将会从GC Roots集合中查找引用链

GC Roots集合:通常是常量、类静态属性、栈帧中的本地变量表

由于目前java应用越做越大,每次都要从GC Roots集合中找会很慢,于是HotSpot给出了一种解决方案,使用OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译(见第11章)过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找

但是并不是每一次的操作都会存到OopMap的数据结构中,这样做的话所需要的空间太大了,违背最初节省空空间的想法,因此通常是到了安全点之后,才会进行

数据结构

OopMap

记忆集

作用:记录从非收集区域指向收集区域的指针集合的抽象数据结构

目前经常采用的实现记忆集的方式是卡表

卡表和记忆集的关系可以用hashmap和map之间的关系来对比,一个是具体实现方式,一个是抽象数据结构

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

并发情况下是如何保证卡表变脏的,是采用写屏障的方式

垃圾回收器的优化

把遍历对象图中遇到的对象按照“是否访问过”这个条件标记成以下三种颜色

  1. 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达
  2. 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象
  3. 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

“对象消失”的问题需要同时满足以下两种条件

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  2. ·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)

增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了

原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索

增量更新和原始快照这两种解决方案都有实际应用,譬如,CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。