GC面临的困境,JVM是如何解决跨代引用的?

这篇具有很好参考价值的文章主要介绍了GC面临的困境,JVM是如何解决跨代引用的?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文已收录至GitHub,推荐阅读 👉 Java随想录

微信公众号:Java随想录

原创不易,注重版权。转载请注明原作者和原文链接

目录
  • 跨代引用问题
  • 记忆集
  • 卡表
  • 写屏障
  • 写屏障的伪共享问题

前面我们讲了可达性分析和根节点枚举,介绍完了GC的前置工作,下面开始讲GC的工作过程。

然而在GC开始工作之前,有一个不得不解决的问题摆在我们面前:「跨代引用问题」。

本篇文章就来聊聊什么是跨代引用问题,以及JVM是如何解决跨代引用问题的。

跨代引用问题

跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用。

为什么说这是一个问题呢?请看下图。

假如现在要进行一次只局限于新生代区域的YGC,但新生代中的对象是完全有可能被老年代所引用的,为了找到新生代中的存活对象,不得不遍历整个老年代来确保可达性分析结果的正确性。

首先,我们得明确一点,跨代引用是极少的,这很重要。

举个例子说明:如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

这简直就是原子弹炸鸟,起重机吊鸡毛。因为跨代引用是极少的,为了找出那么一点点跨代引用,却得遍历整个老年代!

而JVM里GC回收无疑是非常频繁的动作,如果每次都这么搞,性能肯定吃不消,无疑会为内存回收带来很大的性能负担。

别慌,JVM的设计者已经考虑到了这个场景,并想到了解决办法,那就是使用一种叫做:「记忆集(Remembered Set)」的数据结构。

记忆集

记忆集位于新生代中,是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。用以避免把整个老年代加进GC Roots扫描范围。

记忆集的作用和我们之前讲的OopMap很相似,维护了类似一种映射表的关系,避免了全局扫描,本质是用空间换时间。

此后当发生YGC时,只要把记忆集加进来一起扫描,就能知道新生代对象被老年代引用的情况,而不必扫描整个老年代!

虽然说增加了维护记忆集的成本,但比起收集时扫描整个老年代来说这波还是血赚!

上面不知道大家有没有留意我的说辞:「抽象数据结构」。意思就是说记忆集是一种逻辑上的概念,并没有规定具体的实现,类似方法区。

在HotSpot中,采用卡表去实现记忆集。可以把记忆集和卡表的关系理解为Map跟HashMap。

卡表

卡表可以理解为是记忆集的具体实现,英文叫:Card Table。

垃圾收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。

那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:

其中,第三种「卡精度」所指的就是「卡表」的方式去实现记忆集 ,这也是目前最常用的一种记忆集实现形式,HotSpot采用的就是卡表。

在HotSpot虚拟机里面,卡表采用的是字节数组的形式。以下这行代码是HotSpot默认的卡表标记逻辑 :

CARD_TABLE [this address >> 9] = 0;

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作「卡页(Card Page)」。

一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可以看出HotSpot中使用的卡页是2的9次幂,即512字节。

意味着如果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块 ,如图所示:

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。

简单来说,就是卡页的字节数组只有0和1两种状态,1表示哪些内存区域存在跨代指针,那么只要把1的加入GC Roots中一并扫描,就能知道哪些进行跨代引用了,这样就不用挨个去扫描了。

OK,到了这步我们的思路就清晰了。

可以把老年代划分为一个个内存区域,每块内存区域分别对应卡表的元素,然后把卡表中变脏的元素,直接加入GC Roots中一并扫描,跨代引用问题就迎刃而解了。

如图,对象A在老年代 0x0000~0x01FF 内存区域被引用,那只要把对应的卡表标记为1,YGC的时候扫描卡表,就能知道对象A被老年代哪块内存区域引用了。

but,我们还剩下一个问题,卡表元素如何维护?类似问题OopMap也遇到过。

卡表元素如何维护?何时变脏?谁来把它们变脏?

HotSpot解决的办法是使用写屏障。

写屏障

先来解决何时变脏的问题,这个问题很简单,即其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻

但问题是如何变脏,即如何在对象赋值的那一刻去更新维护卡表。

在HotSpot虚拟机里是通过「写屏障(Write Barrier)」解决的。

注意:这里提到的 写屏障 和 volatile 的写屏障不是一回事。

写屏障可以看作在虚拟机层面对「引用类型字段赋值」这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知。用过Spring的弟兄们对AOP肯定不陌生。

在赋值前的部分的写屏障叫作「写前屏障(Pre-Write Barrier)」,在赋值后的则叫作「写后屏障(Post-Write Barrier)」。

HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与YGC时扫描整个老年代的代价相比还是低得多的。

当引入一个解决方案的时候,随之而来的可能还有其他问题。卡表在高并发场景下还面临着「伪共享(False Sharing)」问题。

写屏障的伪共享问题

伪共享是处理并发底层细节时一种经常需要考虑的问题,号称并发的「隐形杀手」。

现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低。

core1 更新 A,同时 core2 更新 B,由于数据的读取和更新是以「缓存行」为单位的,这就意味着当这两件事同时发生时,就产生了竞争,导致 core1 和 core2 有可能需要重新刷新自己的数据(缓存行被对方更新了),最终导致系统的性能大打折扣,这就是伪共享问题。

为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。

即将卡表更新的逻辑变为以下代码所示:

if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;

相当于说其实就是多了一个「if 判断条件」。

在JDK 7之后,HotSpot虚拟机增加了一个新的参数「-XX:+UseCondCardMark」,此参数默认是关闭的,用来决定是否开启卡表更新的条件判断。

开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

看到这,本篇文章就结束啦,这章讲了跨代引用和记忆集。

GC收集还有很多是需要我们去搞清楚的。知道的越多,不知道的越多,这只是个开端,一起期待下篇的「三色标记算法」吧。


感谢阅读,如果本篇文章有任何错误和建议,欢迎给我留言指正。

老铁们,关注我的微信公众号「Java 随想录」,专注分享Java技术干货,文章持续更新,可以关注公众号第一时间阅读。

一起交流学习,期待与你共同进步!文章来源地址https://www.toymoban.com/news/detail-666041.html

到了这里,关于GC面临的困境,JVM是如何解决跨代引用的?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 区块链的三难困境是什么,如何解决?

    人们需要保持社交、工作和睡眠之间的平衡,并且努力和谐相处。同样的概念也反映在区块链的三难困境中。 区块链三难困境是一个术语,指的是现有区块链的局限性:可扩展性、安全性和去中心化。这是一个存在了几十年的设计问题,其问题的本质是找到一种方法,在不使

    2024年02月03日
    浏览(39)
  • 「JVM」Full GC和Minor GC、Major GC

    Full GC (Full Garbage Collection)是Java虚拟机(JVM)中的一种垃圾回收操作。它是指对整个堆内存进行回收,包括新生代和老年代。 在Java中,垃圾回收器通常会将 堆内存划分 为不同的区域,如 新生代和老年代 。当新生代空间不足时,会触发 Minor GC ,只清理新生代内存。而当老

    2024年02月15日
    浏览(42)
  • Iceberg-Trino 如何解决链上数据面临的挑战

    区块链数据公司,在索引以及处理链上数据时,可能会面临一些挑战,包括: 海量数据。随着区块链上数据量的增加,数据索引将需要扩大规模以处理增加的负载并提供对数据的有效访问。因此,它导致了更高的存储成本;缓慢的指标计算和增加数据库服务器的负载。 复杂

    2024年02月02日
    浏览(41)
  • 【JVM】JVM垃圾回收GC相关参数说明

    -XX:+PrintCommandLineFlags : 输出JVM启动参数 -XX:+UseSerialGC :在新生代和老年代使用串行收集器 -XX:SurvivorRatio :设置eden区大小和survivior区大小的比例 -XX:NewRatio :新生代和老年代的比 -XX:+UseParNewGC :在新生代使用并行收集器 -XX:+UseParallelGC :新生代使用并行回收收集器 -XX:+UseParallelO

    2024年02月04日
    浏览(44)
  • JVM GC配置指南

    本文旨在简明扼要说明各回收器调优参数,如有疏漏欢迎指正。 1、JDK版本 以下所有优化全部基于JDK8版本,强烈建议低版本升级到JDK8,并尽可能使用update_191以后版本。 2、如何选择垃圾回收器 响应优先应用:面向C端对响应时间敏感的应用,堆内存8G以上建议选择G1,堆内存

    2024年02月15日
    浏览(34)
  • JVM GC 区别

    串行收集器 : Serial , Serial Old 只有一个垃圾回收线程执行,用户线程会暂停 适用 : 内存较小的嵌入式设备 并行收集器 [吞吐量优先] : Parallel Scanvenge、Parallel Old 多条垃圾收集线程并行工作,但用户线程会是等待状态 适用 : 科学计算、后台处理 并发收集器 [停顿时间优先] : CM

    2024年02月11日
    浏览(58)
  • 【JVM】垃圾回收 GC

    垃圾回收(Garbage Collection,GC)是由 Java 虚拟机(JVM)垃圾回收器提供的一种对内存回收的一种机制,它一般会在内存空闲或者内存占用过高的时候对那些没有任何引用的对象不定时地进行回收。以避免内存溢出和崩溃的问题。JVM的垃圾回收算法包括引用类型、引用计数器法

    2024年01月16日
    浏览(48)
  • JVM 配置GC日志

    开启GC日志 多种方法都能开启GC的日志功能,其中包括:使用-verbose:gc或-XX:+PrintGC这两个标志中的任意一个能创建基本的GC日志 (这两个日志标志实际上互为别名,默认情况下的GC日志功能是关闭的) 使用-XX:+PrintGCDetails标志会创建更详细的GC日志 推荐使用-XX:+PrintGCDetails标志(

    2024年02月15日
    浏览(46)
  • JVM GC 算法原理概述

    对于JVM的垃圾收集(GC),这是一个作为Java开发者必须了解的内容,那么,我们需要去了解哪些内容呢,其实,GC主要是解决下面的三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 回答了这三个问题,也就对于GC算法的原理有了最基本的了解。 1 如何判定哪些内

    2024年02月03日
    浏览(42)
  • JVM 内存和 GC 算法

    对象头(Header) 运行时元数据(Mark word):哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳 类型指针 :指向类元数据,确定对象所属类型。如果是数组还要记录数组的长度 实例数据(Instance Data):类中的各类型变量以及父类的相关类型数据。

    2024年02月05日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包