总结一下在深入理解Java虚拟机中关于垃圾收集器的学习
内存、延迟、吞吐量;
由于服务端占据主要使用面,因此低延迟是现在相对最看重的一个指标
串行 → 并行 → 与用户线程并发 → STW可控,低延迟
ZGC、G1和Shenandoah 都是以低延迟为主的收集器,总结了一下三者的区别
对象添加一个引用计数器,当有地方使用计数器就+1; 引用时效计数器-1,当计数器为零说明对象不可能被使用
使用领域如: 微软的COM技术 ,ActionScript 3 的FlashPlayer, Python,游戏脚本领域等
通过一系列 GC Roots
的跟对象作为起始节点集, 从这些节点为起点,根据引用关系往下搜索, 搜索过程走过的路径称为引用链(Reference Chain), 如果对象没有在任何引用链上,则说明对象不可达
JAVA中GC Roots对象包括:
判定对象是否存活和引用离不开关系,但是对象如果只有 被引用
和 未被引用
的话,对于一些希望内存足够时先不回收就有些力不从心了,
JDK1.2后,引用的概念进行扩充,分为强引用,软引用,弱引用,虚引用
Strongly Reference
程序代码中普遍存在的引用赋值, 无论任何情况,GC都不会回收调该对象
Soft Reference
通过SoftReference类实现软引用, 描述还有用但非必须的对象; 系统在发生内存溢出异常前会尝试对这些对象进行回收,如果回收后内存还不足才会跑出内存溢出异常
Weak Reference
通过WeakReference类实现弱引用,弱引用关联的对象只能生存到下一次垃圾收集发生时;
Phantom Reference
通过PhantomReference类实现虚引用,又称:幽灵引用或者幻影引用; 无法通过虚引用获取对象实例, 虚引用的唯一目的是为了对象回收时收到一个系统通知
对象不可达≠对象死亡
死亡路线:
对象不可达 → 第一次标记 → 筛选需要执行finalize方法的对象 → 放到F-Queue对象中 → 执行finalize方法 → 第二次标记 → 回收,对象死亡
这里的finalize是老版本的一个妥协,finalize本身只会被调用一次;
如果没有覆盖finalize方法或者对象第二次发现没有引用链,会被认为不需要执行,然后进行回收
JDK 9 中标记为不推荐使用; JDK18应该会移除; 如果有资源需要释放应该try-finally进行处理
Java虚拟机规范中提过方法区(JDK 1.8之前的永久代,JDK1.8之后的元空间)可以不实现垃圾回收;
譬如JDK 11时期的ZGC收集器;
不同多数垃圾回收期是会实现方法区垃圾回收的;方法区的垃圾回收主要有两部分内容:
- 类的所有实例都被回收了,堆中不存在任何该类和派生类的实例
- 加载这个类的类加载器已经被回收,一些场景会有临时的类加载器
- 这个Class对象没有任何地方被引用,无法通过反射访问该类的方法
大多数垃圾回收机都是基于分代收集理论进行设计,分带理论基于连个分带假说建立:
基于分代理论的收集器将堆划分为不同的区域,然后基于不同的区域进行垃圾收集,实现垃圾回收的效率 ;
分代并不完美,因此最新出现的垃圾收集器都是面向全区域或者支持区域部分带的垃圾收集模式 |
---|
- 执行效率不稳定, 可能会有大量标记和清理的动作,效率随对象增多而降低
- 内存空间碎片化问题, 碎片太多会导致因大对象没有足够连续空间而提前GC
- 碎片化必须依赖更为复杂的内存分配器和内存访问器来解决标记复制: 内存分两块,将存活的对象复制到另一块内存中
- G1的新生代分为Eden和Survivor(又分from和to),默认比例: 8:1:1缺点:
- 需要两份内存空间,空间浪费严重
- 如果存活对象太多,复制的开销会很大优点:
- 实现简单,内存连续,对象分配高效标记整理: 存活对象往内存一端移动, 关注吞吐量
- G1的老年代就是基于这种算法的缺点:
- 整理时对象可能需要移动,移动存活对象并更新引用是极为负重的操作,此时就必须要STW(Stop The World);
扩充知识:
主流Java虚拟机使用准确式垃圾收集,用户线程停顿下来的时候并不需要一个不漏的检查所有执行上下文和全局的引用位置,应该有办法直接得到哪些地方存放对象引用的; HotSpot中是通过OopMap来存储
类加载动作完成时,HotSpot会把对象内什么偏移量是设么类型的数据计算出来,即使编译过程中也会在特定位置记录栈和寄存器里哪些位置是引用,收集器在扫码时就可以得知这些信息,并不需要GC Roots开始查找
有了OopMap,可以快速准确完成GC Roots枚举,但是引用关系可能发生变化;如果OopMap记录所有变换,那么空间成本会很高昂;
HotSpot是选择在特定位置生成安全点,GC时让所有线程都跑到最近的安全点然后停顿下来;
停顿方案有两种:抢先式中断、主动式中断(主动式是主流)
安全点的选择准则: 是否具有让程序长时间执行的特征
,长时间的最基本特征就是指令序列的复用
比如: 方法调用、循环跳转、异常跳转等都属于指令序列复用
安全点解决了如何丁顿用户线程让虚拟机可以进入垃圾回收状态,但是无法处理线程阻塞或睡眠状态;
安全区域指的是能够确保一段代码片段之中,引用关系不会发生变化,因此这个区域中任意位置进行来讲回收都是安全的,或者可以把安全区域看做安全点的扩展
用户线程进入安全区域时,会标识自己进入安全区域,当线程离开安全区域会检查虚拟机是否完成了根节点枚举,如果完成了继续执行,否则就必须一致等待,直到收到离开安全区域的信号为止
记忆集是一种用于记录非收集区域指向了收集区域的指针集合的抽象数据结构
记忆集的实现方案比如:
最常用的是卡精度,通过卡表进行实现的,所以卡表是记忆集的一种实现方式而已;
每一块区域是一个卡页, HotSpot中一个卡页的大小是2^9=512 字节,只要卡页内存在跨带指针,那么卡页变脏,垃圾回收时只针对变脏的卡页进行跨带扫描
卡页什么时候进行更新呢?HotSpot是通过写屏障进行实现(和volatile中的写屏障不是一个概念);
在 引用类型字段赋值
阶段加入aop切面,引用对象复制产生一个环绕通知,虽然有一定开销,但是YoungGC的负担大大减小了,整体性能提升了
卡表并发时存在一个伪共享的问题;因此有一个 -XX:UseCondCardMark
的启动参数决定 不采用无条件的写屏障而是先检查卡表的标记再决定是否变为脏页
标记阶段是所有追踪式垃圾收集器的共同特征,通常使用三色标记法来对来讲回收过程进行辅助推导:
在标记阶段用户线程和收集器在并发的执行;用户线程会修改引用关系,如果想存活对象标记为可收回,可能会引起致命的问题;这种情况的两个必要条件为:
新生代收集器(基于标记-复制算法),通过单线程进行工作;
可以与CMS(基于标记-清理算法)和Serial Old(基于标记-整理算法)搭配使用
Serial收集器的最大优势就是简单而高效(与其他收集器的单线程比较) ;内存资源受限的环境中,它是所有收集器里面额外内存小号最小的; 是客户端模式下的默认新生代收集器
新生代内存占比小(如几十兆或者一两百兆),控制停顿时间一般可以控制在一百毫秒之内,只要不频繁发生收集,并且停顿用户可以接受,那么Serial是很好的选择,比如:
新生代收集器;实质上是Serial的多线程并行版本;
虽然该收集器没有太多创新,但是由于从JDK1.5开始使用CMS进行老年代收集,能够和CMS配合的只有Serial和ParNew; 那么ParNew自然而然的成为了激活CMS时新生代的默认收集器
G1出现之后,官方希望推广G1,取消了CMS+Serial和ParNew+Serial Old组合; 所以CMS+ParNew只能互相搭配使用,ParNew被合并到了CMS中,成为了收个HotSpot中退出历史舞台的首款垃圾收集器
ParNew默认开启的收集线程数等于处理器的核心数量; 可以通过 -XX:ParallelGCThreads 来限制垃圾收集的线程数
新生代收集器;基于标记复制算法的多线程并行垃圾收集器;
CMS等收集器关注的是尽可能缩减垃圾收集时用户线程的停顿时间; 而 Parallel Scavenge关注的是达到一个可控制的吞吐量
吞吐量 = 运行用户代码时间 / (用户代码执行时间+垃圾收集时间)
停顿时间短的收集器适合于用户交互或者需要保证服务响应的质量2. 高吞吐量则可以高效的利用服务器资源,尽快的完成程序的运算任务,适合在后台运算而不需要太多交互的分析任务 |
---|
Parallel Scavenge的特性:
老年代收集器,基于标记-整理算法;
单线程收集器,主要意义
老年代收集,基于标记-整理算法; 支持多线程并发收集
JDK6 才开始提供; 也就是说在这个之前 Parallel Scavenge 只能与Serial Old进行搭配; 而Serial old又是单线程的,所以Parallel Scavenge的高吞吐量就有些名不符实了,甚至不如ParNew + CMS组合 ;
直到Parallel Old,Parallel Scavenge终于有了适合它的老年代收集器了
老年代垃圾收集; 基于标记-清理算法
目的: 获取最短回收停顿时间;
很大一部分的Java应用在互联网网站或者基于浏览器的B/S系统的服务端上; 应用通常都会特别关注服务的相应时间;希望停顿时间尽可能的短,提高用户体验;这些场景CMS就很适合使用;
CMS的整体过程分为四步:
CMS是一款并发低停顿收集器; 存在三个明显缺点
浮动垃圾
; 为了Garbage First : 主要面向服务端应用的垃圾收集器,基于"停顿时间模型"收集器
目的: 关注停顿时间,可控制的停顿时间
G1开创了基于Region的堆内存分布模型;维护了一个优先级的回收队列. 通过对Region进行评估,在不超过期望停顿时间的约束下,选择对应的Region获取最高的收益;
抛开与用户线程并发执行的过程,G1的运作过程大概分 四个步骤
G1与CMS
因为G1和CMS都是比较关注停顿时间的垃圾收集器,用两个进行比较;
JDK12中出现的一款强大但是由于不是Oracle或者Sun出品(RedHat出品)导致一直被排挤的垃圾收集器; 在OpenJDK中存在但是没有存在于OracleJDK ;
目标: 低停顿 ; 目前RedHat积极扩展其使用范围,将其能够在JDK11甚至JDK8上,让不方便升级JDK版本的应用也能够享受垃圾收集技术的前沿成果
G1是现在使用比较广泛的垃圾收集器; 但是G1的重点在与最耗时的标记阶段可以与用户线程并发执行; 目标就是在延迟可控的情况下尽可能的高吞吐,但是回收阶段G1只能做到多个线程并发回收,此时用户线程必须中断 ;
Shenandoah在G1的基础上进行改善,
目前Shenandoah大概分为了九个阶段
JDK11中新增加基于标记-整理的低延迟垃圾收集器(服务端最关注的就是低延迟), JDK11也是LTS版本,很多公司通过升级JDK到11来使用ZGC这款来收集器
之前的GC是基于当前JVM规范之上进行相应的处理措施,而ZGC是通过在使用引用对象的指针上新增了四个标志位(染色指针), 通过标记位可以快速的确认对象的三色标记状态,是否重分配,是否只能通过finalize访问这些信息; 对于GC这个过程,只要知道这些信息就够了,所以与之前的GC相比,就不需要先获取引用再获取引用对应的信息; 对应,因为压缩了对象的地址空间,所以ZGC管理的内存不可以超过4TB(这个我目前碰到大数据对内存使用比较大,也不过就128G…)
ZGC的三个优势:
ZGC的运作过程
1. 并发标记; 更新染色指针
2. 并发预备重分配: 扫描整个堆得到本次收集过程中要清理的region(重分配集)
3. 并发重分配: 核心阶段, 将存活对象复制到新的region中; 如果访问旧对象会被映射到新的对象地址上(自愈能力)
4. 并发重映射: 虽然有自愈能力; 但是依旧会主动修正引用;同时也由于有自愈能力;这一步并不是一个迫切的任务
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。