JVM学习--GC&&MAP

概述

垃圾收集(GC)

  1. 哪些内存需要回收
  2. 什么时候回收
  3. 如何回收

垃圾收集主要关注的是Java堆和方法区

  程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。故垃圾回收不需要考虑这些

回收

回收JAVA堆

引用计数算法

	在对象中添加一个引用计数器,每当有一个地方引用的时候,计数器数值加一;当引用失效,计数器值则减一;	
	需要配合大量的额外处理才能保证正确的工作
  • 可达性分析算法
      这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
    在这里插入图片描述

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
·所有被同步锁(synchronized关键字)持有的对象。
·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

引用

在JDK 1.2版之前,Java里面的引用是很传统的定义:
  如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

  • 强引用

在程序代码之中普遍存在的引用赋值,即类似“Object
obj=new Object()”这种引用关系。
只要强引用关系还存在,GC就永远不会回收被引用的对象

  • 软引用

用来描述一些还有用,但非必须的对象。
只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用

强度比软引用更弱
被弱引用关联的对象只能生存到下一次的垃圾收集发生为止

  • 虚引用

也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
设置虚引用关联的唯一目的只是为了能在这个对象被垃圾收集器回收的时收到一个系统通知

Die Or Live

  在可达性分析的算法中即使判定为不可达对象,也不是"非死不可"的;
真正宣告一个对象的死亡,至少经历俩次标记
1. 如果对象在进行可达性分析后没有与GC Roots相连接的引用链,则会进行标记,随后进行筛选
2. 判断是否需要执行finalize()方法

若对象没有覆盖finalize(),或者finalize()方法已经被虚拟机调用过,则没有必要执行死刑

若确定执行“死刑”

该对象会被放置在一个名为F-Queue的队列中
由一条由虚拟机自动建立的、低调度优先级的Finalizer程去执行它们的finalize()方法。

注意,即使是死刑也可以被拯救

虚拟机会触发finalize()方法,但是并不承诺一定会等待它运行结束
若某个对象不想死了,它的finalize()方法执行缓慢,或者是发生死循环,导致F-Queue队列队列中其他的对象永久处于等待状态,甚至导致整个内存回收子系统崩溃
finalize()方法是对象逃脱死亡的最后一次机会,稍后收集器会对F-Queue中的对象进行第二次小规模的标记,如果对象重新与引用链上的任何一个对象建立关联即可逃离

回收方法区

方法区垃圾收集的“性价比”通常是比较低的;
  在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
方法区的GC主要回收俩部分:废弃的常量不再使用的类型

回收废弃的常量与回收Java堆中的对象非常类似

判断一个类型是否属于“ 不再被使用的类

  • 该类的所有实例都已经被回收,即Java堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收(这个条件很难实现)
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、
-XX+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在
Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版[1]的虚拟机支持。

垃圾收集算法

引用计数器式垃圾收集(CRGC)

也叫直接垃圾收集

追踪式垃圾收集(TGC)

也叫间接垃圾收集

分代收集

  • 弱分代假说
    大部分都是朝生夕灭
  • 强分代假说
    越战越强

   这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

  • 跨代引用假说
    存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

标记-清除算法

算法思想:

  首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

  标记的过程就是判断对象是否属于垃圾的判定过程
缺点:
1. 执行效率不稳定
2. 内存空间的碎片化
执行过程
在这里插入图片描述

标记-复制算法

为了解决标记-清除算法的执行效率低的问题而提出的
算法思想:

  将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

缺点:

  1. 将可用内存缩小为原来的一半
  2. 在对象存活率较高时就要进行较多的复制操作,效率将会降低

执行过程
在这里插入图片描述

标记-清理

针对老年代对象的存亡特征提出的
算法思想:

  标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

执行过程
在这里插入图片描述

HotSpot的算法细节

根节点枚举

背景:
  所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问根节点枚举与之前提及的整理内存碎片一样会面临相似的“Stop The World”的困扰。

  当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。

解决方案:
  HotSpot使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

安全点

问题:
  导致OopMap内容变化的指令非常多,若为每一条指令产生一个OopMap,那将会需要大量的额外空间
安全点:
  HotSpot只在“特定的位置”记录了这些信息,这些位置称为安全点
  安全点的设定决定用户程序执行时并非在代码指令流的任意位置都能停顿下来进行垃圾收集,而是强制要求必须执行到达安全点以后才能暂停
安全点的选择原则:

  • 不能让收集器等待时间过长
  • 不能太过频繁,否则会过分增大运行时的内存负荷
  • 以“是否具有让程序长时间执行的特征”为标准进行选定

“ 长时间执行”的最明显的特征就是指令序列的复用
例如:方法调用、循环跳转、异常跳转等都属于指令序列的复用
只有具备长时间执行功能的指令才会产生安全点

如何在GC的时候让所有的线程都跑到最近的安全点?

  • 抢断式中断
      不需要线程的执行代码主动去配合。在垃圾收集发生时,系统首先把所有用户进程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上
  • 主动式中断
      当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停的主动去轮询这个标志,一旦发现中断标志为真的时候就自己在最近的安全点上主动中断挂起,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。

  轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

  HotSpot使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

安全区域

问题:
  安全点机制保证了程序执行时,在不太长的时间内就会可能遇到可进入垃圾的安全点但是程序不执行的时候(没有分配处理器时间,例如用户进程出于Sleep状态或者是Blocked状态),此时的线程无法响应虚拟机的中断请求,不能在走到安全的地方去中断挂起,虚拟机也不可能持续等待
安全区域:
  安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的

记忆集

目的:
解决对象跨代引用带来的问题
记忆集:
一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
伪代码:

	//通过对象指针实现记忆集
Class RememberedSet {
Object[] set[OBJECT_INTERGENERATIONAL_REFERENCE_SIZE];
}
	//记忆集并不需要了解跨代指针的全部细节,只需要知道某块非收集区域是否存在指向收集区域的指针即可

记忆集的性能指标

  • 字长精度
      每个记录精确到一个机器字长(就是处理器的寻址位数),该字包含跨代指针
  • 对象精度
      每个记录精确到一个对象,该对象中有字段含有跨代指针
  • 卡精度
      每个记录精确到一块内存区域,该区域内有对象韩有跨代指针

  卡表是记忆集的一种具体实现,定义了记忆集的记录精度、与堆内存之间的映射关系等
  卡表与记忆集的关系可以类比成HashMap和Map的关系
  卡表最简单的实现形式是字节数组(HotSpot虚拟机也是这样做的)
   CARD_TABLE [this address >> 9] = 0;

字节数组

  CARD_TABLE中的每一个元素对应着表示的内存区域中一块特定大小的内存块(这个内存块被称作“卡页”)。
  卡页的大小是2的N次幂的字节数
  一个卡页的内存中通常包含不止一个对象,只要卡页中有一个(或者更多)对象的字段存在跨代指针,就将对应卡表的数组元素的值标示为1,在GC发生的时候,根据卡表中的元素将包含跨代指针的卡页加入GC Roots中

写屏障

作用:
  维护卡表
写屏障:
  类似于Spring里面的Aop编程思想;写屏障可以看做是在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值的时候回产生一个环形(Around)通知,供程序执行额外的动作,即赋值的前后都被写屏障包含

经典的垃圾收集器

Seria收集器

特点:

  • 单线程收集器

  单线程不仅仅说明它只会使用一个处理器或一条收集线程去完成垃圾收集,而是说它在进行垃圾回收的时候,会把所有的其他的工作线程都停掉,直至它工作结束

  • 新生代收集器
    在这里插入图片描述
  • 简单高效(与其他的收集器单线程相比)
  • 额外内存消耗最小(内存资源受限的环境)
  • 对於单核处理器或者处理器核心数较少的环境而言,Serial收集器由于没有线程交互的开销,专心做垃圾收集可以获得最高的单线程收集效率

ParNew收集器

本质上是Serial收集器的多线程并行版本
能与CMS收集器配合工作
工作过程:
在这里插入图片描述

Parallel Scavenge收集器

  • 新生代收集器
  • 基于标记-复制算法实现
  • 目标是达到一个可控制的吞吐量(CMS收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间)

吞吐量:处理器用于运行用户代码的时间与处理器总时间的壁纸
在这里插入图片描述

  • 提供了俩个拥有精确控制吞吐量的参数-XX

    1. MaxGCPauseMillis(控制最大垃圾收集停顿时间)

      MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。如果把这个参数的值设置得更小一点并不能使得系统的垃圾收集速度变得更快。
      垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

    1. GCTimeRatio(直接设置吞吐量的大小)

    GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。

  • 垃圾收集的自适应的调节策略(GC Ergonomics)

  +UseAdaptiveSizePolicy,这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时
间或者最大的吞吐量

Serial Old收集器

  • Serial收集器的老年代版本,使用标记-整理算法
  • 服务器模式下,俩种用途
    1. 在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用
    2. 作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

工作流程:
在这里插入图片描述

Parallel Old收集器

  •   Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
  •   新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PSMarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。 由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。
  •   Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
    工作流程:
    在这里插入图片描述

CMS收集器

   CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

运作流程:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

  其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

工作流程:
在这里插入图片描述
缺点:

  • 对处理器资源敏感

  CMS默认启动的回收线程数是(处理器核心数量+3)/4
  如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大

  • CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。

  在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。

-CMS是一款基于“标记-清除”算法实现的收集器

  这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore-Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。

Garbage First收集器

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。G1是一款主要面向服务端应用的垃圾收集器。
目标: 替换CMS收集器

  JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

Mixed GC模式:
  G1可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。

  G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
  Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
在这里插入图片描述
  G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:

  • 内存占用(Footprint)
  • 吞吐量(Throughput)
  • 延迟(Latency)

Shenandoah收集器

  •   RedHat公司独立发展,被Orcale(包括以前的Sun)公司排挤,只能用于OpenJDK
  •   目标是实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器,该目标意味着相比CMS和G1,Shenandoah不仅要进行并发的垃圾标记,还要并发地进行对象清理后的整理动作。
  • Shenandoah使用转发指针读屏障来实现并发整理
    相对于G1的改进:
  • 支持并发的整理算法,G1的回收阶段是可以多线程并行的,却不能与用户线程并发。
  • Shenandoah不支持分代收集,没有实现分代

  因为出于性价比的权衡,基于工作量上的考虑将分代放到一个优先级比较低的位置上

  • 抛弃了G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。

  连接矩阵可以简单理解为一张二维表格,如果Region N有对象指向Region M,就在表格的N行M列中打上一个标记
在这里插入图片描述

工作流程:

  • 初始标记
      首先标记GC Roots直接关联的对象,该阶段仍然是“Stop The World”的,但停顿时间与堆的大小无关,只与GC Roots的数量相关。
  • 并发标记
      遍历全图,标记出全部可达对象,该阶段和用户线程一起并发,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
  • 最终标记
      处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,并将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。
  • 并发清理
      该阶段用于清理那些整个区域内连一个存活对象都没有的Region(这类Region被成为Immediate Garbage Region)
  • 并发回收
      该阶段会把回收集里面存活的对象先复制一份到其他未被使用的Region中。该阶段的运行时长取决于回收集的大小。

  复制对象这件事情如果将用户线程冻结起来再做是很简单的,但如果是俩者必须要同时并发进行的话,就变得复杂了。困难点在于移动对象的同时,用户的线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性行为,但移动后整个内存中,所有指向该对象的引用还是就对象的地址。这些很难一瞬间全部改变过来。
  Shenandoah通过读屏障和被称为“Brooks Pointers”的转发指针来解决这个问题

  • 初始引用更新
      并发回收阶段复制对象结束以后,还需要把堆中所有引向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。
      该阶段时间很短,回产生一个非常短暂的停顿。

  引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程以完成分配给特曼的对象移动任务而已。

  • 并发引用更新
      开始进行真正的引用更新操作。这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。

  并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。

  • 最终引用更新
      这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。

  解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。

  • 并发清理
      经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

  初始标记之前还有Initial Partial、Concurrent Partial和Final Partial阶段,它们可以不太严谨地理解为对应于以前分代收集中的Minor GC的工作;
  初试标记,并发标记,最终标记与G1一样

ZGC收集器

  • 目标与Shenandoah高度相似,实现思路千差万别。
  • ZGC的关键技术与PGC和C4只存在术语称谓上的差别,实质几乎一摸一样。
    内存布局:
  • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
  • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对
    象。
  • 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常高昂。
    并发整理算法:
  • 读屏障
  • 染色指针
      染色指针是一种直接将少量额外信息存储在指针上的技术
      在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节

  染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。这点相比起Shenandoah是一个颇大的优势,使得理论上只要还有一个空闲Region,ZGC就能完成收集,而Shenandoah需要等到引用更新阶段结束以后才能释放回收集中Region,这意味着堆中几乎所有对象都存活的极端情况,需要1∶1复制对象到新Region的话,就必须要有一半的空闲Region来完成收集。
  染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。实际上,到目前为止ZGC都并未使用任何写屏障,只使用了读屏障(一部分是染色指针的功劳,一部分是ZGC现在还不支持分代收集,天然就没有跨代引用的问题)。能够省去一部分的内存屏障,显然对程序运行效率是大有裨益的,所以ZGC对吞吐量的影响也相对较低。
  染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。现在Linux下的64位指针还有前18位并未使用,它们虽然不能用来寻址,却可以通过其他手段用于信息记录。如果开发了这18位,既可以腾出已用的4个标志位,将ZGC可支持的最大堆内存从4TB拓展到64TB,也可以利用其余位置再存储更多的标志,譬如存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。

工作流程:

  • 并发标记
      与G1、Shenandoah相同的是,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。
      与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。
  • 并发预备重分配
      这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。

  ZGC划分Region的目的并非为了像G1那样做收益优先的增
量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。

  • 并发重分配
      重分配是ZGC执行过程中的核心阶段,这个过程要把重分
    配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象。

ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。

  • 并发重映射
      重映射所做的就是修正整个堆中指向重分配集中旧对象的所
    有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。
      重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。

选择合适的垃圾收集器

  • 应用程序应该关注什么?
      如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点;
      如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点;
      如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的;
  • 运行应用的基础设施如何?
      譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是ARM/Aarch64;处理器的数量多少,分配内存的大小;选择的操作系统是Linux、Solaris还是Windows等。
  • 使用JDK的发行商是什么?版本号是多少?

内存分配与回收策略

对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

大多数对象直接进入老年代

长期存活的对象进入老年代

动态对象年龄判定

  为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

空间分配担保

  在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:
HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

垃圾收集器常用参数

在这里插入图片描述
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章