垃圾回收算法与垃圾回收器
垃圾回收算法
1、可达性分析
从GC Roots
的对象作为起始点,从这些节点出发所走过的路径称为引用链。
当一个对象到 GC Roots
没有任何引用链相连的时候说明对象不可用,很显然 object5 相关的引用已经没用了。
从根节点出发看能不能到某个节点,如果不能达到的话说明对象不可用,看图可知 object5、object6、object7 都不可用。
2、引用计数算法
如果不小心直接把 Obj1-reference
和 Obj2-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 优化实践为例
指定场景下的对比