垃圾回收算法
引用计数算法
在对象中添加一个引用计数器,每当有个地方引用他时,计数器值加1;当引用失效时,计数器值减1,;任何时刻计数器为零的对象就是不可能再被使用的。
但在Java虚拟机里都没有选用引用计数算法,主要原因是:很难解决对象之间相互循环引用的问题。
可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系从上至下搜索,搜索被根对象集合所连接的对象是否可达,搜索过程所走过的路径称为“引用链”。
如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可达的,意味着对象已死,标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
引用
强引用(Strongly Reference)
指在程序代码之中普遍存在的引用赋值,即“Object obj=new Object()”
无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用(Soft Reference)
当内存足够时,不会回收软引用的可达对象;当内存不够时,会回收软引用的可达对象。
弱引用(Weak Reference)
当垃圾收集器开始工作,无论当前内存是否足够,都会回收被弱引用关联的对象。
虚引用(Phantom Reference)
无法通过虚引用来取得一个对象实例
唯一目的是为了能在这个对象被收集器回收时收到一个系统通知
垃圾收集算法
标记清除算法(Mark-Sweep)
算法分为“标记”和“清除”两个阶段:首先标记出存活对象,在标记完成后,统一回收掉所有未被标记的对象。
缺点:1、执行效率不稳定;2、内存空间碎片化问题。
何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够就就存放(空闲列表分配)
标记—复制算法(Copying)
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
Appel式回收:
把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和已用过的那块Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例时8:1,也即每次新生代可用内存空间为整个新生代的90%,只有一个Survivor空间,即10%的新生代是会被浪费的。
标记—整理算法(Mark-Compact)
执行过程:从根节点开始标记所有被引用对象,将所有存活的对象压缩到内存的一 侧,按顺序排放,清理边界外所有的空间。
缺点:
1、移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址;
2、移动过程中,需要全程暂停用户应用程序,即:STW
STW:
Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应。
垃圾回收器
前言
安全点与安全区域
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”(Safepoint)
SafePoint如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
主动式中断:
设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,这个区域中断任何位置开始GC都是安全的。
GC的性能指标:
吞吐量:运行用户代码的时间占总运行时间的比例
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
内存占用:Java堆区所占的内存大小
这三者共同构成了一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。这三项里,暂停时间的重要性日益凸显。
现在标准:在最大吞吐量优先的的情况下,降低停顿时间
垃圾收集器的发展史
七款经典的垃圾收集器:
串行:Serial、Serial Old
并行:ParNew、Parallel Scavenge、Parallel Old
并发:CMS、G1
垃圾收集器组合关系:
记忆集与写屏障
为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。
字节数组CARD_TABLE的每一个元素都对应着起标识的内存区域中一块特定大小的内存块,这个内存块被称作卡页。(在HotSop中使用的卡页是2的9次幂,即512字节)
只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,把这个元素变脏(Dirty),没有则标识为0,。在垃圾收集时只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
当进行GC时,在GC根节点的枚举范围加入Remembered Set,就可以保证不进行全局扫描,也不会有遗漏。
Serial(串行)
最基本、历史最悠久的垃圾收集器,JDK1.3之前回收新生代唯一选择。
采用复制算法、串行回收和“Stop-the-World”机制的方式执行内存回收。
HotSpot虚拟机运行在客户端模式的默认新生代收集器。
优势:
简单而高效,对于单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
运行在Client模式下的虚拟机是个不错的选择
Serial Old
Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记—整理算法”。
运行在Client模式下默认的老年代的垃圾回收器
用途:
-
- 与新生代的Parallel Scavenge配合使用
- 作为老年代CMS收集器的后备垃圾收集方案
ParNew(并行)
是Serial收集器的多线程版本,采用并行回收方式,复制算法,“Stop-the-World”机制。
ParNew是很多JVM在server模式下新生代的默认垃圾收集器。
在单个CPU的环境下,ParNew收集器不比Serial收集器更高效。
目前ParNew成为了CMS专门处理新生代的组成部分,是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。
Parallel Scavenge(吞吐量优先)
新生代收集器,基于并行回收,复制算法,“Stop-the-World”机制。
Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,尽可能缩短垃圾收集时用户线程的停顿时间,也被称为吞吐量优先的垃圾收集器。
自适应调节策略:虚拟机根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
parallel Old
是Parallel Scavenge收集器老年代版本,支持多线程并行收集,基于标记—整理算法。
CMS(低延迟)
在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾回收器:CMS。这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作。
目前很大一部分的Java应用集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,CMS收集器就非常符合这类应用的需求。
JDK9中CMS被标记为Deprecate,在JDK9中开启CMS的话,用户会收到一个警告信息,提示CMS未来将会被废弃。
JDK14中移除了CMS垃圾回收器,如果在JDK14中使用CMS的话,JVM不会报错只是给出一个warning信息,但是不会exit,JVM会自动回退以默认GC方式启动。
运作过程:
初始标记
- 初始标记、重新标记这两个步骤仍然需要“Stop-the-World”,初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快。
并发标记
- 从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但不需要停顿用户线程,可以和垃圾收集线程一起并发运行。
重新标记
- 为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
并发清除
- 清除删除掉标记阶段判断已经死亡的对象。
缺点:
对处理器资源非常敏感
- 在并发阶段,虽然不会导致用户线程停顿,但会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就很有可能导致用户程序的执行速度忽然大幅度降低。
无法处理浮动垃圾
- 在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉他们,只好留待下一次垃圾收集时在清理掉,这一部分被称为“浮动垃圾”。
- 因此在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS在工作工程中依然有足够的空间支持应用程序运行。
空间碎片过多
- CMS是一款基于“标记—清除”+“标记—整理”算法实现的收集器,意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
- 为解决这个问题CMS不得不在执行过若干次不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理。
Garbage First(并行)
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
Mxied GC:
- 面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是那块内存中存放的垃圾数量最多,回收的收益最大。
基于Region堆内存布局:
- 把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region可以根据需要扮演不同的角色。
- G1认为只要大小超过了一个Region容量一般的对象即可判定为大对象(1.5倍)。对于这些大对象将会被存放在N个连续的Humongous Region之中,G1大多数行为都把Humongous Region作为老年代的一部分来进行看待
优势:
- 并行与并发
- 分代收集
- 从分代上看,G1依然属于分代型垃圾回收器。但从堆的结构上看,它不要求整个Eden区、新生代或者老年代是连续的,也不再坚持固定大小和固定数量。
- 将堆空分为若区域(Region),这些区域中包含了逻辑上的年轻代和老年代。
- 兼顾年轻代和老年代
- 空间整合
- 内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上可看作是标记—压缩算法,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿时间模型
- 在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息
缺点:
- 在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上发挥其优势。
回收过程:
年轻代GC(Young GC)
- 当年轻代的Eden区用尽时开始年轻代回收过程:G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收,然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
- 年轻代垃圾回收只回收Eden区和Survivor区。
老年代并发标记过程(Concurrent Marking)
- 当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程
混合回收(Mixed GC)
- 标记完成马上开始混合回收过程,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了,同时这个老年代Region是和年轻代一起被回收的。
ZGC
是一款在JDK11中新加入的低延迟垃圾收集器。也采用基于Region的堆内存布局,但不同的是ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。
染色指针技术:
染色指针是一种直接将少量额外的信息存储在指针上的技术,尽管Linux下64位指针的高18位不能用来寻址,鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即是否被移动过),也直接导致了ZGC能够管理的内存不可以超过4TB。
优势:
- 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该region的引用都被修正后才能清理,理论上只要还有一个空闲Region,ZGC就能完成收集。
- 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,目前为止,ZGC都并未使用任何写屏障只使用了读屏障。
- 染色指针技术可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据。
ZGC运作阶段:
并发标记
- 并发标记是遍历对象图做可达性分析的阶段,与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的。
并发预备重分配
- 需要根据特定的查询条件得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Region)
- ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
并发重分配
- 这个过程要重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
- 指针的自愈能力:如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象。
并发重映射
- 修正整个堆中指向重分配集中旧对象的所有引用ZGC很巧妙的把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段中完成。