皮皮网

【校园点歌源码】【源码笔记021】【大鹅源码】concurrent源码作者

2024-11-30 07:36:44 来源:全彩票网站源码

1.Java并发源码concurrent包
2.CVS使用好处
3.简单说说ConcurrentSkipListMap
4.concurrenthashmap1.8源码如何详细解析?源码
5.Kafka源码分析(五) - Server端 - 基于时间轮的延时组件
6.死磕 java集合之ConcurrentLinkedQueue源码分析

concurrent源码作者

Java并发源码concurrent包

       深入JAVA杨京京:Java并发源码concurrent包

       在JDK1.5之前,Java并发设计复杂且对程序员负担重,源码需考虑性能、源码死锁、源码公平性等。源码JDK1.5后,源码校园点歌源码引入了java.util.concurrent工具包简化并发,源码提供多种并发模型,源码减轻开发负担。源码

       Java并发工具包java.util.concurrent源自JSR-,源码包含用于并发程序的源码通用功能。该包由Doug Lea开发,源码旨在提供线程安全的源码容器、同步类、源码原子对象等工具,源码减少并发编程的复杂性。

       并发容器如阻塞队列、非阻塞队列和转移队列等,实现线程安全功能,不使用同步关键字,为并发操作提供便利。

       同步类如Lock等,提供线程之间的同步机制,确保数据一致性。原子对象类如AtomicInteger、AtomicLong等,源码笔记021提供高效的原子操作,避免同步锁,实现线程安全。

       原子操作类在多线程环境中实现数据同步和互斥,确保数据一致性。实际应用场景包括线程安全的数据结构和算法实现。

       java.util.concurrent.atomic包中的原子操作类,使用硬件支持的原子操作实现数据的原子性,提高并发程序的效率和性能。

       值得一提的是,Java并发工具包还包含了Fork-Join框架,通过分解和合并任务,实现高效并行处理,减少等待其他线程完成时间,并利用工作偷取技术优化线程执行效率。

       Java线程池如ThreadLocalRandom类,提供高性能随机数生成,通过种子内部生成和不共享随机对象减少资源争用和消耗,提高并发程序的性能。

CVS使用好处

       cvs,全称为Concurrent Versions System,是一种功能强大的版本控制系统。它提供了一系列工具,帮助开发者追踪、管理和回滚代码变更。大鹅源码在软件开发过程中,错误和bug的引入往往在不经意间,而cvs能够帮助开发者轻松恢复到先前的稳定版本,通过比较差异来定位问题的根源。

       在使用cvs时,每个文件的全部版本都被保存在一个文件中,仅记录不同版本间的差异,这种高效的方式节省了存储空间,同时也使得版本对比变得简单直观。

       cvs的历程始于年,由Dick Grune以shell脚本的形式发布,随后Brian Berliner负责设计和编码,Jeff Polk则贡献了模块设计和销售商分支支持,共同推动了cvs的发展。

       cvs的主要功能在于存储和恢复文件版本,但不负责指导代码构造或决定如何管理磁盘空间。开发者需要额外的脚本、Makefile等工具来支持特定的构建系统,尤其是在模块化开发和共享文件的场景中,合理的磁盘规划尤为重要。

       当外部变化影响文件系统时,开发者需要考虑哪些文件需要重新构建。传统方法通常依赖于make工具,并借助自动化工具生成所需的make文件。因此,板块突破源码合理的工具支持和构建流程设计对于利用cvs进行高效版本管理至关重要。

扩展资料

       CVS是一个C/S系统,多个开发人员通过一个中心版本控制系统来记录文件版本,从而达到保证文件同步的目的。CVS版本控制系统是一种GNU软件包,主要用于在多人开发环境下的源码的维护。

简单说说ConcurrentSkipListMap

       åŸºæœ¬ä»‹ç»

       è·³è·ƒè¡¨çš„性质如下:

       æœ€åº•å±‚的数据节点按照关键字key升序排列;

       åŒ…含多级索引,每个级别的索引节点按照其关联的数据节点的关键字key升序排列;

       é«˜çº§åˆ«ç´¢å¼•æ˜¯å…¶ä½Žçº§åˆ«ç´¢å¼•çš„子集;

       å¦‚果关键字key在级别level=i的索引中出现,则级别level<=i的所有索引都包含该key。

       è·³è·ƒè¡¨ConcurrentSkipListMap的数据结构如下图所示,下图一共有三层索引,最底下为数据节点,同一层索引中,索引节点之间使用right指针相连,上层索引节点的down指针指向下层的索引节点。

源码分析核心字段分析

       head 指向 node(BASE_HEADER) 的顶层索引。

/***Thetopmostheadindexoftheskiplist.*/privatetransientvolatileHeadIndex<K,V>head;

       BASE_HEADER 头结点,即最顶层索引的头节点的value值

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()

       Node 静态内部类,即数据节点

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}

       Index 静态内部类,即普通索引节点

/***普通索引节点*/staticclassIndex<K,V>{ finalNode<K,V>node;//索引节点指向的数据节点finalIndex<K,V>down;//当前索引节点的正下方索引节点volatileIndex<K,V>right;//当前索引节点的右索引节点/***Createsindexnodewithgivenvalues.*/Index(Node<K,V>node,Index<K,V>down,Index<K,V>right){ this.node=node;this.down=down;this.right=right;}}

       HeadIndex 静态内部类,即当前级别索引的头节点

/***当前级别索引的头节点*/staticfinalclassHeadIndex<K,V>extendsIndex<K,V>{ finalintlevel;//所处索引级别/***node:当前索引指向的数据节点*down:当前索引节点的正下方索引节点*right:当前索引节点的右索引节点*level:当前索引头节点所处的索引级别*/HeadIndex(Node<K,V>node,Index<K,V>down,Index<K,V>right,intlevel){ super(node,down,right);this.level=level;}}查询

       æ ¹æ®æŒ‡å®šçš„key查询节点,源码如下:

publicVget(Objectkey){ //调用doGet方法returndoGet(key);}/***真正实现查询方法*/privateVdoGet(Objectkey){ if(key==null)thrownewNullPointerException();Comparator<?superK>cmp=comparator;outer:for(;;){ for(Node<K,V>b=findPredecessor(key,cmp),n=b.next;;){ Objectv;intc;if(n==null)breakouter;Node<K,V>f=n.next;if(n!=b.next)//inconsistentreadbreak;if((v=n.value)==null){ //nisdeletedn.helpDelete(b,f);break;}if(b.value==null||v==n)//bisdeletedbreak;if((c=cpr(cmp,key,n.key))==0){ @SuppressWarnings("unchecked")Vvv=(V)v;returnvv;}if(c<0)breakouter;b=n;n=f;}}returnnull;}

       åœ¨ä¸Šè¿°ä»£ç ä¸­ï¼Œouter处的for自旋中,首先查看findPredecessor:查询指定key节点的前驱节点。该方法在下面的好多地方会调用,例如插入元素,删除元素以及删除元素对应的索引时都会调用。

       findPredecessor方法源码如下:

/***作用1:找到key对应节点的前驱节点,不一定的真的前驱节点,也可能是前驱结点的前驱节点*作用2:删除无效的索引,即要删除节点时,将节点的索引也删除掉*/privateNode<K,V>findPredecessor(Objectkey,Comparator<?superK>cmp){ if(key==null)thrownewNullPointerException();//don'tpostponeerrorsfor(;;){ //r为q节点的右指针指向的节点,r为当前比较节点,每次都比较r节点的key跟查找的key的大小关系for(Index<K,V>q=head,r=q.right,d;;){ if(r!=null){ Node<K,V>n=r.node;Kk=n.key;//该节点已经删除,需要删除其对应的索引if(n.value==null){ //该节点已经删除,需要删除其对应的索引if(!q.unlink(r))break;//restartr=q.right;//rereadrcontinue;}//当前查找的key比r节点的key大,所以r、q节点都向右移动if(cpr(cmp,key,k)>0){ q=r;r=r.right;continue;}}//当q的下方索引节点为空,则说明已经到数据节点层了,需要退出进行后续查找处理if((d=q.down)==null)returnq.node;/***此时当前查找的key小于r节点的key,需要往下一级索引查找*d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点*/q=d;r=d.right;}}}

       findPredecessor方法的查找过程图示如下:假设要查找节点6

       ç”±äºŽå½“前r节点的key比查询的key小,所以,r、q节点都向右移动,即执行如下代码:

//当前查找的key比r节点的key大,所以r、q节点都向右移动if(cpr(cmp,key,k)>0){ q=r;r=r.right;continue;}

       æ­¤æ—¶r节点指向的数据节点为,节点的key比6节点的key大,此时需要执行如下代码:

/***此时当前查找的key小于r节点的key,需要往下一级索引查找*d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点*/q=d;r=d.right;

       æ­¤æ—¶r节点指向的数据节点为5,5节点的key比6节点的key小,q、r节点向右移动,如下图所示

       æ­¤æ—¶r节点指向的数据节点为,节点的key比6节点的key大,同理需要往下级索引走,如下图所示:

       æ­¤æ—¶r节点指向的数据节点为,节点的key比6节点的key大,同理需要往下级索引走,但是此时下一级索引为空了,即(d = q.down) == null了,此时执行的代码如下, 返回q索引指向的节点,即返回节点5.

//当q的下方索引节点为空,则说明已经到数据节点层了,需要退出进行后续查找处理if((d=q.down)==null)returnq.node;

       ä»¥ä¸Šå°±æ˜¯æ–¹æ³•findPredecessor的查找流程,咱们接着继续看上面的doGet方法

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()0

       é¦–先初始化b、n、f三个节点,如下图所示

        发现此时n节点指向的节点就是要查询的节点,于是执行如下代码:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()1

       ç›´æŽ¥è¿”回n节点的value值。查询操作完成。

插入

       è·³è·ƒè¡¨çš„插入操作分以下四种情况:

       æƒ…况1:跳跃表内存在key一致元素,做替换

       æƒ…况2:插入新元素,无须给新元素生成索引节点

       æƒ…况3:插入新元素,需要给新元素生成索引节点,且索引高度 < maxLevel

       æƒ…况4:插入新元素,需要给新元素生成索引节点,且索引高度 > maxLevel

       æºç å¦‚下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()2

       é¦–先还是跟查询操作类似,调用findPredecessor方法先查找到待插入key的前驱节点,举个例子,例如我们想要插入节点7,如下图所示:

       æŽ¥ç€è·ŸæŸ¥è¯¢æ“ä½œä¸€æ ·çš„步骤如下,直接看图:

        此时r节点指向数据节点1,节点1的key小于待插入的节点7的key,于是节点q、r同时向右移动。

       æ­¤æ—¶r节点指向数据节点,节点的key大于待插入节点7的key,于是往下一层索引继续查找,执行的代码如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()3

       åŽé¢çš„操作类似

       æ­¤æ—¶r节点的key大于待插入的节点6的key,但是q节点的down指针已为空,此时直接返回q节点指向的节点5。

       æŽ¥ç€å›žåˆ°doPut方法,先来查看outer循环,如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()4

       é¦–先初始化三个节点b、n、f,n节点为b节点的下一个节点,而f节点为n节点的下一个节点,如下图所示

       æŽ¥ç€æ¯”较节点n与待插入的key的大小,此时n节点的key小于待插入节点的key,于是b、n、f三个节点均向下移动如下图所示

       æ­¤æ—¶n节点的key大于待插入的key,此时执行如下代码,通过cas方式修改b节点的下一个节点为z节点,接着跳出outer循环。

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()5

       ç„¶åŽæˆ‘们知道doPut剩下的代码无非就是判断是否给新插入的节点z创建索引,如果需要创建对应的索引。

       é¦–先通过int rnd = ThreadLocalRandom.nextSecondarySeed();计算出一个随机数,接着进行如下判断:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()6

       å¦‚æžœrnd & 0x) == 0就给新插入的z节点创建索引,我们知道0x = 即最高位和最后一位为1,其余全部是0,

       æ¡ä»¶ï¼š(rnd & 0x) == 0什么时候成立?

       rnd这个随机数最低位和最高位同时是0的时候,条件成立,概率是1/4

       ä¸¾ä¸ªä¾‹å­ï¼šä¾‹å¦‚rnd = = 3条件就成立。

       å¦‚果条件成立的话,接着计算到底给z节点创建几级索引,代码如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()7

       é€šè¿‡while条件((rnd >>>= 1) & 1) != 0满足几次就创建几级索引。例如:

       rnd = 计算出来的level => 3

       rnd = 计算出来的level => 8

       ç„¶åŽæŽ¥ç€æ¯”较计算出来的z节点的索引跟现有的跳跃表的索引级别大小。

       æƒ…况一:z节点计算出来的索引level比跳跃表的level小

       æƒ…况二:z节点计算处理的索引level比跳跃表的level大。此时会选择最终的level为原来的调表的level + 1

       æƒ…况一

       ç»™z节点创建索引的步骤如下图所示,此时z节点的索引还没有加入跳跃表现有的索引队列中

       æŽ¥ç€ç»§ç»­æ‰§è¡Œsplice循环,代码如下:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()8

       åˆå§‹åŒ–q、r节点如下图所示

       æ­¤æ—¶r节点的key比新插入z节点,即7节点小,于是两个节点q、t都向右移动如下图所示

       æ­¤æ—¶r节点的key比新插入z节点,即7节点大,执行如下代码:

/***Specialvalueusedtoidentifybase-levelheader*/privatestaticfinalObjectBASE_HEADER=newObject()9

       æ­¤æ—¶r节点的key比新插入z节点,即7节点小,于是两个节点q、t都向右移动如下图所示

       æ­¤æ—¶r节点的key比新插入z节点,即7节点大,同理,直接看图

       æƒ…况二

       è·Ÿæƒ…况一类似,这里就不一一画图了

删除

       åˆ é™¤æ–¹æ³•å®Œæˆçš„任务如下:

       è®¾ç½®æŒ‡å®šå…ƒç´ value为null

       å°†æŒ‡å®šnode从node链表移除

       å°†æŒ‡å®šnode的index节点 从 对应的 index 链表移除

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}0

       åŒæ ·ï¼Œé¦–先通过findPredecessor方法查找到要删除key的前驱节点,就不一一画图了,直接看找到的前驱节点的图,如下:

       æŽ¥æ¯”较n节点的key与待删除的key的大小,此时n节点的key小于待删除的key,即7节点的key,于是将b、n、f三个节点都向右移动,如下图:

       æ­¤æ—¶n节点的key跟待删除的key一样,于是执行如下代码:

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}1

       æœ€åŽå†è°ƒç”¨findPredecessor清楚无效的索引,即上面删除的节点的索引。

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}2

       é‡ç‚¹é å¦‚下代码块删除索引的:

/***数据节点*/staticfinalclassNode<K,V>{ finalKkey;//数据节点的keyvolatileObjectvalue;//数据节点的valuevolatileNode<K,V>next;//指向下一个数据节点/***Createsanewregularnode.*/Node(Kkey,Objectvalue,Node<K,V>next){ this.key=key;this.value=value;this.next=next;}}3

       æˆ‘们知道在上面已经将待删除的7节点的value置为null了,直接看图:

       æ­¤æ—¶r节点的key小于待删除节点的key,于是r、q节点都向右移动。

       æ­¤æ—¶r,n节点指向的数据节点的value值为null于是执行上面的q.unlink(r)代码,将q的右指针指向r的右指针指向的节点,即就是删除了该level上的7节点的索引节点,如下图所示

       æ­¤æ—¶r节点的key大于待删除节点的key,于是往下一索引走,如下图所示

       æ­¤æ—¶r节点的key小于待删除节点的key,于是r、q节点都向右移动。

       æ­¤æ—¶r,n节点指向的数据节点的value值为null于是执行上面的q.unlink(r)代码,将q的右指针指向r的右指针指向的节点,即就是删除了该level上的7节点的索引节点,如下图所示

       åŽç»­æ“ä½œåŒç†ï¼Œæœ€ç»ˆå°†7节点的索引一一删除完,最终的图下所示

concurrenthashmap1.8源码如何详细解析?

       ConcurrentHashMap在JDK1.8的线程安全机制基于CAS+synchronized实现,而非早期版本的分段锁。

       在JDK1.7版本中,ConcurrentHashMap采用分段锁机制,包含一个Segment数组,每个Segment继承自ReentrantLock,并包含HashEntry数组,每个HashEntry相当于链表节点,用于存储key、value。默认支持个线程并发,每个Segment独立,互不影响。

       对于put流程,与普通HashMap相似,首先定位至特定的Segment,然后使用ReentrantLock进行操作,后续过程与HashMap基本相同。

       get流程简单,刀客系统源码通过hash值定位至segment,再遍历链表找到对应元素。需要注意的是,value是volatile的,因此get操作无需加锁。

       在JDK1.8版本中,线程安全的关键在于优化了put流程。首先计算hash值,遍历node数组。若位置为空,则通过CAS+自旋方式初始化。

       若数组位置为空,尝试使用CAS自旋写入数据;若hash值为MOVED,表示需执行扩容操作;若满足上述条件均不成立,则使用synchronized块写入数据,同时判断链表或转换为红黑树进行插入。链表操作与HashMap相同,链表长度超过8时转换为红黑树。

       get查询流程与HashMap基本一致,通过key计算位置,若table对应位置的key相同则返回结果;如为红黑树结构,则按照红黑树规则获取;否则遍历链表获取数据。

Kafka源码分析(五) - Server端 - 基于时间轮的延时组件

       Kafka内部处理大量的延时操作,例如,在接收到PRODUCE请求后,副本可以等待一个timeout的时间再响应客户端。下面我们来探讨一个问题:为什么Kafka要自己实现一个延时任务组件,而不是直接使用Java的java.util.concurrent.DelayQueue呢?我们可以从以下两个方面来分析这个问题。

       1.1 DelayQueue的能力

       DelayQueue相关的接口/类如下所示:

       相应地,DelayQueue提供的能力如下:

       1.2 Kafka的业务场景

       Kafka的业务背景具有以下特点:

       相应地,Kafka对延时任务组件有以下两点要求:

       这两点要求都无法通过直接应用DelayQueue的方式得到满足。

       二. 组件接口

       让我们来看看Kafka的延时任务组件对外提供的接口,从而了解其提供的能力和使用方式。

       如下所示:

       左边的两个类定义了"延时操作",右边的DelayedOperationPurgatory类定义了一个维护DelayOperaton的容器,其核心操作如下:

       三. 实现

       以下是关于"延时"实现方式的介绍。

       3.1 业务模型

       时间轮延时组件的思路如下:

       接下来,通过一个具体的例子来说明这种映射逻辑:

       首先关注上图中①号时间轮。圆环中的每一个单元格表示一个TimerTaskList。单元格有其关联的时间跨度;下方的"1s x "表示时间轮上共有个单元格,每个单元格的时间跨度为1秒。有一个指针指向了"当前时间"所对应的单元格。顺时针方向为时间流动方向。

       当收到一个延迟时间在0-1s的TimerTask时,会将其追加到①号时间轮的橙色单元格中。当收到一个延迟时间在3-4s的TimerTask时,会将其追加到①号时间轮的**单元格中。以此类推。

       现在有一个问题:①号时间轮能表示的最大延迟时间是秒,那如果收到了延迟秒的任务该怎么办?这时该用到②号时间轮了,我们称②号为①号的"溢出时间轮"。②号时间轮的特点如下:

       如此,延迟时间在-s的TimerTask会被追加到②号的紫色单元格,延迟时间在-s的TimerTask会被追加到②号的绿色单元格中。③号时间轮同理。

       刚刚是按①->②->③的顺序来分析时间轮的逻辑,反过来也可以得到有用的想象手里有一个"放大镜",其实③号时间轮的蓝色单元格"放大"后是②号时间轮;②号时间轮的蓝色单元格"放大"后是①号时间轮;蓝色单元格并不实际存储TimerTask。

       3.2 数据结构

       DelayedOperationPurgatory有一个Timer类型的timeoutTimer属性,用于维护延时任务。实际使用的是Timer的实现类:SystemTimer。该类用于维护延时任务的核心属性有两个:delayQueue和timingWheel。TimingWheel表示单个时间轮,接下来我们来看看其类图:

       各属性含义如下:

       3.3 算法

       3.3.1 添加任务

       添加任务的入口是DelayedOperationPurgatory.tryCompleteElseWatch,其核心逻辑分为如下两步:

       SystemTimer.add直接调用了addTimerTaskEntry方法,后者逻辑如下:

       TimingWheel.add的逻辑也很清晰,分如下4种场景处理:

       3.3.2 尝试提前触发任务

       入口是DelayedOperationPurgatory.checkAndComplete:

       接下来看Watchers.tryCompleteWatched方法的内容:

       DelayedOperation.maybeTryComplete方法最终调用了DelayedOperation.tryComplete;

       DelayedOperation的子类需要在后者中实现自己的"触发条件"检查逻辑;若满足了提前触发的条件,则调用forceComplete方法执行事件触发场景下的业务逻辑。

       3.3.3 任务到期自动执行

       DelayedOperationPurgatory中维护了一个expirationReaper线程,其职责就是循环调用kafka.utils.timer.SystemTimer#advanceClock来从时间轮中获取已超时的任务,并更新时间轮的"当前时间"指针。

       四. 总结

       才疏学浅,未能窥其十之一二,随时欢迎各位交流补充。若文章质量还算及格,可以点赞收藏加以鼓励,后续我继续更新。

       另外,也可以在目录中找到同系列的其他文章:

       感谢阅读。

死磕 java集合之ConcurrentLinkedQueue源码分析

       ConcurrentLinkedQueue

       (1)不是阻塞队列

       (2)通过CAS+自旋保证并发安全

       (3)可用于多线程环境,但不能用在线程池中

       简介

       主要属性

       两个属性:头节点与尾节点

       主要内部类

       典型单链表结构

       主要构造方法

       构造简单,实现无界单链表队列

       入队

       add(e)与offer(e)方法

       无异常抛出,流程清晰

       出队

       remove()与poll()方法

       逻辑清晰,不阻塞线程

       总结

       非阻塞队列,不适用于线程池

       彩蛋

       与LinkedBlockingQueue对比

       线程安全与返回null特性相似

       效率与锁机制差异显著

       无法实现等待元素与用在线程池中的限制

OpenJDK-JVM 源码阅读 - ZGC - 并发标记 | 京东物流技术团队

       ZGC简介:

       ZGC是Java垃圾回收器的前沿技术,支持低延迟、大容量堆、染色指针、读屏障等特性,自JDK起作为试验特性,JDK起支持Windows,JDK正式投入生产使用。在JDK中已实现分代收集,预计不久将发布,性能将更优秀。

       ZGC特征:

       1. 低延迟

       2. 大容量堆

       3. 染色指针

       4. 读屏障

       并发标记过程:

       ZGC并发标记主要分为三个阶段:初始标记、并发标记/重映射、重分配。本篇主要分析并发标记/重映射部分源代码。

       入口与并发标记:

       整个ZGC源码入口是ZDriver::gc函数,其中concurrent()是一个宏定义。并发标记函数是concurrent_mark。

       并发标记流程:

       从ZHeap::heap()进入mark函数,使用任务框架执行任务逻辑在ZMarkTask里,具体执行函数是work。工作逻辑循环从标记条带中取出数据,直到取完或时间到。此循环即为ZGC三色标记主循环。之后进入drain函数,从栈中取出指针进行标记,直到栈排空。标记过程包括从栈取数据,标记和递归标记。

       标记与迭代:

       标记过程涉及对象迭代遍历。标记流程中,ZGC通过map存储对象地址的finalizable和inc_live信息。map大小约为堆中对象对齐大小的二分之一。接着通过oop_iterate函数对对象中的指针进行迭代,使用ZMarkBarrierOopClosure作为读屏障,实现了指针自愈和防止漏标。

       读屏障细节:

       ZMarkBarrierOopClosure函数在标记非静态成员变量的指针时触发读屏障。慢路径处理和指针自愈是核心逻辑,慢路径标记指针,快速路径通过cas操作修复坏指针,并重新标记。

       重映射过程:

       读屏障触发标记后,对象被推入栈中,下次标记循环时取出。ZGC并发标记流程至此结束。

       问题回顾:

       本文解答了ZGC如何标记指针、三色标记过程、如何防止漏标、指针自愈和并发重映射过程的问题。

       扩展思考:

       ZGC在指针上标记,当回收某个region时,如何得知对象是否存活?答案需要结合标记阶段和重分配阶段的代码。

       结束语:

       本文深入分析了ZGC并发标记的源码细节,对您有启发或帮助的话,请多多点赞支持。作者:京东物流 刘家存,来源:京东云开发者社区 自猿其说 Tech。转载请注明来源。