java的内存分配和回收机制

news2024/11/15 15:25:58

Java 与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

  1. 概述

垃圾收集(GC)需要完成的三件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

Java 内存运行时区域中程序计数器****、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出 栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,在这几个区域 内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着 回收了。

Java 堆和方法区这两个区域则有着很显著的不确定性, 一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样, 只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分 内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,本文后 续讨论中的“内存”分配与回收也仅仅特指这一部分内存。

这里方法区和虚拟机栈垃圾回收处理策略不同在于,虚拟机栈中方法执行完毕会自己出栈,而方法区中的类信息需要垃圾回收机制来回收,二者生命周期不同,虚拟机栈在线程结束是就会被销毁,不需要额外的垃圾回收,方法区在JVM创建时创建,关闭时销毁

  1. 对象已死

在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前, 第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”,下面是几种判断对象是否存活的算法

  1. 引用计数算法

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

引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来 进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。但是,在 Java 领域,至少主流的 Java 虚拟机里面都没有选用 引用计数算法来管理内存,这个看似简单的算法有很多例外情况要考虑, 必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

  1. 可达性分析算法

当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(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 集合中去,才能保证可达性分析的正确性。

  1. 再谈引用

上面的两种判断算法,都需要通过引用判断对象是否存活,引用技术算法需要判断对象的引用数量,可达性算法需要判断对象的引用链是否可达,这里就需要具体处理引用的定义,在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 类来实现虚引用。
  1. 生存还是死亡?

finalize**()能做的所有工作,使用 try-finally 或者其他方式都可以做得更 好、更及时,所以笔者建议大家完全可以忘掉 Java 语言里面的这个方法。**

在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

如果对 象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,

随后进行一次筛选,筛选的条件是此对象是否有必要执行 finalize**()方法**。假如对象没有覆盖 finalize()方法,或者 finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为确有必要执行 finalize()方法,那么该对象将会被放置在一个 名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。(执行是会开始,但是不会等他结束)。finalize()方法是对象逃 脱死亡命运的最后一次机会,稍后收集器将对 F-Queue 中的对象进行第二次小规模的标 记,如果对象要在 finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建 立关联即可

任何一个对象的 finalize()方法都只会被系统自动 调用一次,如果对象面临下一次回收,它的 finalize()方法不会被再次执行

  1. 回收方法区

有人认为方法区是没有垃圾收集行为的,《Java 虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集

也确实有虚拟机没有回收方法区,方法区垃圾收集的“性价比”通常也是比较低的

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

  1. 垃圾收集算法

垃圾收集算法可以划分为**“引用计数式垃圾收集” 和“追踪式垃圾收集”两大类,这两类也常被 称作“直接垃圾收集”和“间接垃圾收集”,**下面介绍的算法都属于追踪式垃圾收集

  1. 分代收集理论

分代收集名为理论,实质是一套符合大多数程序运行实 际情况的经验法则,它建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾收集过程的对象就 越难以消亡。

俩个假说确定了垃圾收集器的一致设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

在 Java 堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域,也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。

分代收集理论具体放到现在Java 虚拟机里,设计者一般至少会把 Java 堆 划分为新生代和老年代两个区域,顾名思义, 在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象, 将会逐步晋升到老年代中存放。

分代收集并非只是简单划分 一下内存区域那么容易,它至少存在一个明显的困难:**对象不是孤立的,对象之间会存 跨代引用。这种情况下回收新生代中的对象,判断存活不光需要判断它的GC roots集合,还需要判断老年代中的数据,**为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

  • 跨代引用假说:跨代引用相对于同代引 用来说仅占极少数。

这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对 象,是应该倾向于同时生存或者同时消亡的。

我们就不应再为了少量的跨代引用去扫描整个老年代,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有 包含了跨代引用的小块内存里的对象才会被加入到 GCRoots 进行扫描。虽然这种方法需 要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增 加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

  1. 标记—清除算法

最早出现也是最基础的垃圾收集算法是**“标记-清除”算法,这个算法有俩个阶段,标记和清除,**首先标记出所有的需要的回收的对象,标记完成后同一回收所有被标记的对象,或者反过来标记存活的对象,然后回收未被标记的对象,**这个标记过程就是对垃圾的判断过程,**就是前面对象已死中提到的内容

后面的算法都是在这个基础上针对缺点进行了更改,标记清除算法主要有俩个缺点,

  • 执行效率不稳定:如果由java堆中有大量对象需要进行回收,这时候就需要大量进行标记和清除的动作,俩个动作的执行效率都会随对象的增长而降低,
  • **内存空间碎片化:**标记清除会产生大量碎片化的内存碎片,然后碎片太多可能导致在后续内存分配的时候一些比较大的对象分配时无法找到足够的连续内存
  1. 标记-复制算法

为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,标记复制算法应运而生,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。**当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。**如果内存中多数对象都是存活的,这种算法将会 产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。

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)。

  1. 标记-整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更 关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使 用的内存中所有对象都 100%存活的极端情况,所以在老年代一般不能直接选用这种算 法。

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

工作原理

标记-整理算法也分为两个主要阶段:标记阶段和整理阶段。

标记阶段:

  • 从GC Root集合开始,遍历对象引用图,标记所有可达的对象。
  • 这一步与标记-清除算法中的标记阶段相同,标记过程是递归的,沿着对象引用链进行,直到所有可达的对象都被标记。

整理阶段:

  • 遍历整个堆,将所有存活的对象向一端移动(通常是堆的起始位置),保持对象之间的紧密排列。
  • 更新所有对象的引用,以反映它们的新位置。
  • 移动完成后,释放未被标记对象的内存,未被标记的对象被回收,形成一块连续的空闲区域。

在这里插入图片描述

优点

  • 无内存碎片:对象被紧密排列在一起,没有内存碎片,提高了内存利用率。
  • 高效的内存分配:由于所有存活对象被移动到堆的一端,剩下的内存是连续的,内存分配速度更快。
  • 适用于长生命周期对象:尤其适合老年代(Old Generation)的垃圾回收,因为老年代对象生命周期较长,不需要频繁移动。
  1. HotSpot 的算法细节实现

  2. 根节点枚举

(为了提供元素回收时需要的快照)

这里首先是可达性算法中的****GC Roots引用链处理,java内存中的数据是非常多的,哪怕的方法区中的类或者常量数量都是多到以亿无法计量,

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,现在可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须在一个能保障一致性的快照中才得以 进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上,枚举根节点时也是必须要停顿的。

目前主流 Java 虚拟机使用的都是准确式垃圾收集,所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。

在 HotSpot 的解决方案里,是使用一组称为 OopMap 的数据结构来达到这个目的。一旦类加载动作完成的时候, HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等 GC Roots 开始查找。

  1. 安全点

(前面提到过,在根节点的时候需要停下所有线程,这里是为了解决如何停顿用户线程)

在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots 枚举,但是在这种情况下,引用关系发生变化,即改变OopMap集合的操作特别多,每一个指令都会生成对应的OopMap,会额外占据大量额外空间,实际上HotSpot没有为每一个指令生成OopMap,前面提到了在特定位置记录信息,这些位置称为安全点****,安全点是一种特殊的执行点,在这个点上,垃圾回收器可以安全地中断程序执行,开始进行垃圾回收操作。

安全点是程序执行过程中的特定位置,这些位置被选定是因为在这个点上,所有的线程都处于稳定状态,即它们不会引用新的对象或修改现有的对象引用。

有了安全点的设定, 也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收 集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定既不能太少 以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。

对于安全点,另外一个需要考虑的问题是,如何在垃圾收集****发生时让所有线程(这 里其实不包括执行 JNI 调用的线程)都跑到最近的安全点,然后停顿下来。这里有两种 方案可供选择:抢先式中断和主动式中断

  • 抢先式中断:在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。(几乎没有虚拟机使用这种方式)
  • 主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现 中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的
  1. 安全区域

(这里是在非执行状态的用户线程停顿问题)

使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了

程序在执行的时候可以遇到安全点进行垃圾回收,在程序不执行(没有分配处理器时间)的时候,要如何处理,也就是线程处于Sleep或者Blocked状态,这种情况下线程无法相应虚拟机的中断需求,也无法到达安全点进行中断自己,这种情况需要安全区域来处理,

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在 这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸 了的安全点。

当线程运行到安全区域时,会标示自己进入了安全区域,在虚拟机进行垃圾回收的时候就不用额外关注处于安全区域的线程,当线程离开安全区域的时候,它会检查虚拟机是否完成了对Roots的枚举,如果完成就正常进行执行,如果没有就等待,等待到它可以离开为止

  1. 记忆集与卡表

(直接扫描整个Roots集合太费时,这里通过记忆集来缩减扫描范围)

讲解分代收集理论的时候,提到了为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集的数据结构,为了比避免扫描整个老年代,其实不止在新生代和老年代之间会有这种跨代问题出现,在涉及部分区域收集的时候也会出现这种问题,

记忆集是一种记录从非收集区指向收集区的指针集合的抽象数据结构,

在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的 32 位或 64 位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

卡表就是记忆集的一种具体实现, 它定义了记忆集的记录精度、与堆内存的映射关系等。

卡表最简单的形式可以只是一个字节数组,而 HotSpot 虚拟机确实也是这样做的。

字节数组的每一个元素对应的内存区域都有一块特定大小的内存块,这个内存块叫做卡页,

在这里插入图片描述

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

  1. 写屏障

卡表元素是需要维护的,例如它们何时变脏、谁来把它们变脏等。

卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象 时,其对应的卡表元素就应该变脏,这里的问题是如何使表中元素变脏,在解释字节的时候有足够的介入空间,但是在编译的时候,字节码已经变成了机器指令流,这时候一个如何维护这个表,

在 HotSpot 虚拟机里是通过写屏障技术维护卡表状态的。

写屏障可以看作在虚拟机层面对 “引用类型字段赋值”这个动作的 AOP 切面,在引用对象赋值时会产生一个环形 通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴 内。在赋值前的部分的写屏障叫作写前屏障,在赋值后的则叫作写 后屏障

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

除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”问题,除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才 将其标记为变脏

  1. 并发的可达性分析

在可达性分析中,要求比喻冻结全部的用户线程,这里的GCRoots数量还是比较小的,他在优化技巧下,带来的停顿还是比较小的,但是他继续向下遍历对象图,对象数量可能会很多,这种情况下停顿时间就会更长,现在需要想办法降低这部分停顿

为了能解释清楚这个问题,我们引入三色标记作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照“是否访 问过”这个条件标记成以下三种颜色:

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

在这里插入图片描述

这里可能出现两种情况,本来应该删除的节点因为引用关系的改变导致没有删除继续存活,或者本来应该存活的节点因为引用关系的改变导致删除,第一种情况不过是在下一次检查的时候删除问题不大,但是第二种情况的问题非常严重,正常不应该出现这种情况

只有在下面来个条件都满足的时候才会导致**”节点消失“**(节点本来应该黑色但是标成白色)

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别产生了 两种解决方案:增量更新和原始快照。
  • 增量更新:当黑色对象插入新的指向白色对象的引用关系 时,就将这个新插入的引用记录下来,等并发扫描结束之后**,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。**
  • 原始快照:当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次

这里的插入和删除操作,虚拟机的记录都是通过写屏障完成的

总结

首先进行垃圾回收要通过可达性算法判断对象是否存活,同时垃圾回收要求在根节点枚举的时候暂停所有的用户线程(打扫房间的时候不能还有人在丢垃圾,其他人应该乖乖等着),**虚拟机需要快速确定对象的引用关系并且在必要时候为对象提供快照(这里使用了OopMap 实现),执行中的线程会在安全点中统一暂停,处于暂停中的线程会在安全区域中完成垃圾回收。**这里线程如何停顿已经解决了。

下面的问题是对象一旦跨代引用或者跨区引用,我们如果没有特殊方法可能需要将整个内存区域扫描,前面分代收集理论的时候也有提到过,记忆集的出现避免了扫描整个老年代,他的使用不光是在新生代,老年代中,在其他跨区域引用也可以使用,记忆集的精度如果太大可能导致占用太多内存,所以记忆集可以选择不同的精度,记录到一块内存区域精度的记忆集叫做卡表,卡表中记录的跨带的引用关系,卡表的出现就需要维护,在其他分代引用了本区域对象时,就需要使对应的卡表元素改变,这个改变的操作使用了写屏障,

最后是并发问题,在处理对象的回收时并发改变引用状态,可能导致本不应该回收的对象被回收,这里使用增量更新和原始快照的方法处理了这个问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2138251.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CloudXR 套件扩展 XR 工作流

NVIDIA为开发者提供了一个先进的平台,开发者可以在该平台上使用全新NVIDIA CloudXR 套件来创建可扩展、品牌化的定制扩展现实(XR)产品。 NVIDIA CloudXR 套件基于全新架构而打造,是扩展XR生态的重要工具。它为开发者、专业人士和…

高级 API 性能:着色器

着色器通过使您能够控制渲染过程的各个方面,在图形编程中发挥着关键作用。它们在 GPU 上运行,负责操作顶点、像素和其他数据。 常规着色器计算着色器像素渲染顶点着色器几何体、域和外壳着色器 常规着色器 这些提示适用于所有类型的着色器。 推荐 避…

[产品管理-10]:NPDP新产品开发 - 8 - 波士顿矩阵(当下与未来)在产品市场战略方面的应用

目录 一、波士顿矩阵 理论基础 产品类型划分 分析步骤 重要性 注意事项 二、波士顿矩阵的应用实例 示例背景 数据收集与准备 绘制波士顿矩阵 产品线分类 制定战略对策 一、波士顿矩阵:现在 VS 未来 波士顿矩阵理论,又称市场增长率-相对市场份…

读构建可扩展分布式系统:方法与实践04应用服务

1. 应用服务 1.1. 任何系统的核心都在于实现应用需求的特定业务逻辑 1.2. 服务是可扩展软件系统的核心 1.2.1. 它们将契约定义为一个API,向客户端声明它们的能力 1.3. 应用服务器高度依赖于编程语言,但通常都会提供多线程编程模型,允许服…

Ubuntu系统使用Docker部署Jupyter Notebook并实现笔记云同步

文章目录 前言1. 选择与拉取镜像2. 创建容器3. 访问Jupyter工作台4. 远程访问Jupyter工作台4.1 内网穿透工具安装4.2 创建远程连接公网地址4.3 使用固定二级子域名地址远程访问 前言 本文主要介绍如何在Ubuntu系统中使用Docker本地部署Jupyter Notebook,并结合cpol…

Netty(零散记录)

Netty: 1、Netty三种IO 2、Netty和Reactor的 1、Netty对Reactor的支持 Netty的线程模型时基于Reactor模型实现的,Netty对Reactor三种模式都有非常好的支持,并做了一定的改善,一般情况下,在服务端会采用主从架构模型…

Leetcode面试经典150题-739.每日温度

应读者私信要求,本题协商题目的具体内容 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0…

计算机毕业设计 二手闲置交易系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…

【目标检测数据集】厨房常见的水果蔬菜调味料数据集4910张39类VOC+YOLO格式

数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):4910 标注数量(xml文件个数):4910 标注数量(txt文件个数):4910 标注…

SSH公私钥后门从入门到应急响应

目录 1. SSH公私钥与SSH公私钥后门介绍 1.1 SSH公私钥介绍 1.1.1 公钥和私钥的基本概念 1.1.2 SSH公私钥认证的工作原理(很重要) 1.2 SSH公私钥后门介绍 2. 如何在已拿下控制权限的主机创建后门 2.1 使用 Xshell 生成公钥与私钥 2.2 将公钥上传到被需要被植入后门的服务…

Qt_显示类控件

目录 一、QLabel 1、QLabel属性介绍 2、textFormat文本格式 3、pixmap标签图片 3.1 resizeEvent 4、QFrame边框 5、alignment文本对齐 6、wordWrap自动换行 7、indent设置缩进 8、margin设置边距 9、buddy设置伙伴 二、QLCDNumber 1、QLCDNumber属性介绍 2、实…

再次探讨最小生成树Prim算法

二次复习Prim算法时得到了一些新的启示。正常初次学算法时都特别注意代码,但是慢慢的发现,随着代码能力的增强,代码反而不是这么重要,重要的是算法的思路。既在自己的脑子中能有这个算法的图解。 下面展示一下二次学习的思路&…

秋韵虫趣.

文章目录 虫鸣概览虫坛文化蟀种纷呈中华蟋蟀宁阳蟋蟀刻点铁蟋长颚斗蟋 油葫芦棺头蟋中华灶蟋小素蟋树皮蟋蟀 花生大蟋斑腿针蟋其他鸣虫树蟋,又名竹蛉、邯郸梨片蟋,又名金钟、天蛉、绿蛣蛉、银琵琶凯纳奥蟋,又名石蛉,鳞蟋黄蛉蟋&am…

基于A2C与超启发式的航天器星载自主任务规划算法-笔记

1. Actor-Critic 模块 主要文件:AC.py, PolicyNet.py, ValueNet.py作用:该模块实现了 A2C(Advantage Actor-Critic)强化学习算法。其中,ActorCritic 类是核心,它同时管理策略网络(Actor&#x…

misc合集(1)

[Week3] 这是一个压缩包 有密码,提示QmFzZUNURj8/Pz8/P0ZUQ2VzYUI base64解密是BaseCTF??????FTCesaB 猜测这应该是⼀个轴对称的密码 python ⽣成了密码字典,再通过 ARCHPR 进⾏字典爆破 lowercase abcdefghijklmnopqrstuvwxyz uppercase l…

Vue生命周期;Vue路由配置;vue网络请求;vue跨域处理

一&#xff0c;Vue生命周期 <template><div > <h1 click"changeText">{{ info }}</h1></div> </template><script> export default {name: HelloWorld,data(){return{info:"介绍组件生命周期"}},methods:{chang…

Android源码导入Android Studio

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 前言 需要先把 Android 源码编译一遍 然后执行下面指令就可以导入android源码了 关于 Android 源码编译可以参考这篇文章【LineageOS源码下载和编译&#xf…

GitLab CI_CD 从入门到实战笔记

第1章 认识GitLab CI/CD 1.3 GitLab CI/CD的几个基本概念 GitLab CI/CD由以下两部分构成。 &#xff08;1&#xff09;运行流水线的环境。它是由GitLab Runner提供的&#xff0c;这是一个由GitLab开发的开源软件包&#xff0c;要搭建GitLab CI/CD就必须安装它&#xff0c;因…

搜索二叉树BSTree的原理及实现

目录 一、简介 二、功能的实现 节点的实现 这里为什么模板参数采用的是K而不是T呢&#xff1f; 树体的实现 非递归版本 Insert函数 Find函数 Erase函数 递归版本 中序遍历 FindR InsertR EraseR 构造函数 析构函数 拷贝构造 赋值重载 一、简介 BSTree&#x…

Python 数学建模——Prophet 时间序列预测

文章目录 前言原理使用方法&#xff08;初级&#xff09;代码实例Prophet 高级应用add_seasonality 添加自定义周期性add_regressor 添加外生变量交叉检验 前言 Prophet 是 Facebook 团队开发的一个时间序列分析工具&#xff0c;相比传统的 ARMA 时间序列分析&#xff0c;能够综…