作为Java程序员,除了业务逻辑以外,随着更深入的了解,都无法避免的会接触到JVM以及垃圾回收相关知识。JVM调优是一个听起来很可怕,实际上很简单的事。
感到可怕,是因为垃圾回收相关机制都在JVM的C++层实现,我们在Java开发中看不见摸不着;而实际很简单,是因为它说到底,也只是JVM替我们实现的垃圾对象回收机制,也是普通的程序代码,只要理解了垃圾回收器的底层设计思想,掌握JVM调优并非难事!
元数据区:JDK8之前是方法区。存放虚拟机加载的:类型信息,域(Field)信息,方法(Method)信息,常量,静态变量,即时编译器编译后的代码缓存
虚拟机栈:虚拟机栈中保存了每一次方法调用的栈帧信息,栈帧中包含以下信息:
本地方法栈:和虚拟机栈功能上类似,它管理了native方法的一些执行细节,而虚拟机栈管理的是Java方法的执行细节。
程序计数器:程序计数器记录线程执行的字节码行号,如果当前线程正在运行native方法则为空。每个线程都有自己的计数器
堆:JVM中产生的实例对象的存储位置
所谓的垃圾回收,主要就是回收JVM中堆内存的区域
通过以上三种算法的排列组合,产生了各种各样的垃圾回收器
堆内存逻辑分区
• Serial:单线程STW垃圾回收器,工作在年轻代。采用拷贝算法
• Serial Old:单线程STW垃圾回收器,工作在老年代。采用标记清除加压缩算法
• Parallel Scavenge:并行垃圾回收,工作在年轻代。采用拷贝算法
• Parallel Old:并行垃圾回收,工作在老年代。采用标记清除加压缩算法
• ParNew:并行垃圾回收,工作在年轻代。专门配合CMS使用
• CMS(Concurrent Mark-Sweep):并发标记清除,工作在老年代,采用标记清除算法。
• G1(Garbage First):垃圾优先算法,采用拷贝算法
• ZGC(Z Garbage Collector):一种可伸缩的低延迟垃圾回收器,旨在处理TB级别的堆,同时保持低毫秒级别的停顿时间。它通过使用读屏障和染色指针来实现这一点,并且在垃圾回收过程中几乎不需要暂停应用线程
• Shenandoah GC:是一种旨在实现低停顿时间的垃圾回收器,它通过并发的方式来回收内存。Shenandoah的目标是减少停顿时间,而不是优化吞吐量,适用于需要大内存和低延迟的应用
eden区和s0、s1的默认比例是8:1:1,可通过参数-XX:SurvivorRatio配置
对象头的年龄可通过-XX:MaxTenuringThreshold参数配置,但由于对象头中只用4个比特位存储分代年龄,因此它的区间是0-15
CMS是用于回收老年代的垃圾回收器,它采用的是标记清除算法。CMS的诞生的目的在于提供在多核环境下的并发处理中大型堆(MB~GB)垃圾的能力
作用于并发标记阶段
对象标记为黑白灰三个颜色,记录当前扫描标记的位置。
由于并发标记是与用户线程并行的,所以在并发标记的过程中对象的引用是可能发生变化的,所以可能会产生多标和漏标。并且重新标记为了减少STW的时间不会再标记黑色对象,而是扫描灰色对象的直接引用
如上图:在并发标记的过程中,同时产生这两种情况时就会发生回收错误问题:A和C断开了引用,A又引用了D。
CMS的处理方式是Increment Updater(增量更新),即当已经被扫描完的黑色对象如果产生了新的引用,则把自己标记为灰色,等待下次扫描重新标记。
但在上述的多标案例中,CMS存在却依然并发标记Bug,如下时序图
当两个垃圾回收线程m1和m3加上一个业务线程m2同时标记一个对象时,m3认为应该标灰,但m1认为应该标黑,如果最终m1的标记覆盖了m3的标记,那么对象的颜色标记错误,它下面新增的引用也不会被扫描到
CMS对于这个严重的bug的解决方案是,在重新标记阶段重新扫描时,必须从头扫描一遍,这样就增加了STW的时间
G1(Garbage-First)垃圾回收器是Java虚拟机(JVM)的一个高级垃圾回收器,旨在为具有大内存的多处理器机器提供高吞吐量和低延迟。G1垃圾回收器的主要特点包括:
G1的物理分区从分代变成了分区(Region),逻辑上分代,物理上则取消了分代,把堆整体划分成了多个(2048)相同大小的小格子(Region)
其中,每个Region的大小可通过-XX:G1HeapRegionSize设定,取值范围为1-32MB,且必须为2的N次幂,即只能为2,4,8,16,32这五个数
每一个Region都可以根据需要充当新生代的Eden区、S区(G1取消了S0和S1,只使用一个Survivor区)或者老年代。在一般的垃圾收集中对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。 G1的大多数行为都把H区作为老年代的一部分来看待。当一个对象的大小超过了一个Region容量的一半,即被认为是大对象。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,而是一系列区域(不需要连续,逻辑连续即可)的动态集合。由于G1这种基于Region回收的方式,可以预测停顿时间。G1会根据每个Region里面垃圾“价值”的大小,在后台维护一个优先级列表,每次根据用户设定的允许收集停顿的时间(-XX:MaxGCPauseMillis,默认为200毫秒)优先处理价值收益最大的Region。
G1采用的复制(copying)算法进行回收
从上述可以看出,除了并发标记,其他阶段都是需要STW的,G1收集器不单单是追求低延迟的收集器,也衡量了吞吐量,所以在延迟和吞吐量之间做了一个权衡。
从上述过程可以看出G1的处理方式是SATB(snapshot at the begining),即在并发标记中,如果出现引用的变更,G1的垃圾回收器会记录在SATB中,每次线程切回来进行垃圾回收时,先读取SATB中的记录。
简称RSet,记录了其他Region的对象到本Region的引用,使得垃圾回收器不需要扫描整个堆找到谁引用了当前分区的对象,只需扫描RSet即可
更多技术干货,欢迎关注我!