Java 与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
垃圾收集(GC)需要完成的三件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
Java 内存运行时区域中程序计数器****、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出 栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,在这几个区域 内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着 回收了。
Java 堆和方法区这两个区域则有着很显著的不确定性, 一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样, 只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分 内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,本文后 续讨论中的“内存”分配与回收也仅仅特指这一部分内存。
这里方法区和虚拟机栈垃圾回收处理策略不同在于,虚拟机栈中方法执行完毕会自己出栈,而方法区中的类信息需要垃圾回收机制来回收,二者生命周期不同,虚拟机栈在线程结束是就会被销毁,不需要额外的垃圾回收,方法区在JVM创建时创建,关闭时销毁
在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前, 第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”,下面是几种判断对象是否存活的算法
在对象中添加一个引用计数器,每 当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的
引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来 进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是,在 Java 领域,至少主流的 Java 虚拟机里面都没有选用 引用计数算法来管理内存,这个看似简单的算法有很多例外情况要考虑, 必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
这个算法的基本思路就是**通过一系列称为“**GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是 从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:
- 在虚拟机****栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法 堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。 ·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象 (比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
除了这些固定对象,根据用户的选择不同的垃圾回收器以及当前回收的内存区域,还会有临时性其他对象的加入,构成完整的GC Roots对象集合
譬如后文将会提到的分代收集和局部回收,如果只针对 Java 堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机****自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入 GC Roots 集合中去,才能保证可达性分析的正确性。
上面的两种判断算法,都需要通过引用判断对象是否存活,引用技术算法需要判断对象的引用数量,可达性算法需要判断对象的引用链是否可达,这里就需要具体处理引用的定义,在JDK 1.2 版之前,Java 里面的引用是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 reference 数据是代表某块内存、某个对象的引用。
这种情况下,一个对象只有被引用和不被引用俩种状态,对一些对象的描述就有些乏力,比如一些对象我们需要在内存空间充足的情况下保留,内存紧张的时候回收,很多系统的缓存功能都符合这样的应用场景。
JDK 1.2 版之后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和****虚引用4 种,这 4 种引用强度依次逐渐减弱。
- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类 似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在, 垃圾收集器****就永远不会回收掉被引用的对象。
- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在 系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果 这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 版之后提供了 SoftReference 类来实现软引用。
- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,**无论当前内存是否足够,都会回收掉只被弱引用关联的对象。**在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引用。
- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时 收到一个系统通知。在 JDK 1.2 版之后提供了 PhantomReference 类来实现虚引用。
finalize**()能做的所有工作,使用 try-finally 或者其他方式都可以做得更 好、更及时,所以笔者建议大家完全可以忘掉 Java 语言里面的这个方法。**
在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对 象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,
随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize**()方法**。假如对象没有覆盖 finalize()方法,或者 finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为确有必要执行 finalize()方法,那么该对象将会被放置在一个 名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。(执行是会开始,但是不会等他结束)。finalize()方法是对象逃 脱死亡命运的最后一次机会,稍后收集器将对 F-Queue 中的对象进行第二次小规模的标 记,如果对象要在 finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建 立关联即可
任何一个对象的 finalize()方法都只会被系统自动 调用一次,如果对象面临下一次回收,它的 finalize()方法不会被再次执行
有人认为方法区是没有垃圾收集行为的,《Java 虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集
也确实有虚拟机没有回收方法区,方法区垃圾收集的“性价比”通常也是比较低的
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型
垃圾收集算法可以划分为**“引用计数式垃圾收集” 和“追踪式垃圾收集”两大类,这两类也常被 称作“直接垃圾收集”和“间接垃圾收集”,**下面介绍的算法都属于追踪式垃圾收集
分代收集名为理论,实质是一套符合大多数程序运行实 际情况的经验法则,它建立在两个分代假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就 越难以消亡。
俩个假说确定了垃圾收集器的一致设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
在 Java 堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域,也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。
分代收集理论具体放到现在Java 虚拟机里,设计者一般至少会把 Java 堆 划分为新生代和老年代两个区域,顾名思义, 在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象, 将会逐步晋升到老年代中存放。
分代收集并非只是简单划分 一下内存区域那么容易,它至少存在一个明显的困难:**对象不是孤立的,对象之间会存 跨代引用。这种情况下回收新生代中的对象,判断存活不光需要判断它的GC roots集合,还需要判断老年代中的数据,**为了解决这个问题,就需要对分代收集理论添加第三条经验法则:
- 跨代引用假说:跨代引用相对于同代引 用来说仅占极少数。
这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对 象,是应该倾向于同时生存或者同时消亡的。
我们就不应再为了少量的跨代引用去扫描整个老年代,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有 包含了跨代引用的小块内存里的对象才会被加入到 GCRoots 进行扫描。虽然这种方法需 要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增 加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
最早出现也是最基础的垃圾收集算法是**“标记-清除”算法,这个算法有俩个阶段,标记和清除,**首先标记出所有的需要的回收的对象,标记完成后同一回收所有被标记的对象,或者反过来标记存活的对象,然后回收未被标记的对象,**这个标记过程就是对垃圾的判断过程,**就是前面对象已死中提到的内容
后面的算法都是在这个基础上针对缺点进行了更改,标记清除算法主要有俩个缺点,
- 执行效率不稳定:如果由java堆中有大量对象需要进行回收,这时候就需要大量进行标记和清除的动作,俩个动作的执行效率都会随对象的增长而降低,
- **内存空间碎片化:**标记清除会产生大量碎片化的内存碎片,然后碎片太多可能导致在后续内存分配的时候一些比较大的对象分配时无法找到足够的连续内存
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,标记复制算法应运而生,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。**当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。**如果内存中多数对象都是存活的,这种算法将会 产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
Andrew Appel 针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel 式回收”Appel 式回收的具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,**每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾搜集时,将 Eden 和 Survivor 中仍然存活的对象一 次性复制到 另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。**HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8∶1,也即每次新生代中可用内存空间为整个新生代容量的 90%(Eden 的 80%加上一个 Survivor 的 10%),只有一 个 Survivor 空间,即 10%的新生代是会被“浪费”的。当然,98%的对象可被回收仅仅是 “普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于 10%的 对象存活,因此 Appel 式回收还有一个充当罕见情况的“逃生门”的安全设计,当 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时,就需要依赖其他内存区域 (实际上大多就是老年代)进行分配担保(Handle Promotion)。
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更 关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使 用的内存中所有对象都 100%存活的极端情况,所以在老年代一般不能直接选用这种算 法。
标记-整理算法,其中的标记过程仍然与“标记-清除”算法一样,但 后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移 动,然后直接清理掉边界以外的内存
工作原理
标记-整理算法也分为两个主要阶段:标记阶段和整理阶段。
标记阶段:
- 从GC Root集合开始,遍历对象引用图,标记所有可达的对象。
- 这一步与标记-清除算法中的标记阶段相同,标记过程是递归的,沿着对象引用链进行,直到所有可达的对象都被标记。
整理阶段:
- 遍历整个堆,将所有存活的对象向一端移动(通常是堆的起始位置),保持对象之间的紧密排列。
- 更新所有对象的引用,以反映它们的新位置。
- 移动完成后,释放未被标记对象的内存,未被标记的对象被回收,形成一块连续的空闲区域。
优点
- 无内存碎片:对象被紧密排列在一起,没有内存碎片,提高了内存利用率。
- 高效的内存分配:由于所有存活对象被移动到堆的一端,剩下的内存是连续的,内存分配速度更快。
- 适用于长生命周期对象:尤其适合老年代(Old Generation)的垃圾回收,因为老年代对象生命周期较长,不需要频繁移动。
-
HotSpot 的算法细节实现
-
根节点枚举
(为了提供元素回收时需要的快照)
这里首先是可达性算法中的****GC Roots引用链处理,java内存中的数据是非常多的,哪怕的方法区中的类或者常量数量都是多到以亿无法计量,
迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,现在可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须在一个能保障一致性的快照中才得以 进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,枚举根节点时也是必须要停顿的。
目前主流 Java 虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。
在 HotSpot 的解决方案里,是使用一组称为 OopMap 的数据结构来达到这个目的。一旦类加载动作完成的时候, HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等 GC Roots 开始查找。
(前面提到过,在根节点的时候需要停下所有线程,这里是为了解决如何停顿用户线程)
在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots 枚举,但是在这种情况下,引用关系发生变化,即改变OopMap集合的操作特别多,每一个指令都会生成对应的OopMap,会额外占据大量额外空间,实际上HotSpot没有为每一个指令生成OopMap,前面提到了在特定位置记录信息,这些位置称为安全点****,安全点是一种特殊的执行点,在这个点上,垃圾回收器可以安全地中断程序执行,开始进行垃圾回收操作。
安全点是程序执行过程中的特定位置,这些位置被选定是因为在这个点上,所有的线程都处于稳定状态,即它们不会引用新的对象或修改现有的对象引用。
有了安全点的设定, 也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收 集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定既不能太少 以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。
对于安全点,另外一个需要考虑的问题是,如何在垃圾收集****发生时让所有线程(这 里其实不包括执行 JNI 调用的线程)都跑到最近的安全点,然后停顿下来。这里有两种 方案可供选择:抢先式中断和主动式中断
- 抢先式中断:在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。(几乎没有虚拟机使用这种方式)
- 主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现 中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的
(这里是在非执行状态的用户线程停顿问题)
使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了
程序在执行的时候可以遇到安全点进行垃圾回收,在程序不执行(没有分配处理器时间)的时候,要如何处理,也就是线程处于Sleep或者Blocked状态,这种情况下线程无法相应虚拟机的中断需求,也无法到达安全点进行中断自己,这种情况需要安全区域来处理,
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在 这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸 了的安全点。
当线程运行到安全区域时,会标示自己进入了安全区域,在虚拟机进行垃圾回收的时候就不用额外关注处于安全区域的线程,当线程离开安全区域的时候,它会检查虚拟机是否完成了对Roots的枚举,如果完成就正常进行执行,如果没有就等待,等待到它可以离开为止
(直接扫描整个Roots集合太费时,这里通过记忆集来缩减扫描范围)
讲解分代收集理论的时候,提到了为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集的数据结构,为了比避免扫描整个老年代,其实不止在新生代和老年代之间会有这种跨代问题出现,在涉及部分区域收集的时候也会出现这种问题,
记忆集是一种记录从非收集区指向收集区的指针集合的抽象数据结构,
在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:
- 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的 32 位或 64 位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
卡表就是记忆集的一种具体实现, 它定义了记忆集的记录精度、与堆内存的映射关系等。
卡表最简单的形式可以只是一个字节数组,而 HotSpot 虚拟机确实也是这样做的。
字节数组的每一个元素对应的内存区域都有一块特定大小的内存块,这个内存块叫做卡页,
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字 段存在着跨代指针,那就将对应卡表的数组元素的值标识为 1,称为这个元素变脏 (Dirty),没有则标识为 0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能 轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描。
卡表元素是需要维护的,例如它们何时变脏、谁来把它们变脏等。
卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象 时,其对应的卡表元素就应该变脏,这里的问题是如何使表中元素变脏,在解释字节的时候有足够的介入空间,但是在编译的时候,字节码已经变成了机器指令流,这时候一个如何维护这个表,
在 HotSpot 虚拟机里是通过写屏障技术维护卡表状态的。
写屏障可以看作在虚拟机层面对 “引用类型字段赋值”这个动作的 AOP 切面,在引用对象赋值时会产生一个环形 通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴 内。在赋值前的部分的写屏障叫作写前屏障,在赋值后的则叫作写 后屏障
应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障 中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与 Minor GC 时扫描整个老年代的代 价相比还是低得多的。
除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”问题,除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才 将其标记为变脏
在可达性分析中,要求比喻冻结全部的用户线程,这里的GCRoots数量还是比较小的,他在优化技巧下,带来的停顿还是比较小的,但是他继续向下遍历对象图,对象数量可能会很多,这种情况下停顿时间就会更长,现在需要想办法降低这部分停顿
为了能解释清楚这个问题,我们引入三色标记作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访 问过”这个条件标记成以下三种颜色:
- 白色:表示对象**尚未被垃圾收集器访问过****。**显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描 过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对 象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
这里可能出现两种情况,本来应该删除的节点因为引用关系的改变导致没有删除继续存活,或者本来应该存活的节点因为引用关系的改变导致删除,第一种情况不过是在下一次检查的时候删除问题不大,但是第二种情况的问题非常严重,正常不应该出现这种情况
只有在下面来个条件都满足的时候才会导致**”节点消失“
**(节点本来应该黑色但是标成白色)
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了 两种解决方案:增量更新和原始快照。
- 增量更新:当黑色对象插入新的指向白色对象的引用关系 时,就将这个新插入的引用记录下来,等并发扫描结束之后**,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。**
- 原始快照:当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这里的插入和删除操作,虚拟机的记录都是通过写屏障完成的
总结
首先进行垃圾回收要通过可达性算法判断对象是否存活,同时垃圾回收要求在根节点枚举的时候暂停所有的用户线程(打扫房间的时候不能还有人在丢垃圾,其他人应该乖乖等着),**虚拟机需要快速确定对象的引用关系并且在必要时候为对象提供快照(这里使用了OopMap 实现),执行中的线程会在安全点中统一暂停,处于暂停中的线程会在安全区域中完成垃圾回收。**这里线程如何停顿已经解决了。
下面的问题是对象一旦跨代引用或者跨区引用,我们如果没有特殊方法可能需要将整个内存区域扫描,前面分代收集理论的时候也有提到过,记忆集的出现避免了扫描整个老年代,他的使用不光是在新生代,老年代中,在其他跨区域引用也可以使用,记忆集的精度如果太大可能导致占用太多内存,所以记忆集可以选择不同的精度,记录到一块内存区域精度的记忆集叫做卡表,卡表中记录的跨带的引用关系,卡表的出现就需要维护,在其他分代引用了本区域对象时,就需要使对应的卡表元素改变,这个改变的操作使用了写屏障,
最后是并发问题,在处理对象的回收时并发改变引用状态,可能导致本不应该回收的对象被回收,这里使用增量更新和原始快照的方法处理了这个问题