3. JVM GC算法及具体垃圾收集器
3. JVM GC算法及具体垃圾收集器 3.1 基础目标
GC和内存分配的作用
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
什么内存需要回收?
3.2 对象已死?
从上面的什么内存许要回收中我们知道许要回收的对象大部分存在堆/方法区中,此时还要解决一个找到可被回收对象的问题,否则一个对象正在使用就被内存回收了 程序允许会走向不可预知的错误。
判断对象已死的方法
可达性分析 3.2.1再谈引用
Java引用分类
软引用(Soft )
弱引用(Weak )
虚引用( )
3.2.2回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
常量只要没有被引用就能被回收
类对象则需要
没有实例化的对象没有类加载器没有被反射 3.3垃圾收集算法 3.3.1分代收集理论
当前大多数垃圾收集器都遵循分代收集( ),垃圾收集建立在分代收集理论之上,分代收集理论指导了如何高效回收垃圾
分代收集建立在一些假说之上
弱分代假说(Weak )
绝大多数对象都是朝生夕灭的
强分代假说( )
熬过越多次垃圾收集过程的对象就越难消亡
跨代收集假说( )
跨代对象相对来说是少数,并且最后会到同一分代中消除跨代(例如新生代被老年代依赖,新生代对象就会熬过一次次GC最终进入老年代消除跨代)
分代收集假说推出了对象要分代存放,并且根据不同区域使用不同的收集算法
JVM中一般将Java堆划分为两部分
新生代(young )老年代(old )
分代收集理论引出一个问题:对象不是孤立的,会存在跨代引用
减少新生代对象跨代引用扫描整个老年代的解决方案记忆集( Set)
将老年代划分为若干个小块标识出老年代的哪一块内存会存在跨代引用,发生Minor GC只扫描这一块
GC定义
整堆收集(Full GC)
以下GC图片来源
3.3.2 标记-清除算法
问题 3.3.3标记-复制算法
带整理的GC算法
解决标记清除算法随着要回收对象增加导致效率降低的问题,通过内存块复制到另一区域(并整理)来保证清理的效率以及内存的连续
// 新生代收集器都使用这种算法
问题
内存浪费
解决
通过8:1:1将新生代划分为Eden+两个区域来避免内存浪费,因为新生代的特性每次回收大量内存,所以存货的对象占用内存不会很多,若超过限制则直接进入老年代
3.3.4标记-整理算法
标记-复制与标记-清除算法一致,但最后回收是将所有对象移动到内存一端,然后清理掉边界以外的所有对象。
老年代算法,但移动对象与更新引用需要STW,避免运行对对象移动与清理造成影响
注重吞吐量,CMS注重延迟
CMS是老年代垃圾收集器,采用标记-清理算法,在内存碎片太多后采用标记-整理算法
3.4 的算法细节 3.4.1 根节点枚举 3.4.2 安全点
使用会随着程序运行导致map变更,每条指令都要维护map成本太高
安全点就是更新的时间点,这个时间点不能太多也不能太少,太多影响性能太少影响准确性。
安全点选在程序长时间执行之前。
程序中断分为两种
抢先式
对程序运行不友好
主动式
设定标识位让线程沦陷然后主动中断
3.3.3 安全区域
在一段时间内对象关系不变的点,可理解为安全点的集合
3.4.4记忆集和卡表
卡表是记忆集的具体实现之一
为了解决跨代引用问题在新生代中维护记忆集来避免对整个老年代扫描。
记忆集的精度
3.4.5写屏障
解决怎么去维护记忆集的问题,通过写屏障(Write )。
写屏障可理解你额为AOP,虚拟机对引用类型复制就是整个切面,写屏障在这些对象赋值指令前后产生一个唤醒通知,供程序做额外动作。
3.4.6 并发的可达性分析
GC Roots通过等优化时间段所以SWT 而并发标记阶段则占用大量时间不能STW 所以需要和用户程序一起执行,通过三色标记(Tri-color )来保障一致性。
三色标记解决的问题:由于和用户线程并发执行导致对象引用变更,若改回收的对象标记为黑色那还好,等待下一次回收即可
但不该回收对象被标记为白色则会导致程序出错
两个将黑色对象错误的标记为白色的必要条件
赋值器插入了一条或多条黑色到白色对象的引用赋值器删除了全部灰色对象到该白色对象的直接或间接引用
为了破坏这两个条件产生了两种方案
增量更新( )
黑色对象一旦新插入了对白色对象的引用变回灰色对象需要从他开始重新扫描,类似MVCC 与乐观锁,加入对象变更导致重要数据变化需要重新扫描
原始快照( at the ,STAB)
下引用删除时记录这些对象,将其置为灰色重新扫描
两种都类似乐观锁思路是在对象发生某一点的变更时记录并重新扫描
CMS 增量更新
G1 原始快照
3.5经典垃圾收集器
垃圾收集器是垃圾收集算法的实现
3.5. 会产生长时间STW简单,不会产生并发问题
在微服务场景下为每个服务分配的内存不会很多在不频繁收集的情况下可能也不错
3.5.2 收集器
的多线程版本
能与CMS收集器配合
-XX:+ 激活+CMS
-XX:+/- 指定/禁用
JDK9取消+ Old / +CMS
XX: 限制收集线程数
·并行():并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。·并发():并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
并行指多个GC线程互相合作,在多线程场景下也可能是并发
这里的并发语义指GC线程与用户线程的运行情况,是交替运行还是暂停用户线程只允许GC线程
3.5.3
与CMS对比
如果读者对于收集器运作不太了解,手工优化存在困难的话,使用 收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:参数(更关注最大停顿时间)或-XX:(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是 收集器区别于收集器的一个重要特性。
PS+PO
实际上不管如何,/ 都是需要STW的,区别在于单线程还是多线程收集,或者说关注收集细节,但实质上还是STW
CMS关注停顿,而PS关注吞吐量(用户代码运行时间/(用户代码运行+GC时间))
用 户 代 码 运 行 时 间 用 户 代 码 运 行 时 间 + G C 时 间 \frac{用户代码运行时间}{用户代码运行时间+GC时间} 用户代码运行时间+GC时间用户代码运行时间
可自适应调节。
若GC运行的时间少,并不会加快,反而说明清理空间少,那么GC会更频繁
3.5.6 CMS收集器
解决GC时产生的STW导致服务不可用
CMS( Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。
分为四个步骤
初始标记
寻找GC Roots
并发标记
时间长但是可与用户线程并发执行
通过三色算法,主要的思想就是在标记的过程中一旦对象的引用发生改变则记录下来,标记完成后发现还有问题就从对象引用关系变化处重新搜索标记
重新标记
解决并发标记阶段由并发执行用户线程产生的引用变化问题
并发清除
由于最耗时的两个阶段与用户线程并发执行,所以总体而言可看作与用户线程并发执行的收集器。
CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。
但CMS堆CPU的要求高,核心数少说明处理线程能力弱,多线程带来的切换负担增加反而可能降低性能。
由于采用标记-清理 算法 所以没有连续空间分配对象内存的时候会产生Full GC 并且是带着整理的,这段对象移动产生的消耗无法变短。(/ZGC 可进行并发移动对象)
CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上
3.5.7 First 收集器
基于的内存布局与管理。
JDK9后G1是默认收集器取代PS+PO。
JDK10后进行实现解耦,抽取接口实现与抽象分离从而方便编译器的扩展与替换也进一步替换CMS。
G1将内存划分为一个个区域,区域又被标记为新生代(Eden、)、老年代 。
跨代引用问题,需要采用双向卡表结构来解决,额外消耗内存大约10%-20%左右的内存用户解决跨代引用问题。
并发运行问题
CMS通过增量更新G1采用原始快照SATB
相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。
运行过程
初始标记
与同步进行,实际不消耗时间
并发标记
最终标记
SWT处理SATB
筛选回收
存活对象移动到空的再整体清空,但可根据用户指定的期望时间来定制回收计划。
并发收集器选择
按照笔者的实践经验,目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着的开发者对G1的不断优化,也会让对比结果继续向G1倾斜。
G1的跨代引用问题更严重,内存集需维护双向卡表结构。占用内存更多
3.6低延迟垃圾收集器
衡量垃圾收集器的指标
硬件提升 内存扩大 提高吞吐量 要被回收的内存也更多,将提高延迟通过三色标记算法并发执行GC以及用户线程从而提高效率
CMS会带来内存碎片问题从而STW G1虽然通过粒度细化减少停顿但毕竟还是有停顿
3.6.
目标:任何内存大小下都只有10ms停顿
G1后继者,G1借助实现多线程Full GC
摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”( )的全局数据结构来记录跨的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题(见3.4.4节)的发生概率
工作流程
若访问过期对象通过保护陷阱自动转发对象引用
提出的新方案不需要用到内存保护陷阱,而是在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己
使用CAS解决对象移动与对象变化产生的并发问题
对象访问通过内存屏障解决并发问题,读屏障比写屏障多 使用读屏障,后改为引用访问屏障降低原生操作的影响
3.6.2 ZGC
ZGC收集器是一款基于内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
基于G1 使用分代,使用读屏障 三色标记,brook
ZGC的具有动态性,容量分为小(256KB)、中(4MB)、大(4MB以上)。复制大对象代价昂贵
并发整理
染色指针三大优势
运行过程
3.7选择合适的垃圾收集器 3.7.1 收集器的权衡 3.8实战:内存分配与回收策略
Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存。
3.8.1对象优先在eden分配
对象优先在eden分配,空间不够引发minor gc,若设置了ld 则超过指定大小的对象直接进入老年代
通过-XX:设置存活多少次可以进入老年代
3.8.2动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须达到-XX:才能晋升老年代,如果在空间中相同年龄所有对象大小的总和大于空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:中要求的年龄。
3.8.3 空间分配担保
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。
GC是否安全取决于老年代是否有足够的连续空间分配新生代要晋升的对象
3.9 本章小结
本章介绍了垃圾收集的算法、若干款虚拟机中提供的垃圾收集器的特点以及运作原理。通过代码实例验证了Java虚拟机中自动内存分配及回收的主要规则。
垃圾收集算法
虚拟机中提供的垃圾收集器的特点以及运作原理