垃圾回收算法

1、可达性分析

GC Roots的对象作为起始点,从这些节点出发所走过的路径称为引用链。

当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用,很显然 object5 相关的引用已经没用了。

从根节点出发看能不能到某个节点,如果不能达到的话说明对象不可用,看图可知 object5、object6、object7 都不可用。

2、引用计数算法

如果不小心直接把 Obj1-referenceObj2-reference 设置为 null。

则在 Java 堆当中的两块内存依然保持着互相引用无法回收。无法解决循环引用的情况

3、标记清除算法

原理

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被 称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除

标记

从引用根节点开始标记所有被引用的对象。标记的过程其实就是遍历所有的GC Roots, 然后将所有GC Roots可达的对象标记为存活的对象

清除

遍历整个堆,把未标记的对象清除

缺点

此算法需要暂停整个应用,会产生内存碎片(内存空间不连贯了,因为内存中未标记的对象被清除了)

4、标记压缩算法

老年代进行垃圾回收采用的是标记压缩算法 。

5、复制算法

新生代进行垃圾回收采用的是复制算法 。

原理

对象产生的时候在 eden 区创建对象,当 eden 空间用完时,程序又需要创建对象,这个时候触发 JVM 垃圾回收,不再被其他对象所引用的对象就会被销毁,然后将存活对象移动到 from 区。

此时 eden 为空,from 区有数据,当 eden 空间再次用完时,再次触发垃圾回收的时候,这时 eden+from 作为主战场,存活对象移动到 to 区(这个时候 to 变为 from) ,原先 form 变为 to,谁空谁为 to,有个交换的过程。

从 from 到 to 的过程每次复制一次对象年龄增长一岁,当年龄达到一定年龄(默认 15 岁),若养老区也满了,那么这个时候将产生 MajorGC(FullGC),进行养老区的内存清理。若养老区执行了 Full GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常”OutOfMemoryError”

交换

经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次 GC 前的“From”,新的“From”就是上次 GC 前的“To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的,谁空谁为 to

因为 Eden 区对象一般存活率较低,一般的,使用两块 10%的内存作为空闲和活动区间,而另外 80%的内存,则是用来给新建对象分配内存的。一旦发生 GC,将 10%的 from 活动区间与另外 80%中存活的 eden 对象转移到 10%的 to 空闲区间,接下来,将之前 90%的内存全部释放,以此类推。

缺点

1、它浪费了一半的内存

2、如果对象的存活率很高,我们可以极端一点,假设是 100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服 50%内存的浪费。

垃圾回收器

垃圾回收收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

1、并行和并发的区别

并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。

并行和并发的区别:

  • 并发,指的是多个事情,在同一时间段内同时发生了。

  • 并行,指的是多个事情,在同一时间点上同时发生了。

  • 并发的多个任务之间是互相抢占资源的。

  • 并行的多个任务之间是不互相抢占资源的、

只有在多 CPU 或者一个 CPU 多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的

2、Serial 收集器(过时)

Serial 是单线程执行垃圾回收的。当需要执行垃圾回收时,程序会暂停一切手上的工作,然后单线程执行垃圾回收。

Serial New

因为新生代的特点是对象存活率低,所以收集算法用的是复制算法,把新生代存活对象复制到老年代,复制的内容不多,性能较好。

Serial Old

Serial Old 收集器老年代的收集器,与 Serial 一样是单线程,不同的是算法用的是标记压缩算法

因为老年代里面对象的存活率高,如果依旧是用复制算法,需要复制的内容较多,性能较差。并且在极端情况下,当存活为 100%时,没有办法用复制算法。所以需要用标记压缩 Mark-Compact 算法,以有效地避免这些问题

3、Parallel 收集器(过时)

ParNew

ParNew 同样用于新生代,是 Serial 的多线程版本,并且在参数、算法(同样是复制算法)上也完全和 Serial 相同。

Par 是 Parallel 的缩写,但它的并行仅仅指的是收集多线程并行,并不是收集和原程序可以并行进行。

ParNew 也是需要暂停程序一切的工作,然后多线程执行垃圾回收。

因为是多线程执行,所以在多 CPU 下,ParNew 效果通常会比 Serial 好。但如果是单 CPU 则会因为线程的切换,性能反而更差

Parallel Scavenge

新生代的收集器,同样用的是复制算法,也是并行多线程收集。与 ParNew 最大的不同,它关注的是垃圾回收的吞吐量。

这里的吞吐量指的是 总时间与垃圾回收时间的比例。这个比例越高,证明垃圾回收占整个程序运行的比例越小。

Parallel Old

老年代的收集器,是 Parallel Scavenge 老年代的版本。其中的算法替换成标记压缩算法(Mark-Compact)

4、CMS 收集器

CMS:Concurrent Mark Sweep同样是老年代的收集器。它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁 GC 的场景下,是比较适用的。

CMS 命名中用的是 concurrent,而不是 parallel,说明这个收集器是有与工作执行并发的能力的。MS 则说明算法用的是Mark Sweep

来看看具体地工作原理,CMS 整个过程比之前的收集器要复杂,整个过程分为四步

  • 初始标记(initial mark)

单线程执行,需要“Stop The World”,但仅仅把 GC Roots 的直接关联可达的对象给标记一下,由于直接关联对象比较小,所以这里的速度非常快。

  • 并发标记(concurrent mark)

对于初始标记过程所标记的初始标记对象,进行并发追踪标记,此时其他线程仍可以继续工作。此处时间较长,但不停顿。

  • 重新标记(remark)

在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”,时间比初始时间要长一点。

  • 并发清除(concurrent sweep)

并发清除之前所标记的垃圾。其他用户线程仍可以工作,不需要停顿。

由于最耗费时间的并发标记并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

缺点

Mark Sweep 算法会导致内存碎片比较多(因为用的标记清除,没有用标记压缩)

CMS 的并发能力依赖于 CPU 资源,所以在 CPU 数少和 CPU 资源紧张的情况下,性能较差。

并且并发清除阶段,用户线程依然在运行,所以依然会产生新的垃圾,此阶段的垃圾并不会再本次 GC 中回收,而放到下次。所以 GC 不能等待内存耗尽的时候才进行 GC,这样的话会导致并发清除的时候,用户线程可以利用的空间不足。所以这里会浪费一些内存空间给用户线程预留。

有人会觉得既然 Mark Sweep 会造成内存碎片,那么为什么不把算法换成 Mark Compact 呢?

答案其实很简答,因为当并发清除的时候,用 Compact 整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响。Mark Compact 更适合“Stop the World”这种场景下使用

5、G1 收集器

G1,GarbageFirst,在 JDK1.7 版本正式启用,是当时最前沿的垃圾收集器。G1 可以说是 CMS 的终极改进版,解决了 CMS 内存碎片、更多的内存空间登问题。虽然流程与 CMS 比较相似,但底层的原理已是完全不同。高效益优先。

G1 会预测垃圾回收的停顿时间,原理是计算老年代对象的效益率,优先回收最大效益的对象。

堆内存结构的不同。

以前的收集器分代是划分新生代、老年代、持久代等。

6、ZGC

在 JDK11 当中,加入了实验性质的 ZGC。它的回收耗时平均不到 2 毫秒。它是一款低停顿 高并发 的收集器。ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么其他阶段是怎么做到可以并发执行的呢?ZGC 主要新增了两项技术,一个是着色指针 Colored Pointer ,另一个是 读屏障 Load Barrier 。

着色指针 Colored Pointer

ZGC 利用指针的 64 位中的几位表示 Finalizable、Remapped、Marked1、Marked0(ZGC 仅支持 64 位平台),以标记该指向内存的存储状态。相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于 Java 术语当中的引用。在这个被指向的内存发生变化的时候(内存在 Compact 被移动时),颜色就会发生变。

在 G1 的时候就说到过,Compact 阶段是需要 STW,否则会影响用户线程执行。那么怎么解决这个问题呢?

读屏障 Load Barrier

读屏障 Load Barrier 由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了。那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。

把这两项技术联合下理解,与标记对象的传统算法相比,ZGC 在指针上做标记,在访问指针时加入 Load Barrier(读屏障),比如当对象正被 GC 移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与 GC 一致而粗暴整体的 Stop The World。

ZGC 虽然目前还在 JDK 11 还在实验阶段,但由于算法与思想是一个非常大的提升,相信在未来不久会成为主流的 GC 收集器使用

7、AliGC(了解原理)

AliGC 是阿里巴巴 JVM 团队基于 G1 算法, 面向大堆(LargeHeap)应用场景

如何降低 90%Java 垃圾回收时间?以阿里 HBase 的 GC 优化实践为例

指定场景下的对比