垃圾收集
对象已死?
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
- 优点:原理简单,效率高
- 缺点:不能解决对象之间循环引用的问题
可达性分析算法
从GC Roots根对象作为起始节点集开始,根据引用关系向下搜索。
GC ROOT 包含
- 虚拟机栈中局部变量(也叫局部变量表)中引用的对象
- 方法区中类的静态变量、常量引用的对象
- 本地方法栈中 JNI (Native方法)引用的对象
- 虚拟机内部的引用
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
垃圾收集算法
分代收集理论
一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择**“标记-清除”或“标记-整理”算法**进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
标记-复制算法(针对新生代)
将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
- 优点:每次都是对其中的一块进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
- 缺点:将内存缩小为原来的一半,代价太高了一点。
标记-清除算法
算法分为“标记”和“清除”阶段:
标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);
也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。
-
缺点:
-
效率问题 (如果需要标记的对象太多,效率不高)
-
空间问题(标记清除后会产生大量不连续的碎片)
-
标记-整理算法(针对老年代)
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
Hotspot的算法细节实现
安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint)
SafePoint的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。
如何在GC发生时,让所有线程都跑到最近的安全点停顿下来呢?
-
抢先式中断:(目前没有虚拟机采用了)
首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。 -
主动式中断:
设置一个中断标志,各个线程运行到SafePoint的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法走到安全点去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域(Safe Region)来解决。
当线程进入安全区域时,会标识已进入安全区域;当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
记忆集与卡表
记忆集(Remembered Set)是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。收集器记录特别细致的对象引用会造成很大的性能损失,只需要通过判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,避免把整个老年代加入GC扫描范围。
卡表(Card Page)是记忆集的一种实现方式,最简单的方式可以只是一个字节数组。字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作卡页(Card Page)。
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。
写屏障
虚拟机中通常利用写屏障来维护卡表状态和记录对象引用的变动情况,这里的写屏障和解决并发乱序执行的内存屏障是不同含义的,此处的写屏障是对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑,相当于为引用赋值挂上的一小段钩子代码。
三色标记
我们引入三色标记作为工具,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
对于可达性分析的扫描过程,如果用户线程是冻结的,只有收集器线程在工作,那不会有任何问题。但是收集器在标记颜色的同时,用户线程修改了引用关系,可能出现两种后果,一种是把原本消亡的对象错误标记为存活,可以容忍,只不过产生了一点逃过本次收集的浮动垃圾,下次收集清理掉就好;另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误。
当且仅当以下两个条件同时满足时,会产生**“对象消失”**的问题
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
当我们要解决这种问题时,只需破坏这两个条件中的任意一个就可以
- 增量更新要破坏的是第一个条件(CMS)。当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来。黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
- 原始快照要破坏的是第二个条件(G1)。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来。即将删除的白色对色染成灰色。
full gc出现的情况
-
调用System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
-
未指定老年代和新生代大小,堆伸缩时会产生fullgc,所以一定要配置-Xmx、-Xms
-
老年代空间不足。老年代空间不足的常见场景比如大对象、大数组直接进入老年代、长期存活的对象进入老年代等。
-
空间分配担保失败。每次晋升的对象的平均大小 > 老年代剩余空间;Minor GC后存活的对象超过了老年代剩余空间
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器
Serial/Serial Old收集器
单线程收集器。进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:简单高效
ParNew收集器
parNew收集器是Serial收集器的多线程并行版本,新生代收集器(标记-复制),可以和CMS收集器配合使用。
目标:达到一个可控制的吞吐量(吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值)
优点:可以高效利用处理器资源。
默认开启的收集线程数与处理器核心数量相同。
Parallel Scavenge/Parallel Old收集器
Parallel Scavenge收集器是新生代收集器(标记-复制)。
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集。(标记-整理)
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器**(标记-清除)**。
CMS的回收线程数是(处理器核数+3)/4
,当处理器不足4个时,CMS对应用程序的影响可能很大,会抢占cpu。
整个过程分四个步骤:
- 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
- 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。
- 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
- **并发重置:**重置本次GC过程中的标记数据。
-
优点:并发收集、低停顿
-
缺点:
-
对CPU资源敏感(会和服务抢资源)
-
标记-清除算法会导致收集结束时会有大量空间碎片产生。
-
无法处理浮动垃圾 (在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了)
-
垃圾收集阶段用户线程还需要持续运行,需要预留足够内存空间提供给用户线程使用。
如果GC期间预留的内存无法满足程序的需要,就会出现一次并发失败(Concurrent Mode Failure),此时虚拟机将启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集
-
G1收集器
目标:建立起停顿时间模型,支持在M毫秒的时间片段内,在垃圾收集上的时间大概率不超过N毫秒。
G1是一款面向服务器的垃圾收集器,适用于多核及大容量内存的机器,以极高概率满足GC停顿时间要求,具备高吞吐量特征。
基于Region的堆内存布局
把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间、以及大对象区。
JVM最多可以有2048个Region。 一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以 用参数"-XX:G1HeapRegionSize"手动指定Region大小
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize
设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待。
收集思路:
-
将 Region 作为单次回收的最小单元(每次收集到的内存空间都是 Region 大小的整数倍),避免全区域垃圾收集
-
G1收集器跟踪各个Region里面的垃圾堆积的
价值
大小,(价值即回收所获得的空间大小以及回收所需时间的经验值),然后在后台维护一个优先级列表
-
根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region
垃圾收集流程:
- 初始标记:仅仅标记一下GC Roots能直接关联到的对象
- 并发标记:从GC Root开始对堆中对象进行
可达性分析
,递归扫描整个堆
里的对象,找出要回收的对象。(耗时较长,可与用户程序并发执行) - 最终标记:短暂地STW,处理并发阶段结束后遗留下来的少量SATB记录
- 筛选回收:
- 更新Region的统计数据,对各个Region的回收价值和成本进行
排序
- 根据用户所期望的停顿时间来
制定回收计划
,可以自由选择任意多个Region 构成回收集 - 把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。(涉及存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成)
- 更新Region的统计数据,对各个Region的回收价值和成本进行
G1的三种GC方式
-
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,而且G1会计算下现在Eden区回收大 概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills设定的值,那么就会触发Young GC
-
MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中 存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次
-
Full GC
Full GC 停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下 一次MixedGC使用,这个过程是非常耗时的。
优点
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
- 空间整合:G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:降低停顿时间是G1 和 CMS 共同 的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指 定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。
缺点
- 内存占用高
G1收集器优化
假设参数 -XX:MaxGCPauseMills
设置的值很大,年轻代存活下来的对象过多,会导致Survivor区域放不下那么多对象,或者触发了动态年龄判定规则
,会快速导致一些对象进入老年代中。需要调节 -XX:MaxGCPauseMills
这个参数,保证他的年轻代gc别太频繁的同时,还要考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
ZGC收集器
目标:在对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在10ms以内的低延迟。
基于Region的堆内存布局
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置256KB~4MB的对象。
- 大型Region(Large Region):容量不固定,可以动态变化,但必须2MB的整数倍,每个大型Region只会存一个对象。
染色指针技术
收集器有几种染色标记实现方案,有的把标记记录在对象头上,有的记录在内存的一块区域(如BitMap),ZGC则直接把标记记录在引用对象的指针上。使用指针的高4位来存放标记信息。
一旦某个Region的存活对象被移走后,这个Region立即就能够被释放和重用,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
染色指针可以大幅减少内存屏障的使用。设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,使用染色指针可以省去很多操作。
通过虚拟内存映射来重新定义指针的前几位。
Comments | 0 条评论