JAVA的垃圾收集器与内存分配策略【一篇文章直接看懂】

news2024/11/26 0:52:15

内存动态分配和垃圾收集技术是JAVA和C++之间最大的区别之一

垃圾收集(Garbage Collection,GC)只办三件事:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

对于对象回收的方法

  1. 引用计数法:

每处引用时+1,引用失效时-1,但是主流的Java虚拟机里面都没有选用引用计数算法来管理内存。

比如很难解决对象之间相互循环引用的问题
objA.instance=objB ;objB.instance=objA
objA = null; objB = null
后,objA和objB未被回收

  1. 可达性分析算法

主流的商用语言(Java\C#\Lisp)通过可达性分析(Reachability Analysis)进行判断。

基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

可以作为GC Roots的对象:

  • 虚拟机栈引用的对象,如各个线程被调用的方法栈中的参数、局部变量、临时变量等
  • 方法区中类静态属性引用的对象,如JAVA类的引用类型静态变量
  • Native方法使用的对象
  • JAva虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
  • 所有被同步锁(synchronized)持有的对象
  • 反应Java虚拟机内部情况的JMXBean\JVMTI中注册的回调、本地代码缓存等

引用概念的修改

判定对象是否存活都和“引用”离不开关系,但是过去的只有被引用和未被引用放在当今过于狭隘了。

譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空
间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应
用场景。

JDK1.2后队引用进行了补充

  • 强引用 Strong:如Obejct obj = new Object()的引用赋值,GC永远不会回收
  • 软引用 Soft:述一些还有用,但非必须的对象。将溢出时,列入回收内存中进行二次回收
  • 弱引用 Weak:那些非必须对象,但是它的强度比软引用更弱一些。生存到下一次GC发生的时候
  • 虚引用 Phantom:为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

对于对象的消亡判断

在可达性判断中,若判断为不可达的对象时,那么是处于缓刑阶段。宣告一个对象死亡需要两次标记过程

  1. 在可达性分析后,发现没有与GC ROOT相连的引用链,则会被第一次标记
  2. 对象是否有必要执行finalize()方法。若没有覆盖该方法或该方法已经被虚拟机调用过,则视为不需要执行。若判断为需要执行,则将对象放置到名为F-Queue的队列中,然后由优先级低的Finalizer线程去执行。如果在finalize()中重新与引用链上的任何一个对象建立关联则不会被回收,否则被回收。

回收方法区

主要回收两部分的内容:废弃的常量不再使用的类型

判断废弃的常量:假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”
判断一个类型是否属于“不再被使用的类“:
·该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
·加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。

垃圾收集算法

大致可分为 引用计数式垃圾收集(Reference Counting GC)和 追踪式垃圾收集(Tracing GC)
以下介绍的均为主流Java虚拟机中使用的追踪式垃圾收集的范畴

分代收集理论

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

由上诉的前两条理论产生的设计原则::收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

若一个区域的大多数对象都是朝生夕灭的,则只关注如何保留少量存货。若剩下的都是难以消亡的,则集中放在一起,使用较低的频率来回收这个区域,兼顾了时间开销内存的空间

如今,设计者将JAVA堆划分为新生代老年代的两个区域。但是,对象不是孤立的,对象之间会存在跨代引用,由此,引出了跨代引用假说,即存在相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的
由此,只需在新生代上建立一个全局的数据结构(记忆集,Remebered Set),该结构把老年代划分为若干小块,标识出哪块存在跨代引用。因此发生Minor GC时,只将这些小块放入GC Root中进行扫描。

不同分代的名词:
·部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集。
■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
·整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

标记-清除算法(存活率低时较好)

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

主要缺点有两个:

  1. 执行效率不稳定。若被回收的过多,则需要进行大量的标记和清除工作,导致执行效率随数量变化
  2. 内存空间碎片化。在标记清楚后,出现不连续的内存空间,在分配大对象时候,需提前出发GC操作

标记-复制算法(存活率低时较好)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。当下,多数采用此方法回收新生代

主要缺点:
将可用的内存缩小了一半,浪费空间

改进后的Appel式回收:
新生代分为一块较大的Eden空间两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间

标记-整理算法(存活率高时较好)

标记过程与“标记-清除”一直,但后续让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,是移动式的,而清除是非移动式的

移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而清除会产生碎片化空间。
因此。是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂,但是因内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吞吐量仍然是下降的,因此移动相对划算。

也有一种“和稀泥的方法”:平时多数时间都采用标记-清除算法,的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间

HotSpot算法细节实现

根节点枚举

在OopMap帮助下,可以快速准确的完成GC Roots枚举


在根节点枚举时,必须暂停用户线程,但枚举必须在一个保障一致性的快照中进行,即不会在分析过程中,引用关系还在变化。即使在号称停顿时间可控/几乎不停顿的CMS\G1\ZGC中,根节点枚举也是必须要停顿的。
但,目前JAVA虚拟机中采用的都是准确式垃圾收集``,故可以在停顿下来后,检查所有的上下文/全局的引用位置,可以直接得到对象引用(使用一组为OopMap的数据结构存放)。
类加载完成后,会把对象中的偏移量对应的类型数据计算出来,在即时编译过程中,在特定位置会记录下栈里的寄存器哪些位置是引用。
故收集器在扫描时,可以直接查到引用信息,不需要从方法区/GC ROOT查找

安全点

解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题,安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点


HotSpot并没有为每条指令生成OopMap,只是在“特定位置”记录这些信息,成为安全点(Safepoint)

GC强制要求必须执行到安全点后才能执行

  • 安全点的选取,取决于“是否具有让程序长时间执行的特征”。如方法调用、循环跳转、异常跳转等指令序列复用
  • GC发生时,让所有线程跑到最近的安全点,然后停顿:抢先式中断(Preemptive Suspension)主动式中断(Voluntary Suspension)

抢先式(没人用了):在GC发生时中断所有用户线程,若线程不在安全点上,则恢复执行,重新中断知道跑到安全点上
主动式:GC需要中断线程时,不直接对线程操作,设置一个标志位,线程执行时主动地轮询该标志,为时,到附近的安全点挂起。标志位置与安全点是重合的。

安全区域

对于处于Sleep/Blocked状态的线程,解决无法响应虚拟机的中断请求。由此引入安全区域(Safe Region)来解决。


安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点

执行到安全区域的代码时

  1. 标识自己已经进入了安全区域,故GC发生时,不再管理这些线程
  2. 离开安全区域时,检查是否虚拟机完成了根节点枚举,若完成则继续执行,若没完成,则等待直到接收到信号

记忆集与卡表

记忆集(Remember Set):解决对象跨代引用的问题,记录从非收集区域指向收集区域的指针集合的抽象数据结构

最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现


但是对此方案,维护与空间成本很高,但收集器只需要通过记忆集判断是否存在某指针,所以可以使用更粗的粒度进行记录。

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

第三种的卡精度也成为“卡表”,是最常用的记忆集实现形式。卡表定义了记忆集的记录精度、与堆内存的映射关系等。其中的每个元素对应着一块特定大小内存块,成为卡页(Card Page),(大小通常为 2 N 2^N 2N的字节数)

在卡页中不只一个对象,只要有一个存在跨代指针,则将对应的元素值标记为1,称为元素变脏(Dirty),没有则表示为0。

GC发生->筛选脏元素->得出存在跨代指针的内存块->放入GC Root中一并扫描

写屏障

为了解决卡表元素如何维护的问题,如:何时变脏、谁把他们变脏


  1. 有其他分代区域中对象引用了本区域的对象时变脏
  2. 机器码层面中,使用写屏障,把维护卡表的动作放到每个赋值操作之中

解释执行的字节码,VM执行每条字节码指令,而在编译执行中,代码是纯粹的机器指令流了

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内

这边我认为AOP切面,就是与Spring中的权限验证功能类似,在执行时判断是否可以执行。将我们原本一条线执行的程序在中间切开加入了一些其他操作一样。

赋值前的部分为:前屏障(Pre-Write Barrier)后的为后屏障(Post-Write Barrier)

引用写屏障后->为所有赋值操作生成指令->写屏障增加更新卡表操作

存在伪共享(False Sharing)问题:
因为CPU的缓存系统是以缓存行(Cache Line)为单位的,当多线程修改互相独立的变量,且变量共享同一行,会彼此影响(写回、无效化、同步)->降低了性能
解决方法:1.先检查未被标记过才标记为脏 2.JDK7新增了参数,但是增加了一次判断的开销

并发的可达性分析

包含“标记”阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会随着堆变大而等比例增加停顿时间,其影响就会波及几乎所有的垃圾收集器,因此并行收益是极大的

三色标记法:

  • 白色:尚未被垃圾收集器访问过
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

–>由此总结出了产生对象消失问题的两条结论:

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

解决方案:

  1. 增量更新(Incremental Update,破坏第一条):黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。【新增白色引用时,记录该引用,扫描结束后,以该引用的黑色为根扫描】
  2. 原始快照(Snap At The Begining,SATB,破坏第二条):无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。【删除白色引用时,记录该引用,扫描结束后,以该引用的灰色为根重新扫描】

以上记录操作都是通过写屏障来实现的

经典垃圾收集器


链接的线指代两个收集器可以搭配使用。
== 不存在“万能”的收集器,只有对具体应用场景更合适的收集器==

Serial收集器(标记-复制算法)

该收集器是一个单线程工作的收集器,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比)
1.是所有收集器里额外内存消耗(Memory Footprint)最小的
2. 对于单核/处理器较少的环境,因为没有线程交互的开销,所以可以获得最高的单线程效率

在部分微服务中,内存一般不会特别大,所以垃圾收集的停顿时间也很短

ParNew收集器(标记-复制算法)

是Serial收集器的并行版本,除了能并行其他与Serial收集器完全一致。

Serial/Serial Old收集过程:

目前的用处:在JDK7之前遗留的系统中,只有他能与CMS(过去可以实现GC线程与用户线程同时工作的收集器)搭配使用,但是CMS作为老年代的无法与新生代的Parallel Scavenge搭配使用了,只有ParNew能搭配使用。后续也被G1收集器所代替。

·并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态
·并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行,但不一定是并行的,可能会交替运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

Parallel Scavenge收集器(标记-复制算法)

达到一个可控制的吞吐量(Throughput)
吞吐量 = 运行用户代码时间 运行用户代码时间 + 运行垃圾收集时间 吞吐量 = \frac { 运行用户代码时间 } { 运行用户代码时间+运行垃圾收集时间 } 吞吐量=运行用户代码时间+运行垃圾收集时间运行用户代码时间

停顿时间越短,越适合交互频繁的程序,高吞吐量可以最高效率地利用处理器资源,适合在后台运算而不需要太多交互的分析任务

用于控制吞吐量的参数:

  1. -XX:MaxGCPauseMillis:最大垃圾收集停顿时间
  2. -XX:GCTimeRatio:直接设置吞吐量大小
  3. -XX:UseAdaptiveSizePolicy:激活后不需要人工指定细节参数,通过运行情况动态调整,称为自适应的调节策略(GC Ergonomics)

自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。
以上皆为新生代收集器


Serial Old收集器(标记-整理算法)

单线程收集器,主要提供给客户端模式下的HotSpot虚拟机使用,在服务端模式下,作为CMS的后备方案

Parallel Old收集器(标记-整理算法)

Parallel Scavenge的老年代版本,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合

CMS收集器(标记-清除算法)

CMS(Concurrent Mark Sweep)一种以获取最短回收停顿时间为目标的收集器,集中在B/S系统的服务端上,较为关注服务的响应速度,带来良好的交互体验。

整个过程分为四个步骤

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

初始标记只是标记GC Roots直接关联的对象,速度很快
并发标记直接关联的对象遍历对象图不需要停顿用户线程,并发运行。
重新标记为了修正并发标记期间,用户线程导致标记有变动的对象标记记录,时间停顿比初试标记稍长远比并发标记短
清除阶段为了删除在标记阶段判断的已死亡的对象,不需要移动存活对象,所以与用户线程并发

因此耗时最长的并发标记和并发清除都是可以与用户线程一起工作的

并发收集、停顿,但也有如下的缺点:

  1. 处理器的资源非常敏感:【面向并发设计的程序都对处理器资源比较敏感】,并发时,占用了一部分线程、导致程序变慢、CMS中,在4核的情况下,GC线程只占用不超过25%,随着核心变多而下降。但是不足4个时,分出一半了。由此,产生了 "增量式并发收集器(Incremental Concurrent Mark Sweep/i-CMS)"的变种,模仿OS的抢占式多任务。在``并发标记、清除`时,让GC与用户讲题运行->时间变慢,下降幅度不明显
  2. 无法处理“浮动垃圾”(Floating Garbage),因为并发,所以在GC时需要给用户线程留足够内存空间,所以不能等老年代快被填满时收集,JDK 5默认下是68%(偏保守了)

浮动垃圾:在CMS中,有可能出现“Con-current ModeFailure”失败进而导致另一次完全“Stop The World”的Full GC的产生。在运行自然就还会伴随有新的垃圾对象不断产生,但是出现在标记过程结束以后。CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。

  1. 因为采用的标记-清除,因此有大量空间碎片产生。所以需要内存碎片合并过程。但是在移动存活对象,是无法并行的,所以在默认情况,每次进入Full GC先进行整理【-XX:CMSFullGCsBefore-Compaction负责,要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理】

Garbage First收集器(整体标记-整理,局部标记-复制)

简称G1,里程碑式的成果,号称全功能的垃圾收集器(Fully-Featured Garbage Collector),开创了收集器面向局部收集的设计思路,和基于Region的内存布局形式,它是主要面向服务端应用的垃圾收集器。

核心思想:回收的衡量不再是属于哪个分代,而是取决于哪块的内存中的垃圾数量最多,回收收益最大,称为G1的Mixed GC模式
关键点:基于Region的堆内存布局【G1将连续的JAVA堆划分为多个大小相同的独立区域(Region),每个Region可以根据需要扮演新生代的Eden、Survivor、老年代空间】。Region中的HuMongous区域专门存放大对象【大小超过Region的一半】

Region分区示意图:

在应用G1收集器所解决的问题

  • Region中存在跨Region引用对象时:使用记忆集可以避免从全栈中搜索GC Root,但是Region都维护包含自己的记忆集,即G1的记忆集是双向卡表结构,【哈希表,Key:Region的起始地址,Value:卡表的索引号】,因此占用更高的内存,相当于JAVA堆的10-20%
  • 如何保证GC与用户互不干扰:1. 解决用户改变引用时,不打破原对象图结构:CMS使用了增量更新,而G1使用原始快照方式(SATB)。2. 收回过程中的新对象的分配:G1为每个Region设计两个TAMS(Top at Mark Start)指针,把Region一部分空间专门用于分配新对象。默认在这个地址的对象都是被隐式标记过,即默认存活,不纳入回收范围
  • 如何建立可靠的停顿预测模型:以衰减均值(Decaying Average)为理论基础,在GC过程中,G1回记录Reion的Region回收成本、记忆集脏卡数等可测量的成本,得出平均值、标准偏差、置信度等信息。衰减平均能够能准确的代表“最近的”平均状态。即Region的统计状态越新,越能决定回收价值

步骤

  1. 初始标记(Initial Marking):标记GC Root直接关联的对象,并修改TAMS指针的值,与Minor GC同步完成
  2. 并发标记(Concurrent Marking):从GC Root开始对堆进行可达性分析,耗时长可与用户并发进行,扫描后进行SATB处理
  3. 最终标记(Final Marking):对用户进行短暂的暂停,处理并发后遗留的SATB记录
  4. 筛选回收(Live Data Counting And Evacuation):更新Region的统计数据,对其回收价值/成本进行排序,根据用户的需求制定回收计划。在这使用标记-复制算法后清除整个旧Region,设计对象移动,暂停用户,多条GC线程并行完成。


达到延迟可控的情况下,尽可能提高吞吐量的目的。需求从一次把整个JAVA堆清干净,变为能够应付应用的内存分配速率(Allocation Rate)即可。与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”,但从局部(两个Region之间)基于“标记-复制”,因此G1运作期间不会产生内存空间碎片。但G1在GC时的内存占用(Footprint)和程序运行的负载(Overload)都比CMS高

  1. 内存占用角度:G1的卡表更复杂,每个Region都要有卡表,而CMS只有一份只处理老年代对新生代的引用
  2. 执行负载:CMS使用写后屏障,G1使用写后维护卡表的同时,因使用SATB,所以使用写前屏障跟踪并发时的并发情况。G1把写前和写后要做的事放入类似消息队列的结构中,进行异步处理

因此小内存选CMS,大内存选G1,平衡点在6-8GB之间

低延迟垃圾收集器

GC的衡量标准:内存占用(Footprint)、吞吐量(Throughput)、延迟(Latency),随着计算机硬件的发展,延迟的重要性日益凸显,越发备受关注

各收集器的并发情况:
浅色为必须挂起用户线程,深色表为GC与用户并发工作。
在CMS/G1之前都要“Stop The World”停顿,在CMS/G1分别使用增量更新/原始快照,实现了标记阶段的并发。但是CMS中整理碎片空间也要“Stop The World”、G1可以按Region来回收,但是也是需要在筛选回收时暂停的。

目前Shenandoah/ZGC都是实验阶段的GC

Shenandoah收集器

在商用被ban了
像是G1的下一代继承整,有着类似的堆内存布局。在管理内存的领域改进如下:

  1. 支持并发的整理算法
  2. 默认不适用分代 收集
  3. 摒弃了记忆集,改用链接矩阵(Connection Matrix):N有对M的引用->标记matrix[N][M]。

步骤

  1. 初始标记(Initial Marking):标记GC Root直接相关联的对象,短停
  2. 并发标记(Concurrent Marking):标记对象图中可达对象,与用户并发
  3. 最终标记(Final Marking):处理剩余的SATB扫描,统计出回收价值最高的Region,组成回收集,小暂停

---------以上与G1相同----------

  1. 并发清理(Concurrent Cleanup):清除没有存活对象的Region【称为 Immediate Garbage Region
  2. 并发回收(Concurrent Evacuation):核心差异,先复制回收集中存活的对象到空Region中,使用读屏障和“Brooks Pointers转发指针解决,时间取决于回收集大小
  3. 初始引用更新(Initial Update Reference):并未操作,只是为了确保GC完成了对象移动工作,有短暂停顿
  4. 并发引用更新(Concurrent Update Reference):开始更新引用,但是是按照内存物理地址的顺序搜索引用类型后修改
  5. 最终引用更新(Final Update Reference):修改中的引用,要修改GC Root中的引用,最后一次停顿,与GC Root有关
  6. 并发清除(Concurrent Cleanup):回收集中的所有Region都没有存活对象了,直接全回收掉

黄色:被选入回收集的Region
绿色:还存活的对象
蓝色:用户可以用来分配对象的Region

支持并行整理的核心概念:Brooks Pointer
在原有对象结构前添加一个在不处于并发移动时,引用指向对象自己的字段。【像句柄定位】

存在的问题

  1. 执行效率的问题:保证并发时的访问一致性,需要设置读、写屏障拦截,与其他GC模型相比,加入了额外的转发处理,故而读代价很大,由此改进为基于引用访问屏障(Load Reference Barrier),只拦截引用类型的读写操作
  2. 性能表现:未实现最大停顿在10毫秒内,高运行负担导致吞吐量下降,但是低延迟

ZGC收集器

基于Region内存布局,不设分代,使用读屏障、染色指针和内存多重映射等技术的标记-整理算法

ZGC的Region具有动态性:动态创建、销毁、容量大小

  • 小型Region(Small Region):2MB,存放小于256kb的对象
  • 中型Region(Medium Region):32MB,存放 256kb< 对象 < 4MB
  • 大型Region(Large Region):不固定,是2MB的整数倍,存放4MB以上的对象,不会被重分配,下文详细说

并发整理算法的实现
标志性的设计染色指针技术(Colored Pointer),直接把标记信息记在对象的指针上,遍历引用图来标记引用。
64位的linux举例:

染色指针的三大优势

  1. 对象被移走后,region可以立即被释放和重用,不需要等待更新引用
  2. 可以大幅减少内存屏障的使用数量。只需要读屏障【写屏障通常是为了记录引用的变动情况】,一部分是因为染色指针,一部分是因为没有分代收集【没有跨代引用】
  3. 存储结构可扩展,来记录更多的对象标记、重定位过程相关的数据。若开发出linux前64中未使用的18位【这些不能用来寻址】

虚拟内存映射技术
JVM重新定义指针中某几位的技术;
在x86系统中,进程共用内存,不隔离。使用分页管理机制,实现线性地址到物理地址空间的映射。故而,linux/x86-64的ZGC使用了多重映射(Multi-Mapping)实现多个虚拟地址映射到同一个内存地址上【n-1】===》ZGC在虚拟地址识别的空间大于物理上的。

染色指针中的标志位看作分段符,将这些不同的地址段映射到同一个内存空间,就可以正常寻址了。【原本是一个整体,现在切开了】

运行过程(四大阶段皆可并发)

  1. 并发标记(Concurrent Mark):遍历对象图做可达性分析,但是在指针上标记,更新染色指针的Marked 0、Marked 1
  2. 并发预备分配(Concurrent Prepare for Relocate):根据查询得出清理哪些Region。每次GC扫描所有Region,扫描成本换记忆集维护成本。故重分配集只是决定存活的对象复制到别的Region中。
  3. 并发重分配(Concurrent Relocate):把重分配集存活的对象复制到新的Region中,并为每个Region维护一个转发表(Forward Table),记录旧->新的引用。并且可以只从染色指针的引用上明确得知一个对象是否处于重分配集中,若用户线程访问当前对象,可以被预置的内存屏障所拦截,根据转发表转发到新的对象上,并修正引用值,称为指针的“自愈”(Self-Healing)能力。好处:1. 只有第一次会转发,比之前的每次的开销低。2. Region中的存活对象都复制后可以立即用于新对象的分配【转发表要留着】
  4. 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中就对象的所有引用。但不是迫切任务。因为引用是可以自愈的,故而合并到了下一次GC的并发标记中完成【因为都要遍历所有对象】

ZGC是迄今为止最前沿的成果,几乎所有收集过程可并发,短暂停留只与GC Roots大小相关,在任何堆上都小于10ms

但是
因为没有分代,所以能承受的对象分配率不会太高。【对一个大堆并发收集时,因为新对象的分配率高,所以有大量的新对象,ZGC只能全都当作存活对象,但是其中大多数是很快就死的===》产生了大量的浮动垃圾】,解决这个问题只能引入分代收集。

性能方面:处于实验阶段

下图:左:吞吐量测试,右:ZGC停顿时间测试

PS:他也支持"NUMA-Aware"内存分配【专为多CPU/多核处理器】

选择合适的垃圾收集器

Epsilon收集器

一款不能够进行垃圾收集为卖点的垃圾收集器。但是还是有”自动内存管理子系统“的功能,这是GC收集器除了GC之外的工作。
如果应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择。

收集器的权衡

应用程序:

  • 数据分析、科学计算===》吞吐量
  • SLA应用===》停顿时间、延迟
  • 客户端应用、嵌入式应用===》GC的内存占用

B/S系统==》延迟时间

钱多==》商用的Vega、Zing VM

钱不够要延迟低能用新版本==》ZGC

要稳定并在Window系统==》Shenandoah

遗留系统==》CMS,大内存G1

虚拟机及垃圾收集器日记

-Xlog参数:
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]

其中最关键的selector是由tag【某个功能块的名字,如gc】与level【日记级别】共同组成。
日志级别:Trace,Debug,Info,Warning,Error,Off,决定了详细程度
HotSpot的日志规则与Log4j、SLF4j框架一致。如果不置顶,默认值是uptime\level\tags:
[3.080s][info][gc,cpu] GC(5) User=0.03s Sys=0.00s Real=0.01s

内存分配与回收策略

  • 对象优先Eden分配,大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
  • 大对象直接进入老年代【大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组】。
  • 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
  • 在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则查看HandlePromotionFailure设置是否允许失败,若可以那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/333249.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件测试标准流程

软件测试的基本流程大概要经历四个阶段&#xff0c;分别是制定测试计划、测试需求分析、测试用例设计与编写以及测试用例评审。因此软件测试的工作内容&#xff0c;远远没有许多人想象的只是找出bug那么简单。准确的说&#xff0c;从一个项目立项以后&#xff0c;软件测试从业者…

第一章 认识Python

本章目录 一、初识Python 二、Python环境安装 三、Python代码的执行 四、Python集成开发环境 五、Python2.x与Python3.x的区别 六、本章小结 Python代码的编辑和运行方式主要分为两种&#xff1a;交互模式和脚本模式。 在交互模式下&#xff0c; 用户输入Python代码并按…

非常棒的13款3DMax渲染器插件推荐给大家

3Ds Max 可能是具有最多可与其集成的外部渲染引擎的 3D 软件包。 今天我们将看看 13 个最好的 3Ds max 渲染插件&#xff0c;我们将从以下列表开始&#xff1a; 13- Radeon ProRender ProRender 的正式名称为 FireRender&#xff0c;是 AMD 的开源路径追踪器。这个 3ds Max …

Redis的缓存雪崩、击穿、穿透和解决方案

2.5 缓存穿透问题的解决思路 缓存穿透 &#xff1a;缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效&#xff0c;这些请求都会打到数据库。 常见的解决方案有两种&#xff1a; 缓存空对象 优点&#xff1a;实现简单&#xff0c;维护…

程序员不得不知道的 API 接口常识

说实话&#xff0c;我非常希望自己能早点看到本篇文章&#xff0c;大学那个时候懵懵懂懂&#xff0c;跟着网上的免费教程做了一个购物商城就屁颠屁颠往简历上写。 至今我仍清晰地记得&#xff0c;那个电商教程是怎么定义接口的&#xff1a; 管它是增加、修改、删除、带参查询…

CentOS7.6 zabix5.0-0 —agent2监控Mysql数据库(linux)

Mysql数据库安装步骤链接&#xff1a;https://bbs.huaweicloud.com/blogs/245624 &#xff08;已安装数据库此步骤可省略~&#xff01;&#xff01;&#xff09; 至少需要两台虚拟机进行试验 一台服务端&#xff08;监控端&#xff09;jk 一台客户端&#xff08;被监控端&…

【(C语言)数据结构奋斗100天】二叉树(上)

【(C语言)数据结构奋斗100天】二叉树&#xff08;上&#xff09; &#x1f3e0;个人主页&#xff1a;泡泡牛奶 &#x1f335;系列专栏&#xff1a;数据结构奋斗100天 本期所介绍的是二叉树&#xff0c;那么什么是二叉树呢&#xff1f;在知道答案之前&#xff0c;请大家思考一下…

Window 10 OpenCV 打开罗技(Logitech)摄像头速度慢问题解决

采用最新版OpenCV 4.7.0 摄像头对罗技摄像头进行视频图像抓取时&#xff0c;发现存在打开摄像头问题。 测试环境如下&#xff1a; 系统Windows 10 专业版CPUIntel i7-7700K 4.20GHz 摄像头型号罗技Logitech C930c 网络摄像头OpenCV版本4.7.0语言C 测试结果表明&#xff1a; …

ASP.NET Core+Element+SQL Server开发校园图书管理系统(完)

随着技术的进步&#xff0c;跨平台开发已经成为了标配&#xff0c;在此大背景下&#xff0c;ASP.NET Core也应运而生。本文主要基于ASP.NET CoreElementSql Server开发一个校园图书管理系统为例&#xff0c;简述基于MVC三层架构开发的常见知识点&#xff0c;本系列共五篇文章&a…

抖yin获客系统简介,精准获取,系统简介

功能介绍功能获取获客系统主要核心数据看板名词介绍当前运行任务&#xff1a;系统正在运行的获客任务总数&#xff0c;获取客户档案&#xff1a;符合任务规则提取的目标客户&#xff0c;总分析任务&#xff1a;系统合计运行的获客任务&#xff0c;总视频数&#xff1a;符合任务…

QuickBuck:一款专为安全研究人员设计的勒索软件模拟器

关于QuickBuck QuickBuck是一款基于Golang开发的勒索软件模拟工具&#xff0c;在该工具的帮助下&#xff0c;广大研究人员可以通过更简单的方法来判断反病毒保护方案是否能够有效地预防勒索软件的攻击。 功能介绍 该工具能够模拟下列勒索软件典型行为&#xff0c;其中包括&a…

洛谷——P1091 合唱队形

【题目描述】 n 位同学站成一排&#xff0c;音乐老师要请其中的 n−k 位同学出列&#xff0c;使得剩下的 k 位同学排成合唱队形。 合唱队形是指这样的一种队形&#xff1a;设 kk 位同学从左到右依次编号为 1,2, … ,k&#xff0c;他们的身高分别为​,​, … ,​&#xff0c;则…

m序列发生器——Verilog设计

引言 本篇文章利用Verilog编写一个m序列发生器模块。本文会给出具体的设计、测试源码。 设计说明 模块功能说明: 支持任意位宽的随机数生成;支持本原多项式配置;支持初始种子配置;设计环境: 设计语言:Verilog HDL 设计验证平台:MATLAB R20222a、Vivado 2018.3 m 序列…

初识shell

文章目录一、shell基本知识1.1为什么学习和使用Shell编程1.2 什么是Shell1.2.1 shell的起源1.2.2 shell的功能1.3 shell的分类1.4 作为程序设计的语言——shell1.5 如何学好shell1.6 shell脚本的基本元素1.7 shell脚本编写规范1.8shell脚本的执行方式1.9 执行脚本的方法1.10 sh…

PPOJ刷题-3

PPOJ刷题-3 1265: 最近公共祖先 题目描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个结点 p、q&#xff0c;最近公共祖先表示为一个结点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&…

Elasticsearch7.8.0版本进阶——分布式集群(单节点集群)

目录一、Elasticsearch集群的安装1.1、Elasticsearch集群的安装&#xff08;win10环境&#xff09;1.2、Elasticsearch集群的安装&#xff08;linux环境&#xff09;二、单节点集群&#xff08;win10环境集群演示&#xff09;2.1、单节点集群的概述2.2、单节点集群的示例一、El…

On Joint Learning for Solving Placement and Routing in Chip Design

On Joint Learning for Solving Placement and Routing in Chip Design 目录On Joint Learning for Solving Placement and Routing in Chip Design一、整体思路和创新点二、相关工作2.1 partitioning-based methods&#xff08;基于分区的方法&#xff09;2.2 stochastic/hill…

Part 4 描述性统计分析(占比 10%)——中

文章目录【后续会持续更新CDA Level I&II备考相关内容&#xff0c;敬请期待】【考试大纲】【考试内容】【备考资料】【扩展知识——大数定律和中心极限定理】3、统计分布3.1、离散型随机变量的三种重要分布3.1.1、两点分布3.1.2、伯努利试验及二项分布3.1.2.1、伯努利试验3…

算法训练营 day39 贪心算法 无重叠区间 划分字母区间 合并区间

算法训练营 day39 贪心算法 无重叠区间 划分字母区间 合并区间 无重叠区间 435. 无重叠区间 - 力扣&#xff08;LeetCode&#xff09; 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互…

Early Stopping中基于测试集(而非验证集)上的表现选取模型的讨论

论文中一般都是用在验证集上效果最好的模型去预测测试集&#xff0c;多次预测的结果取平均计算准确率或者mAP值&#xff0c;而不是单纯的取一次验证集最好的结果作为论文的结果。如果你在写论文的过程中&#xff0c;把测试集当做验证集去验证的话&#xff0c;这其实是作假的&am…