本文主要是介绍【JVM】SafePoint与各类垃圾收集器工作过程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- SafePoint安全点与Stop The World
- 垃圾回收器
- 1. Serial收集器(复制算法)
- 2. ParNew收集器(复制算法)
- 3. Parallel Scavenge收集器(复制算法)
- 4. Serial Old收集器(标记-整理算法)
- 5. Parallel Old收集器(标记-整理算法)
- 6. CMS收集器(标记-清除算法)
- 7. G1收集器
- 总结
SafePoint安全点与Stop The World
在介绍各个垃圾回收器之前,先了解SafePoint的概念。
- 在进行对象可达性分析过程中,工作必须在一个能确保一致性的快照中进行——这里一致性的意思是指整个分析期间整个执行系列看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。
- 这就导致了GC进行时必须停顿所有的java执行线程(Stop The World)
- 当系统停顿下来之后,并不需要一个不漏地检查完所有的执行上下文和全局引用位置,虚拟机通过一组成为OopMap的数据结构,直接得知哪些地方存放着对象引用。
- 在类加载完成的时候,Hotspot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。
- 但是如果为每一条指令都生成对应的OopMap,那么将需要大量的额外空间。前面也提到了,是在“特定位置”上记录了这些信息,这些位置就叫做SafePoint安全点。程序执行时并非在所有的位置都可以停顿下来开始GC,只有在到达安全点时才能暂停。
-
如何在GC发生时让所有的线程都跑到最近的安全点上再停顿下来?
有两种方式:- 抢先式中断:在GC发生时,先把所有的线程中断,如果发现有线程中断的地方不在安全点上,就恢复线程让它跑到安全点再停下。
- 主动式中断:当GC需要中断线程时,不直接对线程进行操作,而是设置一个标志,各个线程在执行到安全点时,去主动轮询这个标志,如果发现需要暂停,就自己中断挂起。大部分虚拟机都是使用的这种方式。
-
什么地方可以放SafePoint?
理论上来讲,每一条指令都要一个SafePoint,但是为了减少性能消耗,要尽可能的减少SafePoint的数量。
通过JIT编译的代码里,在所有的方法返回之前,或者所有的无界循环回跳之前,放一个SafePoint,以防止发生stw时该线程不能暂停。
比如for(int 1=0;i<100;i++)这种有界循环是没有SafePoint的。 -
SafePoint的作用?
SafePoint机制可以Stop The World,保证一致性的工作环境,不仅仅是在GC的时候用,有很多其他地方也会用它来Stop The World,阻塞所有Java线程,从而可以安全地进行一些操作。 -
安全域是什么?
SafePoint保障了程序执行时,在不太长时间内就会遇到可进入GC的安全点,但是程序不执行的时候呢?典型的例子就是线程处于Sleep或者Blocked状态,无法响应JVM的中断请求。
这时就需要safeRegion安全域来解决,安全域是指在一段代码中,引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的。 -
我们知道SafePoint可以实现Stop The World,那么除了GC之外其他全局进入safePoint可能有哪些情况?
- 由于 jstack,jmap 和 jstat 等命令,也就是 Signal Dispatcher 线程要处理的大部分命令,都会导致 Stop the world:这种命令都需要采集堆栈信息,所以需要所有线程进入 Safepoint 并暂停。
- Java Instrument 导致的 Agent 加载以及类的重定义:由于涉及到类重定义,需要修改栈上和这个类相关的信息,所以需要 Stop the world
- Java Code Cache相关:当发生 JIT 编译优化或者去优化,需要 OSR 或者 Bailout 或者清理代码缓存的时候,由于需要读取线程执行的方法以及改变线程执行的方法,所以需要 Stop the world
- GC:这个由于需要每个线程的对象使用信息,以及回收一些对象,释放某些堆内存或者直接内存,所以需要 Stop the world
-
Stop The World耗时过长可能的原因?
Stop the world 阶段可以简单分为:- 某个操作,需要 Stop the world
- 向 Signal Dispatcher 这个 JVM 守护线程发起 Safepoint 同步信号并交给对应的模块执行。
- 对应的模块,采集所有线程信息,并对每个线程根据状态做不同的操作以及标记
- 所有线程都进入 Safepoint 并 block。
- 做需要发起 Stop the world 的操作。
- 操作完成,所有线程从 Safepoint 恢复。
基于这些阶段,导致 Stop the world 时间过长的原因有:- 阶段 4 耗时过长,即等待所有线程中的某些线程进入 Safepoint 的时间过长,这个很可能和有大有界循环 与 JIT优化有关,也很可能是 OpenJDK 11 引入的获取调用堆栈的类StackWalker的使用导致的,也可能是系统 CPU 资源问题或者是系统内存脏页过多或者发生 swap 导致的。
- 阶段 5 耗时过长,需要看看是哪些操作导致的,例如偏向锁撤销过多, GC时间过长等等,需要想办法减少这些操作消耗的时间,或者直接关闭这些事件(例如关闭偏向锁,关闭 JFR 的 OldObjectSample 事件采集)减少进入,这个和本篇内容无关,这里不赘述。
- 阶段2,阶段3耗时过长,由于 Signal Dispatcher 是单线程的,可以看看当时 Signal Dispatcher 这个线程在干什么,可能是 Signal Dispatcher 做其他操作导致的。也可能是系统 CPU 资源问题或者是系统内存脏页过多或者发生 swap 导致的。
垃圾回收器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
- 新生代可配置的回收器:Serial、ParNew、Parallel Scavenge
- 老年代配置的回收器:CMS、Serial Old、Parallel Old
- 新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。
- G1是一个独立的收集器不依赖其他6种收集器
1. Serial收集器(复制算法)
Serial是一个单线程的回收器,在进行垃圾回收时,必须暂停其他所有的工作线程。
2. ParNew收集器(复制算法)
ParNew其实就是Serail的多线程版本,使用了多条线程进行垃圾收集。
3. Parallel Scavenge收集器(复制算法)
Parallel Scavenge也是一个多线程的、使用复制算法的收集器,与ParNew不同的是它的目标是达到一个可控制的吞吐量。
- 吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量可以高效利用CPU时间,尽快地完成程序运算任务,主要适合在后台运算而不需要太多交互的任务。
4. Serial Old收集器(标记-整理算法)
Serial Old是Serial的老年代版本,同样是一个单线程收集器。这个收集器的主要意义在于给Client模式下的虚拟机使用。
5. Parallel Old收集器(标记-整理算法)
Parallel Old是Parallel Scavenge的老年代版本,在注重吞吐量以及CPU资源敏感的场合,都可以优选考虑Parallel Scavenge+Parallel Old。
6. CMS收集器(标记-清除算法)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其注重服务的响应速度,希望系统停顿时间最短,以给用户带来好的体验。
整个过程分为4个步骤:
- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快
- 并发标记:进行GC Roots Tracing的过程
- 重新标记:修正并发标记期间用户线程继续运行导致标记变动的那部分对象记录
- 并发清除
其中初始标记和重新标记仍然需要Stop The World。单总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
它存在3个明显的缺点:
- 对CPU资源非常敏感。CMS的并发能力依赖于CPU资源,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致程序变慢,总吞吐量降低。
- 无法处理浮动垃圾:由于CMS的并发清理阶段用户线程还在运行,在这一阶段出现的垃圾,将无法当次收集中处理掉它们,只好等下一次再回收。
- 收集结束时有大量空间碎片,由其使用的标记-清除算法导致。
7. G1收集器
G1(Garbage First)收集器是当今收集器技术发展的最前沿成果之一。它是一款面向服务端应用的垃圾收集器,具备如下特点:
- 并行与并发:G1可以充分利用多CPU多核环境的硬件优势,使用多个CPU来缩短STW停顿的时间
- 分代收集:虽然G1不需要其他收集器就可以独立管理整个GC堆,但它能够采用不同的方式去处理新建的对象和已经存活了一段时间、熬过多次GC的就对象,以获取更好的收集效果。
- 空间整合:G1整体来看是标记-整理算法,从局部来看是基于复制算法实现的。但无论如何都意味着不会产生内存碎片。
- 可预测的停顿:这是G1相对于CMS的另一优势,G1除了追求低停顿外,还可以建立可预测的停顿时间模型。
使用G1收集器时,java堆内存的分布与其他收集器有很大差别,它将整个java堆划分为多个大小相等的独立区域Region。
- 虽然还保留着新生代和老年代的概念,但它们之间不再是物理隔离的,而是一部分Region的集合。
如何避免全堆扫描?
- 虽然它把堆分为了多个Region,但是Region之间并不可能是孤立的,一个对象分配在某个Region中,并非只能被本Region中的对象引用,而是可以与整个堆中的对象发生引用关系。那么在判定对象是否存活的时候,岂不是要扫描整个堆?
这个问题只是在G1中更加突出,以前的分代收集中,新生代与老年代收集时也面临同样的问题。- 在G1收集中,Region之间的对象引用以及其他收集器中新生代老年代之间的对象引用,虚拟机都是通过Remembered Set来避免全堆扫描的。
- G1中每一个Region都有一个与之对应的Remembered Set,如果虚拟机发现了在对引用类型的数据写操作时,引用对象处于不同的Region,那么将相关信息记录到Remembered Set。当进行内存回收时,在GC Roots的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
G1收集器的整体过程(不包括Remembered Set的操作):
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
它的前几个步骤与CMS很像,最后再筛选回收阶段,首先对各个Region的回收价值与成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,时间是用户可控的。
总结
这篇关于【JVM】SafePoint与各类垃圾收集器工作过程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!