JVM之内存管理篇

二、Java内存区域

2.1运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

 

2.1.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

2.1.2Java虚拟机栈

每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态连接方法出口等信息。

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

2.1.3本地方法栈

本地方法栈是为虚拟机使用到的本地(Native)方法服务。

2.1.4Java堆

Java堆是垃圾收集器管理的内存区域,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。

2.1.5方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

2.1.6运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。

2.1.7直接内存

直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分。在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。

2.2HotSpot虚拟机对象

2.2.1对象的创建

当Java虚拟机遇到一条字节码new指令时,经过类加载检查后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定。

指针碰撞(Bump The Pointer):假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。

空闲列表(Free List):如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

在并发情况下,对象创建也并不是线程安全的。解决这个问题有两种可选方案:

  1. 对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;
  2. 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

2.2.2对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称为“MarkWord”。结构如下:

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标志
偏向线程ID、偏向时间戳、对象分代年龄01可偏向

另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

2.2.3对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象,主流的访问方式主要有使用句柄和直接指针两种:

句柄访问:Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,如下图所示。

 直接指针访问:Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如下图所示。

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销。对于HotSpot虚拟机而言,它主要使用直接指针进行对象访问。

三、垃圾收集

3.1对象判活算法

3.1.1引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

3.1.2可达性分析算法

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

在Java技术体系里面,固定可作为GCRoots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

3.1.3引用

引用分为强引用(Strongly Re-ference)软引用(Soft Reference)、**弱引用(Weak Reference)虚引用(Phantom Reference)**4种,这4种引用强度依次逐渐减弱。

  1. 强引用:最传统的“引用”的定义,类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  2. 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。
  3. 弱引用:用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
  4. 虚引用:也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

3.1.4生存还是死亡

真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

任何一个对象的finalize()方法都只会被系统自动调用一次。

3.1.5回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。

判定一个类型是否属于“不再被使用的类”,需要同时满足下面三个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.2垃圾收集算法

3.2.1分代收集理论

弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。

强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

在新生代上建立一个全局的数据结构(记忆集,RememberedSet),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。

3.2.2标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。存在执行效率不稳定和内存空间的碎片化问题。

3.2.3标记-复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。其中,Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。**HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1。**当Survivor空间不足以容纳一次MinorGC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(HandlePromotion)。

3.2.4标记-整理算法

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

3.3HotSpot的垃圾收集算法

3.3.1根节点枚举

根节点枚举必须在一个能保障一致性的快照中才得以进行,即整个枚举期间执行子系统看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化的情况。

当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。

3.3.2安全点

如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间,实际上HotSpot也的确没有为每条指令都生成OopMap。

HotSpot在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint)。用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。

如何在垃圾收集发生时让所有线程(这里不包括执行JNI调用的线程)都跑到最近的安全点:

抢先式中断:在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机采用这种方式。

主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志(标志与安全点重合),一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。

HotSpot使用内存保护陷阱的方式,当需要暂停用户线程时,虚拟机把0x160100的内存页设置为不可读,那线程执行到test指令时就会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起线程实现等待。

0x01b6d627:call0x01b2b210                  ;OopMap{[60]=Oopoff=460}
                                        ;*invokeinterfacesize
                                        ;-Client1::main@113(line23)
                                        ;{virtual_call}
0x01b6d62c:nop                             ;OopMap{[60]=Oopoff=461}
                                        ;*if_icmplt
                                        ;-Client1::main@118(line23)
0x01b6d62d:test    %eax,0x160100               ;{poll}
0x01b6d633:mov    0x50(%esp),%esi
0x01b6d637:cmp    %eax,%esi

3.3.3安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。

3.3.4记忆集与卡表

垃圾收集器在新生代中建立了名为**记忆集(Remembered Set)**的数据结构,用以避免把整个老年代加进GCRoots扫描范围。

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度:字长精度、对象精度、卡精度

“卡精度”所指的是用一种称为“卡表”(CardTable)的方式去实现记忆集。卡表最简单的形式可以只是一个字节数组。字节数组的每一个元素都对应着其标识的内存区域中一块特定大小的内存块(卡页,Card Page),卡页大小都是以2的N次幂的字节数。

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GCRoots中一并扫描。

3.3.5写屏障

HotSpot虚拟机是通过**写屏障(WriteBarrier)技术维护卡表状态的,写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知**,供程序执行额外的动作,也就是说赋值的 前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)

卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

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

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

3.3.6并发的可达性分析

三色标记(Tri-color Marking):把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:

  1. 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  2. 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  3. 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

并发出现“对象消失”问题

当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:

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

根据对象消失问题的产生条件,分别有两种解决方案:**增量更新(Incremental Update)原始快照(Snapshot At The Beginning,SATB)**。

增量更新:当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

原始快照:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。

3.4经典垃圾收集器

3.4.1Serial收集器

这个收集器是一个单线程工作的收集器,进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。HotSpot虚拟机运行在客户端模式下的新生代收集器默认使用Serial收集器。

3.4.2ParNew收集器

ParNew收集器现已经并入CMS收集器,作为其在新生代区的收集器,采用的是标记-复制算法,同时采用多条线程进行垃圾收集,与Serial的区别在于可以多线程并行。

3.4.3Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,基于标记-复制算法实现的收集器,能够并行收集的多线程收集器,目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即 $$ 吞吐量=运行用户代码时间/运行用户代码时间+运行垃圾收集时间 $$ Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。这也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。

3.4.4Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

3.4.5ParallelOld收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。在注重 吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

3.4.6CMS收集器

CMS收集器是基于标记-清除算法实现的,它的运作过程分为四个步骤,包括:

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

初始标记只是标记一下GCRoots能直接关联到的对象,速度很快;并发标记阶段就是从GCRoots的直接关联对象开始遍历整个对象图的过程,重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

 

3.4.7GarbageFirst收集器

G1开创的基于Region的堆内存布局,把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。

Region里面存在的跨Region引用对象,G1收集器使用记忆集避免全堆作为GCRoots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种*“双向”的卡表结构*(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。

并发标记阶段,G1收集器则是通过原始快照(SATB)算法来实现的。通过为每一个Region设计了两个名为**TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上**。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。

G1收集器的运作过程大致可划分为以下四个步骤:

  1. 初始标记(Initial Marking):仅仅只是标记一下GCRoots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
  2. 并发标记(Concurrent Marking):从GCRoot开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  3. 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

3.5低延迟垃圾收集器

3.5.1 Shenandoah收集器

Shenandoah也是使用基于Region的堆内存布局,同样有着用于存放大对象的Humongous Region,默认的回收策略也同样是优先处理回收价值最大的Region ,摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为*“连接矩阵”(Connection Matrix)* 的全局数据结构来记录跨Region的引用关系。连接矩阵可以简单理解为一张二维表格, 如果Region N有对象指向Region M, 就在表格的N行M列中打上一个标记。

  1. 初始标记(Initial Marking): 与G1一样, 首先标记与GC Roots直接关联的对象, 这个阶段仍是“Stop The World”的, 但停顿时间与堆大小无关, 只与GC Roots的数量相关。
  2. 并发标记(Concurrent Marking):与G1一样, 遍历对象图, 标记出全部可达的对象, 这个阶段是与用户线程一起并发的, 时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
  3. 最终标记(Final Marking):与G1一样, 处理剩余的SATB扫描, 并在这个阶段统计出回收价值最高的Region, 将这些Region构成一组回收集(Collection Set) 。 最终标记阶段也会有一小段短暂的停顿。
  4. 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region) 。
  5. 并发回收(Concurrent Evacuation): 并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。 在这个阶段, Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。
  6. 初始引用更新(initial Update Reference):并发回收阶段复制对象结束后, 还需要把堆中所有指向旧对象的引用修正到复制后的新地址, 这个操作称为引用更新。 通过读屏障和被称为“Brooks Pointers”的转发指针来解决。
  7. 并发引用更新(Concurrent Update Reference): 真正开始进行引用更新操作, 这个阶段是与用户线程一起并发的, 时间长短取决于内存中涉及的引用数量的多少。 并发引用更新与并发标记不同, 它不再需要沿着对象图来搜索, 只需要按照内存物理地址的顺序, 线性地搜索出引用类型, 把旧值改为新值即可。
  8. 最终引用更新(Final Update Reference): 解决了堆中的引用更新后, 还要修正存在于GC Roots中的引用。 这个阶段是Shenandoah的最后一次停顿, 停顿时间只与GC Roots的数量相关。
  9. 并发清理(Concurrent Cleanup): 经过并发回收和引用更新之后, 整个回收集中所有的Region已再无存活对象, 这些Region都变成Immediate Garbage Regions了, 最后再调用一次并发清理过程来回收这些Region的内存空间, 供以后新对象分配使用。

转发指针(Forwarding Pointer): 要做类似的并发操作, 通常是在被移动对象原有的内存上设置保护陷阱(MemoryProtection Trap) , 一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中段, 进入预设好的异常处理器中, 再由其中的代码逻辑把访问转发到复制后的新对象上。 虽然确实能够实现对象移动与用户线程并发, 但是如果没有操作系统层面的直接支持, 这种方案将导致用户态频繁切换到核心态,代价是非常大的, 不能频繁使用。

Brooks Pointers:在原有对象布局结构的最前面统一增加一个新的引用字段, 在正常不处于并发移动的情况下, 该引用指向对象自己。转发指针加入后带来的收益自然是当对象拥有了一份新的副本时, 只需要修改一处指针的值, 即旧对象上转发指针的引用位置, 使其指向新对象, 便可将所有对该对象的访问转发到新的副本上。

显然 Brooks Pointers会带来多线程并发竞争的问题,Shenandoah收集器是通过比较并交换(Compare And Swap,CAS) 操作来保证并发时对象的访问正确性的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值