垃圾回收器、垃圾回收算法、空间分配担保、JVM调优、GC回收对象的过程

简介: 垃圾回收器、垃圾回收算法、空间分配担保、JVM调优、GC回收对象的过程

🍊 垃圾回收器、垃圾回收算法、空间分配担保

垃圾回收器有多个,先说新生代的三个垃圾回收器,serial,parnew,parallel scavenge,然后再说老年代的serial old,parallel old,cms,最后在说一下新生代和老年代都使用的垃圾回收器G1吧。

🎉 Serial

Serial是新生代下使用复制算法,单线程运行的垃圾回收器,简单高效,没有线程交互开销,专注于GC,这个垃圾回收器工作的时候会将所有应用线程全部冻结,而且是单核cpu,所以基本不会考虑使用它。

🎉 ParNew

ParNew是新生代下使用复制算法,多线程运行的垃圾回收器,可以并行并发GC,和serial对比,除了多核cpu并行gc其他基本相同。

🎉 Parallel scavenge

Parallel scavenge也是新生代下使用复制算法,可以进行吞吐量控制的多线程回收器,主要关注吞吐量,通过吞吐量的设置控制停顿时间,适应不同的场景。可以发现新生代的垃圾回收器都使用,复制算法进行gc。

🎉 复制算法

新生代中每次垃圾回收都要回收大部分对象,所以为了避免内存碎片化的缺陷,这个算法按内存容量将内存划分为大小相等的两块,每次只使用其中一块,当这一块存活区内存满后将gc之后还存活的对象复制到另一块存活区上去,把已使用的内存清掉。

🎉 分代收集算法

按照分代收集算法的思想,把应用程序可用的堆空间分为年轻代,老年代,永久代,然后年轻代有被分为Eden区和二个Survivor存活区,这个比例又可以分为8比1比1。当第一次eden区发生minor gc,会把存活的对象复制到其中的一个Survivor区,然后eden区继续放对象,直到触发gc,会把eden区和之前存放对象的Survivor区一起gc,二个区存活下来的对象,复制到另一个空的Survivor里面,这二个区就清空,然后将二个存活区角色互换。

🎉 进入老年代的几种情况

当对象在Survivor区躲过一次GC 后,年龄就会+1,存活的对象在二个Survivor区不停的移动,默认情况下年龄到达15的对象会被移到老生代中,这是对象进入到老年代的第一种情况。

这里就会有个问题,JVM分代年龄为什么是15次?

一个对象的GC年龄,是存储在对象头里面的,一个Java对象在JVM内存中的布局由三个部分组成,分别是对象头、实例数据、对齐填充。而对象头里面有4个bit位来存储GC年龄。

4个bit位能够存储的最大数值是15,所以从这个角度来说,JVM分代年龄之所以设置成15次是因为它最大能够存储的数值就是15。虽然JVM提供了参数来设置分代年龄的大小,但是这个大小不能超过15。从设计角度来看,当一个对象触发了最大值15次gc,还没有办法被回收,就只能移动到old generation了。另外,设计者还引入了动态对象年龄判断的方式来决定把对象转移到old generation,也就是说不管这个对象的gc年龄是否达到了15次,只要满足动态年龄判断的依据,也同样会转移到old generation。

第二种情况就是,创建了一个很大的对象,这个对象的大小超过了jvm里面的一个参数max tenuring thread hold值,这个时候不会创建在eden区,新对象直接进入老年代。

第三种情况,如果在Survivor区里面,同一年龄的所有对象大小的总和大于Survivor区大小的一半,年龄大于等于这个年龄对象的,就可以直接进入老年代,举个例子,存活区只能容纳5个对象,有五个对象,1岁,2岁,2岁,2岁,3岁,3个2岁的对象占了存活区空间的5分之三,大于这个空间的一半了,这个时候大于等于2岁的对象,需要移动到老年代里面,也就是3个2岁的,一个3岁的对象移动到老年代里面。

📝 空间分配担保

第四种情况就是eden区存活的对象,超过了存活区的大小,会直接进入老年代里面。另外在发生minor gc之前,必须检查老年代最大可用连续空间,是不是大于新生代所有对象的总空间,如果大于,这一次的minor gc可以确保是安全的,如果不成立,jvm会检查自己的handlepromotionfailure这个值是true还是false。true表示运行担保失败,false则表示不允许担保失败。如果允许,就会检查老年代最大可用连续空间是不是大于历次晋升到老年代平均对象大小,如果大于就尝试一次有风险的minorgc,如果小于或者不允许担保失败,那就直接进行fgc了。

举个例子,在minorgc发生之前,年轻代里面有1g的对象,这个时候,老年代瑟瑟发抖,jvm为了安慰这个老年代,它在minor gc之前,检查一下老年代最大可用连续空间,假设老年代最大可用连续空间是2g,jvm就会拍拍老年代的肩膀说,放心,哪怕年轻代里面这1g的对象全部给你,你也吃的下,你的空间非常充足,这个时候,老年代就放心了。

但是大部分情况下,在minor gc发生之前,jvm检查完老年代最大可用连续空间以后,发现只有500M,这个时候虚拟机不会直接告诉老年代你的空间不够,这个时候会进行第二次检查,检查自己的一个参数handlepromotionfailure的值是不是允许担保失败,如果允许担保失败,就进行第三次检查。

检查老年代最大可用连续空间是不是大于历次晋升到老年代平均对象大小,假设历次晋升到老年代平均对象大小是300M,现在老年代最大可用连续空间只有500M,很明显是大于的,那么它会进行一次有风险的minorgc,如果gc之后还是大于500M,那么就会引发fgc了,但是根据以往的一些经验,问题不大,这个就是允许担保失败。

假设历次晋升到老年代平均对象大小是700M,现在老年代最大可用连续空间只有500M,很明显是小于的,minorgc风险太大,这个时候就直接进行fgc了,这就是我们所说的空间分配担保。

🎉 Serial Old

Serial Old就是老年代下使用标记整理算法,单线程运行的垃圾回收器。

🎉 Parallel old

Parallel old也是老年代下使用标记整理算法,可以进行吞吐量控制的多线程回收器,在JDK1.6才开始提供,在JDK1.6之前,新生代使用ParallelScavenge 收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器而出现的。

上面的Serial Old,Parallel Old这二个垃圾回收器使用的是标记整理算法.

🎉 标记整理算法

标记整理算法是标记后将存活对象移向内存的一端,然后清除端边界外的对象。标记整理算法可以弥补标记清除算法当中,内存碎片的缺点,也消除了复制算法当中,内存使用率只有90%的现象,不过也有缺点,就是效率也不高,它不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记整理算法要低于复制算法。

🎉 CMS

CMS是老年代使用标记清除算法,并发收集低停顿的多线程垃圾回收器。这个垃圾回收器可以重点讲一下,CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下4个阶段:

初始标记,只是标记一下GC Roots,能直接关联的对象,速度很快,需要暂停所有的工作线程。

并发标记,进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

重新标记,为了修正在并发标记期间,因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要暂停所有的工作线程。

并发清除,清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

但是很明显无法处理浮动垃圾,就是已经标记过的对象,开始进行并发清除的时候,这个时候又有垃圾对象产生,这个时候,没办法清除这部分的浮动垃圾了,还有一个问题就是容易产生大量内存碎片,这和它的算法特性相关。

🎉 标记清除算法

标记清除算法分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。

CMS使用标记清除算法看中的就是它的效率高,只不过内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

🎉 G1

G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,每个区域又可以根据分代理论分为eden区,Survivor区,只要这个区域里面出现了一个对象,超过了这个区域空间的一半就可以把它当作大对象,g1专门开辟了一块空间用来存储大对象,这个区域的大小,可以通过jvm的参数去设置,取值范围是1~32mb之间,那么如果有一个对象超过了32mb,那么jvm会分配二个连续的区域,用来存储这个大对象。

跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,保证了G1 收集器可以在有限时间获得最高的垃圾收集效率。而且基于标记整理算法,不产生内存碎片。可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。在jdk1.9的时候,被设置成默认的垃圾回收器了。

🎉ZGC

JDK 11 中推出的一款低延迟垃圾回收器,停顿时间不超过 10ms,停顿时间不会因堆变大而变长,支持 8MB~4TB 级别的堆。

📝内存结构

ZGC 是页为单位进行划分。

  • 小型 Region(Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。
  • 中型 Region(Medium Region):容量固定为 32MB,用于放置大于 256KB 但是小于 4MB 的对象。
  • 大型 Region(Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。
📝回收过程

ZGC 的三个 STW 阶段指的是:

  1. 初始标记(Initial Marking):ZGC 首先会扫描整个堆,标记出所有存活的对象。这个过程需要 STW,因为在这个阶段中,GC 算法需要扫描 Root 集合,也就是全局可达对象集合,并标记为活动对象,同时标记所有的引用对象以避免被回收。
  2. 再标记(Concurrent Marking):在标记所有存活对象之后,ZGC 会在堆中标记新对象的一些变化,并使用多线程进行 GC。这个阶段是 STW,因为 GC 算法需要遍历存活对象的引用字段,并标记丢弃的对象。这个阶段通常持续几毫秒而已。
  3. 初始转移(Initial Relocate):这个阶段是 ZGC 的独特之处。在所有存活对象都被标记后,ZGC 会开始对堆中的对象进行移动和重构。这个过程是并发的,不需要 STW。在这个阶段中,ZGC 会将活动对象移动到一个新的区域中,并释放已删除的对象的内存。这将导致堆对象的布局和连续性发生变化。

总之,ZGC 的三个 STW 阶段是为了进行有效的垃圾回收和内存整理,其中只有初始标记和再标记是 STW,而初始转移是并发执行的。这些阶段共同协作,使得 ZGC 能够在几毫秒内进行高效的垃圾回收操作,不会过多地影响应用程序的性能。

STW指的是Stop-The-World,即停顿整个程序世界的运行。在Java虚拟机中,STW一般用于垃圾回收、内存分配器的运行等等需要整个虚拟机运行环境停止的场景中。在STW期间,所有的线程都会暂停执行,直到垃圾回收等操作完成。由于STW会导致系统停顿,因此一般需要尽量缩短STW的时间以提升应用程序性能。

📝技术特性

ZGC中的读屏障是一种机制,用于保证对象读取的正确性。当线程读取一个对象时,读屏障会检查该对象是否正在被垃圾回收器回收,如果是,则会阻塞该线程,直到垃圾回收完成。这个机制保证了线程读取的对象都是有效的,避免了出现空指针等错误。

着色指针是ZGC中另一个重要的机制,用于标记对象是否存活。在ZGC中,所有对象都被标记为白色。当垃圾回收器开始执行时,它会从根对象开始遍历所有可达对象,并将它们标记为黑色。被标记为黑色的对象表示它们是存活的,而未被标记的白色对象则表示它们需要被回收。为了提高垃圾回收的效率,ZGC还使用了灰色和原色两个状态。灰色表示该对象已经被标记过,但其引用的其他对象还没有被标记。原色则表示该对象正在被扫描。

JVM调优

JVM调优步骤:首先,我们需要收集数据。我们可以使用jstat命令来监视JVM的内存和处理器使用信息,也可以使用jmap命令生成堆转储快照。另外,我们还可以使用GUI工具如JConsole或VisualVM对CPU、内存或堆使用状态进行监视。第二步,我们需要分析数据。通过使用工具分析收集到的数据,我们可以计算GC吞吐量和新生代大小等,也可以查看堆转储信息,分析堆中对象的分布情况,是否有内存泄漏等问题。接下来,第三步,我们需要制定具体的优化方案。我们可以根据分析的数据确定具体的优化方案,比如适当调整内存大小、调整垃圾回收机制、优化代码等。对于GC调优,可以尝试调整GC算法、分配大对象空间、增加GC并行度等。对于内存调优,可以尝试减少对象的创建、复用对象等。第四步,我们需要验证优化效果。我们可以使用性能测试工具如jmeter或ab进行压力测试,以验证优化效果是否符合预期。最后,第五步,我们需要持续监控。在优化后,我们需要持续监控应用程序,及时发现并解决新问题,进行JVM调优。

JVM调优情况十分复杂,各种情况都可能导致垃圾回收不能够达到预想的效果。对于场景问题,可以从如下几个大方向进行设计:

🎉增大Eden 空间大小

大访问压力下,MGC 频繁一些是正常的,只要MGC 延迟不导致停顿时间过长或者引发FGC ,那可以适当的增大Eden 空间大小,降低频繁程度,同时要保证,空间增大对垃圾回收产生的停顿时间增长是可以接受的。

🎉如果MinorGC 频繁,且容易引发 Full GC

需要从如下几个角度进行分析。

📝S1 区大小 < MGC 存活的对象大小,对象的年龄才1岁

每次MGC存活的对象的大小,是否能够全部移动到 S1区,如果S1 区大小 < MGC 存活的对象大小,这批对象会直接进入老年代。注意 了,这批对象的年龄才1岁,很有可能再多等1次MGC 就能被回收了,可是却进入了老年代,只能等到Full GC 进行回收,很可怕。这种情况下,应该在系统压测的情况下,实时监控MGC存活的对象大小,并合理调整eden和s 区的大小以及比例。

📝相同年龄的对象所占总空间大小>s1区空间大小的一半

还有一种情况会导致对象在未达到15岁之前,直接进入老年代,就是S1区的对象,相同年龄的对象所占总空间大小>s1区空间大小的一半,所以为了应对这种情况,对于S区的大小的调整就要考虑:尽量保证峰值状态下,S1区的对象所占空间能够在MGC的过程中,相同对象年龄所占空间不大于S1区空间的一半, 因此对于S1空间大小的调整,也是十分重要的。

📝解决方案
调整年龄阈值

可以通过设置 JVM 参数"-XX:MaxTenuringThreshold"来调整年龄阈值。该参数指定对象晋升老年代的最大年龄,通常默认值为15岁。可以逐渐增加该值,以减少老年代中相同年龄对象的数量。

请注意,调整年龄阈值需要根据具体应用程序的情况来确定。如果将年龄阈值设置得太高,可能会导致年轻代中的对象数量过多,从而增加Young GC的频率,进而影响系统性能。

增加S区的大小

如果S区足够大,那么S1区所占的比例就会更小。这样可以降低相同年龄段对象的总空间大小,从而使其不大于S1区的一半。

改变对象分配的位置
  • 设置对象的大小阈值。通过调整对象分配的大小阈值,可以让 JVM 将较大的对象分配到老年代中,减少新生代中对象数量,从而减少垃圾回收的频率。可以通过 -XX:PretenureSizeThreshold 参数来设置对象的大小阈值。
  • 调节新生代大小。通过调整新生代的大小,可以增加对象在新生代中的寿命,从而让更多的对象进入老年代,减少在新生代中对象的数量。可以通过调整 -Xmn 参数来设置新生代的大小。
  • 调节垃圾回收器参数。不同的垃圾回收器有不同的参数,可以根据具体的情况调节垃圾回收器的参数,以达到更好的效果。比如使用 G1 垃圾回收器,可以通过调节 -XX:G1HeapRegionSize 参数来控制 region 的大小,从而控制对象在不同 region 中的分配情况。

🎉大对象创建频繁

由于大对象创建频繁,导致Full GC 频繁。对于大对象,JVM专门有参数进行控制,-XX: PretenureSizeThreshold。超过这个参数值的对象,会直接进入老年代,只能等到full GC 进行回收,所以在系统压测过程中,要重点监测大对象的产生。如果能够优化对象大小,则进行代码层面的优化,优化如:根据业务需求看是否可以将该大对象设置为单例模式下的对象,或者该大对象是否可以进行拆分使用,或者如果大对象确定使用完成后,将该对象赋值为null,方便垃圾回收。

📝代码层面无法优化

如果代码层面无法优化,则需要考虑:

调高-XX: PretenureSizeThreshold参数的大小

调高-XX: PretenureSizeThreshold参数的大小,使对象有机会在eden区创建,有机会经历MGC以被回收。但是这个参数的调整要结合MGC过程中Eden区的大小是否能够承载,包括S1区的大小承载问题。

大对象必须进入老年代

这是最不希望发生的情况, 如果必须要进入老年代,也要尽量保证,该对象确实是长时间使用的对象,放入老年代的总对象创建量不会造成老年代的内存空间迅速长满发生Full GC,在这种情况下,可以通过定时脚本,在业务系统不繁忙情况下,主动触发full gc。

🎉MGC 与 FGC 停顿时间长

MGC 与 FGC 停顿时间长导致影响用户体验。其实对于停顿时间长的问题无非就两种情况:

📝gc 真实回收过程时间长

gc 真实回收过程时间长,即real time时间长。这种时间长大部分是因为内存过大导致,从标记到清理的过程中需要对很大的空间进行操作,导致停顿时间长。这种情况,要考虑减少堆内存大 小,包括新生代和老年代,比如之前使用16G的堆内存,可以考虑将16G 内存拆分为4个4G的内存区域,可以单台机器部署JVM逻辑集群,也可以为了降低GC回收时间,进行4节点的分布式部署,这里的分布式部署是为了降低 GC垃圾回收时间。

单台机器部署JVM逻辑集群,可以采用多个JVM进程来实现。通过将堆内存拆分成多个4G的内存区域,可以在单台机器上运行多个JVM进程,每个进程负责处理其中的一个内存区域。这样,每个JVM进程的内存使用量较小,GC回收时间也会更短,从而提高整体性能。

具体实现步骤如下:

  1. 修改JVM启动参数,在启动时指定每个进程使用的内存区域大小。比如,可以使用以下参数启动第一个进程:-Xms4g -Xmx4g,启动第二个进程时,将-Xms和-Xmx参数的值改为另一个4G的内存区域。
  2. 使用不同的端口号启动每个JVM进程。可以通过修改JVM启动参数中的来指定不同的端口号,确保每个进程使用的端口号不会冲突。

不是必须掌握的,这种场景比较少,仅适用于超大规模的

可以通过以下步骤在启动时指定每个进程使用的内存区域大小:

  1. 打开终端或命令提示符,并进入JVM的安装目录。
  2. 执行以下命令启动第一个JVM进程:
java -Xms4g -Xmx4g -Dcom.sun.management.jmxremote.port=8001 > -Dcom.sun.management.jmxremote.authenticate=false > -Dcom.sun.management.jmxremote.ssl=false <main_class>

其中,-Xms和-Xmx参数指定了堆内存的最小值和最大值,这里都设置为4G。-> Dcom.sun.management.jmxremote.port选项指定了JMX监控端口号,这里设置为8001。-> Dcom.sun.management.jmxremote.authenticate和-Dcom.sun.management.jmxremote.ssl> 选项则分别指定了JMX监控的身份验证和SSL配置,这里都设置为false。

3. 以同样的方式,执行以下命令启动第二个JVM进程:

java -Xms4g -Xmx4g -Dcom.sun.management.jmxremote.port=8002 > -Dcom.sun.management.jmxremote.authenticate=false > -Dcom.sun.management.jmxremote.ssl=false <main_class>

其中,-Dcom.sun.management.jmxremote.port选项的值改为8002,确保不与第一个进程的JMX监控端口号冲突。

通过以上步骤,就可以启动多个JVM进程,并指定每个进程使用的内存区域大小。

  1. 修改应用程序代码,将多个JVM进程作为逻辑集群来使用。比如,在应用程序中可以使用Java RMI来实现进程间的通信,从而将多个进程通过网络连接起来,形成一个逻辑集群。这样,在进行任务分配或者数据共享时,可以将任务或数据分配给不同的进程来处理。

不是必须掌握的,这种场景比较少,仅适用于超大规模的

实现多个JVM进程作为逻辑集群的步骤如下:

  1. 编写Java RMI接口
    Java RMI(Remote Method Invocation)是一种用于实现远程过程调用(RPC)的Java API。使用Java RMI,我们可以在不同的JVM之间调用方法。
    首先,我们需要定义RMI接口,定义接口中的方法,这些方法将在JVM之间进行调用。例如:
public interface TaskService extends Remote {
   void execute(Task task) throws RemoteException;
   Object getResult() throws RemoteException;
}
  1. 实现Java RMI接口
    接下来,我们需要实现定义的RMI接口,这样才能在JVM之间进行通信。例如:
public class TaskServiceImpl extends UnicastRemoteObject implements TaskService {
   private Queue<Task> taskQueue;
   protected TaskServiceImpl() throws RemoteException {
       super();
       taskQueue = new LinkedList<>();
   }
   @Override
   public void execute(Task task) throws RemoteException {
       // 处理任务
       // ...
   }
   @Override
   public Object getResult() throws RemoteException {
       // 返回结果
       // ...
       return null;
   }
}
  1. 启动RMI注册表
    使用RMI进行通信时,需要启动RMI注册表。可以使用rmiregistry命令启动:
rmiregistry
  1. 注册RMI服务对象
    在启动RMI注册表后,我们需要创建RMI服务对象,并将其注册到RMI注册表中。例如:
TaskService taskService = new TaskServiceImpl();
Naming.bind("rmi://localhost:1099/TaskService", taskService);
  1. 创建多个JVM进程
    我们需要创建多个JVM进程来运行应用程序。可以使用Java命令启动不同的JVM进程,例>如:
java -Djava.rmi.server.hostname=localhost -Djava.security.policy=java.policy -cp app.jar com.example.Application 1
java -Djava.rmi.server.hostname=localhost -Djava.security.policy=java.policy -cp app.jar com.example.Application 2
java -Djava.rmi.server.hostname=localhost -Djava.security.policy=java.policy -cp app.jar com.example.Application 3
  1. 连接到RMI服务
    在每个JVM进程中,我们需要连接到其他进程中的RMI服务对象。可以使用Naming.lookup方法进行连接,例如:
TaskService taskService = (TaskService) Naming.lookup("rmi://localhost:1099/TaskService");
  1. 分配任务
    现在,我们可以将任务分配给不同的进程来处理了。例如:
Task task = new Task();
taskService.execute(task);

在执行任务时,RMI会自动在不同的进程之间进行通信。

通过使用Java RMI,我们可以将多个JVM进程连接起来,形成一个逻辑集群。在处理任务或共享数据时,可以将任务或数据分配给不同的进程来处理,从而提高应用程序的性能和可靠性。

通过以上步骤,就可以将单台机器上的多个JVM进程组成一个逻辑集群,从而提高了系统的整体性能和可扩展性。同时,由于每个进程的内存使用量较小,GC回收时间也会更短,从而减少了系统的停顿时间。

📝gc真实回收时间 real time 并不长

gc真实回收时间 real time 并不长,但是user time(用户态执行时间) 和 sys time(核心态执行时间)时间长,导致从客户角度来看,停顿时间过长。这种情况,要考虑线程是否及时达到了安全点,通过-XX:+PrintSafepointStatistics和-XX: PrintSafepointStatisticsCount=1去查看安全点日志,如果有长时间未达到安全点的线程,再通过参数-XX: +SafepointTimeout和-XX:SafepointTimeoutDelay=2000两个参数来找到大于2000ms到达安全点的线程,这里 的2000ms可以根据情况自己设置,然后对代码进行针对的调整。除了安全点问题,也有可能是操作系统本身负载比较高,导致处理速度过慢,线程达到安全点时间长,因此需要同时检测操作系统自身的运行情况。

🎉内存泄漏导致的MGC和FGC频繁,最终引发oom

纯代码级别导致的MGC和FGC频繁。如果是这种情况,那就只能对代码进行大范围的调整,这种情况就非常多了,而且会很糟糕。如大循环体中的new 对象,未使用合理容器进行对象托管导致对象创建频繁,不合理的数据结构使用等等。 总之,JVM的调优无非就一个目的,在系统可接受的情况下达到一个合理的MGC和FGC的频率以及可接受的回收时间。

GC的回收对象的过程

当对象不可达就意味着这个对象要被回收,但是它不会立马就回收,对象不可达会把它放到一个F-Queue的队列里面,这个队列里面会启用一个低优先级的线程,去读取这些不可达的对象,然后一个一个的调用对象的finalize方法,如果对象的finalize方法被覆盖过,被调用过,这个时候虚拟机将这两种情况都视为“没有必要执行”。

这个时候这个不可达对象逃过了垃圾回收,稍后会由一条由虚拟机自动建立的、低调度优先级的 Finalizer线程去执行F-Queue中对象的finalize()方法。

finalize()方法是对象逃脱死亡命运的最后一次机会,收集器将对F-Queue中的对象进行第二次小规模的标记。

如果对象重新与引用链上的任何一个对象建立关联,那在第二次标记时它将被移出“即将回收”的集合。如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

代码示例:

public class FQThread extends Thread {
    private static final ReferenceQueue<Object> fq = new ReferenceQueue<>();
    public FQThread() {
        setDaemon(true);
        setName("FQThread");
    }
    public void run() {
        while (true) {
            try {
                // 从F-Queue队列中获取一个已经不可达的对象
                final Reference<?> reference = fq.remove();
                // 如果这个引用对象有关联的对象,那么就调用这个关联对象的finalize方法
                if (reference instanceof FinalizableReference) {
                    ((FinalizableReference) reference).finalizeReferent();
                }
                reference.clear();
            } catch (InterruptedException e) {
                // 忽略中断异常
            }
        }
    }
    static void register(Object referent, FinalizableReference<?> finalizableReference) {
        fq.register(referent, finalizableReference);
    }
}

在需要异步处理不可达对象的时候,可以调用FQThread.register()方法将这个对象和它的FinalizableReference注册到F-Queue队列中。当对象不可达时,低优先级的FQThread线程会处理这些对象,调用它们的finalizeReferent()方法,进行必要的清理操作,防止资源泄露等问题。


🔔如果您需要转载或者搬运这篇文章的话,非常欢迎您私信我哦~

相关文章
|
25天前
|
算法 Java
JVM垃圾回收机制
JVM垃圾回收机制
15 0
|
1月前
|
Java 程序员
探讨JVM垃圾回收机制与内存泄漏
探讨JVM垃圾回收机制与内存泄漏
|
2月前
|
算法 Java 关系型数据库
掌握这3个技巧,你也可以秒懂JAVA性能调优和jvm垃圾回收
JVM 是一个虚拟化的操作系统,类似于 Linux 和 Window,只是他被架构在了操作系统上进行接收 class 文件并把 class 翻译成系统识别的机器码进行执行,即 JVM 为我们屏蔽了不同操作系统在底层硬件和操作指令的不同。
24 0
|
2月前
|
存储 缓存 算法
JVM的垃圾回收机制
JVM的垃圾回收机制
|
15天前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
49 0
|
20天前
|
存储 缓存 算法
深度解析JVM世界:垃圾判断和垃圾回收算法
深度解析JVM世界:垃圾判断和垃圾回收算法
|
1月前
|
Java Serverless 对象存储
Serverless 应用引擎常见问题之jvm在进行垃圾回收的时候会导致重启如何解决
Serverless 应用引擎(Serverless Application Engine, SAE)是一种完全托管的应用平台,它允许开发者无需管理服务器即可构建和部署应用。以下是Serverless 应用引擎使用过程中的一些常见问题及其答案的汇总:
22 0
|
1月前
|
算法 Java UED
【JVM】分代收集算法:提升Java垃圾回收效率
【JVM】分代收集算法:提升Java垃圾回收效率
22 0
|
1月前
|
算法 Java
深入了解JVM和垃圾回收算法
深入了解JVM和垃圾回收算法
24 0
|
1月前
|
算法 Java
JVM中判断对象是否需要回收的方法
JVM中判断对象是否需要回收的方法