简单聊聊G1垃圾回收算法整个流程 --- 理论篇 -- 下

简单聊聊G1垃圾回收算法整个流程 --- 理论篇 -- 下

  • 软实时性
    • 预测转移时间
    • 预测可信度
    • GC 暂停处理的调度
    • 并发标记中的暂停处理
  • 分代 G1 GC 模式
    • 不同点
    • 新生代区域
    • 分代对象转移
    • 具体转移流程
    • 分代选择回收集合
      • 设置最大新生代区域数
    • GC的切换
    • GC执行的时机
  • 总结


上一篇 文章我们简单看了一下G1整个垃圾回收流程,但是关于G1如何计算区域回收价值和G1在分代模式下的工作流程这块,由于篇幅限制没有进行说明,本文主要针对这两块内容进行补齐。


软实时性

在 G1GC 中,用户可以设置如下 3 个值:

  1. 可用内存上限
  2. GC 暂停时间上限
  3. GC 单位时间

设置可用内存上限是为了避免内存被过度占用。就算是为了实现软实时性,也不能让 GC 完全占用内存。

设置GC暂停时间上限 指定的是执行 GC 所导致的用户线程的最大暂停时间。这个最大暂停时间并不包含 G1 GC 的并发处理时间。在多处理器环境下,G1 GC 的并发处理时间可以理解成平均分配给用户线程的负载。

CMS垃圾回收算法目标就是缩短用户线程的最大暂停时间,所以其适合使用在注重响应时间的Web服务器后端程序中。

有一个权宜之计可以避免 GC 暂停时间超过指定上限,那就是频繁地执行暂停时间较短的 GC。虽然这样做确实可以缩短 GC 暂停时间,但是 用户线程 的执行也会频繁地被 GC 打断,从而导致 用户线程 几乎无法正常执行。

要想避免这个问题,需要指定 GC 单位时间。指定该项后,G1GC 将在每个单位时间内遵守 GC 的暂停时间上限。如果 GC 暂停时间上限是 1 秒,而 GC 单位时间是 3 秒,就意味着在任意 3 秒的时间段内,GC 的暂停时间不可以超过 1 秒。

在这里插入图片描述
a、b、c、d 分别代表一个 GC 单位时间,GC 只在 c 中超过了暂停时间上限。

G1 GC 会努力实现软实时性。软实时性的定义是 GC 单位时间内 GC 暂停时间超过上限的次数在用户的容忍范围之内。因此,尽管在上图中,GC 单位时间 c 内的 GC 暂停时间超过了上限,但是只要用户认为可以接受,就算是实现了软实时性。


预测转移时间

要想在 GC 暂停时间上限之内完成转移,就需要选择可以在这个时间范围内完成转移的回收集合。在往回收集合中添加区域时,要先预测一下该区域的转移时间,如果超过了 GC 暂停时间上限,就不再添加该区域,并终止回收集合的选择。

在 G1GC 中,由 GC 导致的用户线程的暂停时间称为消耗。转移回收集合的消耗,等于扫描转移专用记忆集合中的卡片时的消耗与对象转移时的消耗之和

具体公式如下所示:

在这里插入图片描述
公式中各个数值的含义如下:

  • cs : 回收集合
  • V (cs) : 转移回收集合 ( cs ) 的消耗
  • V fixed : 固定消耗
  • U : 扫描脏卡片的平均消耗
  • d : 转移开始时的脏卡数量
  • S : 查找卡片内指向回收集合的引用的消耗
  • r :区域
  • rsSize : 区域的转移专用记忆集合中的卡片总数
  • C : 对象转移时每个字节的消耗
  • liveBytes : 区域内存活对象的总字节数 (大概的值)

V ( cs ) 表示某个回收集合 ( cs ) 的转移时间。V fixed 表示转移过程中的固定消耗,主要是选择和释放回收集合时的消耗。

V fixed ,U ,S ,C 这几个值的大小受实现方法、运行平台以及应用程序特性等各种环境因素的影响,是可变的。因此可以先根据经验设置一些初始值,再通过测量各自的实际处理时间来进行修正,以提高精度

liveBytes 使用上一篇文章讲到的prev_marked_bytes值,对于在并发标记结束后被分配的对象,即使是死亡对象,也要将其当做存活对象来计数。因此liveBytes并非精确值,只是大概的值。

S * rsSize ( r ) + C * liveBytes ( r ) 是一个区域的转移消耗。累加求和操作是回收集合内所有区域的转移消耗的总和。U * d 是在转移回收阶段对转移专用记忆集合维护线程未处理完的脏卡片进行扫描的消耗。这些消耗再加上 V fixed ,就是总体的消耗了。

在理解了公式中各个参数含义后,我们可以把公式翻译成下面的大白话模样:

转移回收集合内所有存活对象的总消耗 = 
固定消耗 + 
扫描剩余脏卡的总消耗 (脏卡数量 * 扫描每个脏卡的平均消耗) +
转移回收集合内所有存活对象的消耗 = 
依次扫描回收集合内每个区域,并计算每个区域转移消耗 =
遍历该区域转移专用记忆集合中每个脏卡,并找出所有指向当前回收区域的引用的总消耗 + 转移当前区域内所有存活对象的总消耗

预测可信度

用户可以通过对消耗的预测值设置预测可信度来调整暂停时间。

预测可信度是一个百分数。如果预测可信度设置为 120%,GC 暂停时间会在消耗预测值的基础上上浮 20%。相反,如果设置为 80%,会在预测值的基础上下浮 20%。预测可信度越高,用户的暂停时间就越短;相反,预测可信度越低,用户的暂停时间就越长。


GC 暂停处理的调度

GC 暂停处理必须在合适的时机进行。这是为了遵守本文一开始提到的规则:

  • 在 GC 单位时间内不得超过 GC 暂停时间的上限

当堆内空间充足时,可以根据需要扩展堆,从而延迟转移处理。而且,转移处理并不一定发生在并发标记完全结束之后。因此,即使并发标记过程中的暂停处理(根扫描等)延迟开始,也不会产生致命的问题。通过这些可知:在一般情况下(除了堆内空间紧缺时),GC 暂停处理发生的时机是可以调度的。

G1GC 中有一个队列名为调度队列,其中的元素是暂停处理的开始时间和结束时间的组合。G1GC 使用这个队列来高效地调度 GC 的暂停处理任务。调度队列中保存了最近一次暂停处理的开始时间和结束时间(队列的元素)。调度队列中元素个数是有上限的,如果添加元素时超过上限,队列头部中最早添加的元素就会被删除。

调度程序会基于调度队列中的信息来决定下次 GC 暂停的适当时机。如下图所示:
在这里插入图片描述
如果像图中②这样执行,GC 单位时间内总的 GC 暂停时间会超过上限。但是如果像③这样指定了合适的 GC 暂停时机 Z,GC 单位时间内总的 GC 暂停时间就不会超过上限了。

上图中①的 X 表示下次 GC 暂停处理的预测暂停时间。调度程序会计算 X 的开始时刻。

首先,假定 X 会像图中②一样立即开始执行,由此计算出 GC 单位时间内总的 GC 暂停时间(包含 X),并检查它是否超过用户指定的 GC 暂停时间上限。如果没有超过上限,则认为 X 可以立即开始执行;相反,如果超过了上限,则需要延迟执行 X。GC 暂停时间上限和总的GC 暂停时间的差用 Y 来表示。

图中③将 X 的开始时刻向后延迟了 Y,延迟后的开始时刻用 Z 表示。这样,GC 单位时间内总的 GC 暂停时间就刚好等于 GC 暂停时间上限。换句话说,Z 就是执行 X 的合适时机。

需要注意的是,调度程序会保证在任意选取的 GC 单位时间内,总的GC 暂停时间不会超过用户指定的 GC 暂停时间上限。假设 GC 单位时间是 3 秒,GC 暂停时间上限是 1 秒,那么就要像:

  • “第 0 秒到第 3 秒内的GC 暂停时间不超过 1 秒”
  • “第 0.0001 秒到第 3.0001 秒内的 GC 暂停时间不超过 1 秒”
  • “第 0.0003 秒到第 3.0002 秒内的 GC 暂停时间不超过 1秒”

这些情况一样,在无论从哪个时刻开始的 3 秒内,GC 暂停时间都不会超过上限哪怕 1 秒。下图展示了实际发生过的 GC 暂停的时间片段。

在这里插入图片描述

GC 单位时间 a、b、c 中任何一个的总 GC 暂停时间都不超过暂停时间上限。

观察一下 GC 单位时间 a、b、c 范围内总的 GC 暂停时间,可以发现GC 暂停处理的确没有超过 GC 暂停时间上限。

当然,在 GC 的预测时间不准确或堆内空间不足等导致 GC 必须提前开始时,GC 暂停处理还是会超出暂停时间上限。


并发标记中的暂停处理

并发标记中的暂停处理阶段也会以上文中所讲的方法,按照合适的间隔执行。具体来说,需要在以下 3 个步骤中执行暂停处理:

  1. 初始标记阶段
  2. 最终标记阶段
  3. 收尾工作

但是,这些步骤的暂停时间不像转移中的暂停时间一样可控,如果暂停时间本身就超过了 GC 暂停时间上限,就不能遵守 GC 暂停时间上限了。

在调度 GC 的暂停时机时,需要预测暂停时间。一开始需要根据经验来设置并发标记中暂停处理的预测暂停时间。然后可以根据测算出的实际暂停时间,并结合过去的执行结果来提高预测暂停时间的精确度。


分代 G1 GC 模式

G1GC 中存在“纯 G1GC 模式”(pure garbage-first mode)和“分代 G1GC模式”(generational garbage-first mode)两种模式。前面介绍的内容都是关于纯 G1GC 模式的。本节开始,我们将介绍分代 G1GC 模式。

实际上,OpenJDK 虽然实现了纯 G1GC 模式,但是并没有将这种模式开放给用户。用户们使用的都是分代 G1GC 模式。


不同点

和纯 G1GC 模式相比,分代 G1GC 模式主要有以下两个不同点。

  • 区域是分代的
  • 回收集合的选择是分代的

在分代 G1 GC 模式中,区域被分为新生代区域和老年代区域两类。和其他分代 GC 算法一样,分代 G1 GC 的对象也保存了自身在各次转移中存活下来的次数。新生代区域用来存放新生代对象,老年代区域用来存放老年代对象。

另外,分代 G1 GC 模式也分为新生代 GC 和老年代 GC 。G1 GC 中的新生代 GC 称为完全新生代 GC(fully-young collection),老年代 GC称为部分新生代 GC(partially-young collection)。

完全新生代 GC 和部分新生代 GC 的主要区别在于回收集合的选择:

  • 完全新生代 GC 将所有新生代区域选入回收集合
  • 部分新生代 GC 将所有新生代区域,以及一部分老年代区域选入回收集合。

这里需要注意的是,所有的新生代区域都会被选入回收集合。这一点非常重要,请务必牢记。


新生代区域

新生代区域可以进一步分为以下两类:

  • 创建区域
  • 存活区域

创建区域用来存放刚刚生成、一次也没有被转移过的对象。存活区域用来存放被转移过至少一次的对象。

另外,转移专用写屏障不会应用在新生代区域的对象上。因此,即使新生代区域的对象存在对其他区域对象的引用,被引用区域的转移专用记忆集合中也不会记录引用方的卡片。

在这里插入图片描述
对于新生代区域 A 中对象 a 对老年代区域 B 中对象 b 的引用,转移专用写屏障是无效的,所以转移专用记忆集合 B 不会记录这次引用(左图)。而对于来自老年代区域对象的引用,转移专用写屏障仍然有效(右图)。

但是,为什么新生代区域间不使用转移专用写屏障也可以呢?

  • 我们先回顾一下转移专用记忆集合的作用。转移专用记忆集合维护的是区域之间的引用关系,因此在转移时无须扫描整个区域就能找到待转移对象所在区域的存活对象。
  • 而在分代 G1GC 模式中,所有的新生代区域都会被选入回收集合,因此在转移新生代区域时所有对象的引用都会被检查。
  • 即使被引用区域的转移专用记忆集合中记录了来自新生代区域的引用,这些记录也都是重复的信息。
  • 因此,转移专用记忆集合中不会记录来自新生代区域的引用。

这里可以再次回顾上一篇文章中贴出的对象转移过程伪代码:

1: def evacuate_obj(ref):
# 拿到被引用对象的地址
2: from = *ref
# 如果被引用对象没有被标记,说明是死亡对象,则无需转移
3: if not is_marked(from):
4: return from
# 如果被引用对象设置了转发标记,说明此时对象已经完成了转移
5: if from.forwarded:
# 在被引用对象的转移记忆集合中,重新添加引用方所在区域对自己的引用关系,如果有需要的话
6: add_reference(ref, from.forwarded)
# 返回被引用对象转移后的新地址
7: return from.forwarding
8:
# 为被引用对象分配新的内存空间,然后copy过去
9: to = allocate($free_region, from.size)
10: copy_data(new, from, from.size)
11:
# 设置被引用对象所处旧位置的对象头的转发标记和转发指针
12: from.forwarding = to
13: from.forwarded = True
14:
# 遍历被引用对象引用的子对象列表
15: for child in children(to):
# 如果子对象所处区域属于回收集合,则将子对象添加到转移队列中
16: if is_into_collection_set(*child):
17: enqueue($evacuate_queue, child)
18: else:
# 在子对象所在区域的转移记忆集合中,重新添加引用方所在区域对自己的引用关系
19: add_reference(child, *child)
20:
# 这里相当于在新的区域重新创建了对象,所以在被引用对象的转移记忆集合中,重新添加引用方所在区域对自己的引用关系
21: add_reference(ref, to)
22:
# 返回对象转移后新的地址
23: return to

被加入引用队列后,后续被处理的流程:

1: def evacuate():
2: while $evacuate_queue != Null:
3: ref = dequeue($evacuate_queue)
4: *ref = evacuate_obj(ref)
  • 如果新生代区域C中的对象c1被新生代区域A中的对象a1所引用,此时对象转移存在两种情况:
    • 如果先转移c1,然后c1旧地址会设置转发标记和转发地址,在转移对象a1的时候,遍历到子对象c1时,发现子对象也位于回收区域内,则加入引用队列等待稍后处理,同时将对象a1新的引用赋值给引用方,然后返回对象a1新的地址。
      • 等到后面子对象c1从引用队列取出处理时,发现其已经被转移过了,此时会更新a1指向c1的引用关系,然后返回c1对象新的地址。
    • 如果先转移a1, 遍历到子对象c1时,发现子对象也位于回收区域内,则加入引用队列等待稍后处理,同时将对象a1新的引用赋值给引用方,然后返回对象a1新的地址。
      • 等到后面子对象c1从引用队列取出处理时,将c1转移到新区域后,此时会更新a1指向c1的引用关系,然后返回c1对象新的地址。

所以当跨区域引用对应的引用方区域和被引用方区域都位于回收集合中时,此时就无需在被引用方的转移专用记忆集合中添加引用方所在卡片了,这也是为什么这里新生代区域无需使用写屏障的原因了。


分代对象转移

存活对象保存了自己被转移的次数,这个次数称为对象的年龄。转移时对象的年龄如果低于阈值,对象就会被转移到存活区域,否则就会被转移到老年代区域。将对象转移到老年代区域的行为称为晋升。

如果转移的目标区域满了,垃圾回收器就会选择一个空闲的区域,把它修改成存活区域或者老年代区域之后,作为转移的目标区域使用。

对象被转移到存活区域之后,即使该对象引用了回收集合以外的区域,也不需要记录在转移专用记忆集合中。关于这一点的原因,上一节末尾已经说明了原因。相反,往老年代区域转移对象时就必须要记录。因为老年代区域并非每次都会被选入回收集合。


具体转移流程

我们来看一下完全新生代 GC 的执行过程。

完全新生代 GC 不会选择老年代区域,而是将所有新生代区域都选入回收集合,然后转移回收集合内的存活对象。晋升的对象会被转移到老年代区域,其余对象则被转移到存活区域。

在这里插入图片描述
部分新生代 GC 则是除了所有新生代区域外,还会选择一部分老年代区域进入回收集合。除了回收集合的选择方式,部分新生代 GC 和完全新生代 GC 的执行过程是一样的。

在这里插入图片描述


分代选择回收集合

刚才介绍过,在回收集合的选择方式上,完全新生代 GC 和部分新生代GC 有所不同。完全新生代 GC 会选择所有新生代区域,而部分新生代GC 会选择所有新生代区域和一部分老年代区域。

分代 GC 的理论基础是大多数对象是“朝生夕死”的。因此,分代 G1GC模式也是通过选择回收集合的方式来确保总是优先转移新生代区域,从而积极地释放年轻对象的内存空间。

不过,选择全部新生代区域的做法可能会打破软实时性。如果新生代区域数太多,就有可能无法遵守用户设置的 GC 暂停时间上限。要想避免这个问题,分代 G1GC 模式就需要计算出合理的最大新生代区域数。


设置最大新生代区域数

完全新生代 GC 和部分新生代 GC 关于最大新生代区域数的计算方法是不一样的。

完全新生代 GC 的最大新生代区域数是在遵守 GC 暂停时间上限的前提下,尽量设置较大的值。即根据过去的转移时间记录,预测出单个新生代区域转移所需的大概时间,然后基于这个时间计算出刚好不超过 GC暂停时间上限的最大新生代区域数。完全新生代 GC 的名字由来就是“尽可能完全地转移新生代区域”。

而部分新生代 GC 的最大新生代区域数是在遵守 GC 单位时间的前提下,尽量设置较小的值:

  1. 首先,采用软实时性一节中介绍的方法计算出下次能够进行 GC 暂停处理的时机。
  2. 然后,预测出在这个“时机”之前大概能回收多少个区域,并以此作为新生代区域的最大数目。
  3. 当预测值命中时,达到最大新生代区域数的时机,刚好就是下次能够进行 GC 暂停处理的时机,因此能够遵守 GC 单位时间。
  4. 另外,因为最大新生代区域数设置的是最小值,所以被选入回收集合的新生代区域数也是最少的。
  5. 这样一来,距离 GC 暂停时间上限很可能还有一段时间,就可以往回收集合里添加一些老年代区域。

部分新生代 GC 的名字由来就是“尽可能少地转移新生代区域”。

最大新生代区域数的设置发生在并发标记结束之后。


GC的切换

垃圾回收器在选择 GC 算法时,通常会选择部分新生代 GC,只有在使用完全新生代 GC 效率更高时才会切换为完全新生代 GC。切换的时间点和设置最大新生代区域数时一样,都是在并发标记结束之后。

首先,参考并发标记中标记出的死亡对象个数,预测出下次部分新生代GC 的转移效率。然后,根据过去的完全新生代 GC 的转移效率,预测出下次完全新生代 GC 的转移效率。如果预测出完全新生代 GC 的转移效率更高,则切换为完全新生代 GC。


GC执行的时机

当新生代区域数达到上限时,会触发转移的执行。换句话说,通过调节最大新生代区域数,可以控制转移执行的时机。

当转移完成并通过以下 4 项检查之后,会开始执行并发标记:

  1. 不在并发标记执行过程中
  2. 并发标记的结果已被上次转移使用完
  3. 已经使用了一定量的堆内存(默认是全部堆内存的 45% 以上)
  4. 相比上次转移完成之后,堆内存的使用量有所增加

其中第二步是为了避免重复地并发标记。如果有并发标记的结果尚未在转移过程中被使用,则不会开始并发标记。

需要注意的是,并发标记过程中的所有暂停处理都需要遵守程序对于GC 暂停处理的调度,以适当的时间间隔来执行。


总结

GC 的各种处理之间关系非常复杂,这里我们用一张图来总结一下。下图展示了 mutator (用户线程) 和 GC 的执行关系示例:

在这里插入图片描述

图中并列的箭头表示可能会并行执行的处理

从上图中可以看出,转移专用记忆集合维护线程和 mutator(用户线程) 在大多数时间中是并发执行的,但是在存活对象计数时,转移专用记忆集合维护线程也是暂停的。

  • 并发计数阶段是和用户线程并行执行的,到此时为止所有存活对象都已经被标记出来了,G1后续会按照当前时刻快照进行筛选回收,所以即使此刻用户线程又更改了引用关系,也不会有什么影响,所以可以停止掉记忆集合线程。
  • 当进入后续筛选回收阶段时,还是处于STW状态,相当于是以最终标记这一刻的快照为准,进行筛选加对象转移,所以以上论述都是为了证明此刻停掉记忆集合线程没有什么关系,用户在计数阶段变更的引用关系,会在下一波GC时再被处理。

还有一点需要注意,那就是转移处理可能发生在并发标记中暂停处理以外的所有时刻。比如在并发标记阶段或者存活对象计数的过程中,都可能执行转移。

下面对G1垃圾回收算法的优缺点进行总结:

优点:

  • 首先,G1GC 具备软实时性,这是一个很大的优点。对于要求软实时性的应用程序,可以由用户控制 GC 暂停时间。
  • 其次,它能够充分发挥高配置机器的性能,大幅缩减 GC 暂停时间,这一点也值得表扬。虽然考虑到算法,总有一些必须要暂停的阶段,但这些阶段也可以通过尽可能地并行执行,来进一步缩短暂停时间。
  • 再次,它通过将写屏障的处理粒度由对象粒度改为更粗的卡片粒度,降低了写屏障发生的频率。这也是缩短暂停时间的一个手段。
  • 另外,因为有转移,所以区域内不会产生内存碎片。由此可以提高引用的局部性和对象存储空间分配的速度。
  • 与其他具备软实时性的 GC 相比,G1GC 的吞吐量保持在较高水平。近年,很多具备软实时性的 GC 会通过频繁地执行“以对象为单位进行复制”这种更细粒度的暂停处理来缩短 GC 暂停时间,从而达成软实时性。因此,无论如何它们的吞吐量都是下降的。另外,这些 GC 中死亡对象的回收处理可能存在延迟,因此内存的使用效率也不高。
  • 而 G1GC 以区域这种较粗的粒度来频繁地执行用户指定时间内的暂停处理,所以暂停时间会稍微长一些,它的吞吐量也会高一些。此外,通过在转移时选择合适的回收集合,还能够更高效地回收死亡对象。

缺点:

  • G1 GC 的适用对象被限定为“搭载多核处理器、拥有大容量内存的机器”。在多数环境下,我们并不能发挥出它的性能。适用环境受限可以说是它的一个缺点。
  • 另外在转移时,尽管区域内不会出现碎片化,但是会出现以区域为单位(整个堆)的碎片化。和普通的 GC 复制算法相比,这一点算是缺点。

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

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

相关文章

【C++】常用排序算法

0.前言 1.sort #include <iostream> using namespace std;// 常用排序算法 sort #include<vector> #include<algorithm>//利用仿函数 打印输出 class myPrint { public:void operator()(int val){cout << val << " ";} };//利用普通函…

【C++深入浅出】类和对象中篇(六种默认成员函数、运算符重载)

目录 一. 前言 二. 默认成员函数 三. 构造函数 3.1 概念 3.2 特性 四. 析构函数 4.1 概念 4.2 特性 五. 拷贝构造函数 5.1 概念 5.2 特性 六. 运算符重载 6.1 引入 6.2 概念 6.3 注意事项 6.4 重载示例 6.5 赋值运算符重载 6.6 前置和后置运算符重载 七. c…

Excel显示列号

默认表格打开列以字母显示 设置方法 文件 -> 工具 -> 选项 -> 常规与保存 设置后效果如下图

使用高斯混合模型进行聚类

一、说明 高斯混合模型 &#xff08;GMM&#xff09; 是一种基于概率密度估计的聚类分析技术。它假设数据点是由具有不同均值和方差的多个高斯分布的混合生成的。它可以在某些结果中提供有效的聚类结果。 二、Kmean算法有效性 K 均值聚类算法在每个聚类的中心周围放置一个圆形边…

推荐10个AI人工智能技术网站(一键收藏,应有尽有)

1、Mental AI MentalAI&#xff08;https://ai.ciyundata.com/&#xff09;是一种基于文心大模型的知识增强大语言模型&#xff0c;专注于自然语言处理&#xff08;NLP&#xff09;领域的技术研发。它具备强大的语义理解和生成能力&#xff0c;能够处理各种复杂的自然语言任务。…

Python第一次作业练习

题目分析&#xff1a; """ 参考学校的相关规定。 对于四分制&#xff0c;百分制中的90分及以上可视为绩点中的4分&#xff0c;80 分及以上为3分&#xff0c;70 分以上为2分&#xff0c;60 分以上为1分; 五分制中的5分为四分制中的4分&#xff0c;4分为3分&#…

RNA 37. SCI 文章中基于转录组计算肿瘤免疫浸润得分

这期推荐软件包 xCell:数字化描绘组织细胞异质性景观&#xff0c;通过它可以计算bulk 转录组的免疫浸润得分&#xff0c;下面我们就看看怎么来实现吧&#xff01; 简 介 组织是由许多细胞类型组成的复杂环境。在癌症领域&#xff0c;了解肿瘤微环境中的细胞异质性是一个新兴…

【css | loading】好看的loading特效

示例&#xff1a; https://code.juejin.cn/pen/7277764394618978365 html <div class"pl"><div class"pl__dot"></div><div class"pl__dot"></div><div class"pl__dot"></div><div c…

第51节:cesium 范围查询(含源码+视频)

结果示例: 完整源码: <template><div class="viewer"><el-button-group class="top_item"><el-button type=

Axure RP 10汉化版下载 Axure RP 10 mac授权码

Axure RP10汉化版是最强大的计划&#xff0c;原型设计和交付给开发人员的方法&#xff0c;而无需编写代码。能够制作逼真的&#xff0c;动态形式的原型。 Axure RP 10汉化版下载 Axure RP 10 mac授权码 RP 10有什么新功能&#xff1f; 1.显示动态面板 使用Axure RP 10&…

docker 镜像内执行命令显示:You requested GPUs: [0] But your machine only has: []

目录 问题描述&#xff1a; 问题解决&#xff1a; 问题描述&#xff1a; 在docker 镜像环境中&#xff0c;执行“docker exec -it container_name /bin/bash “进入容器之后&#xff0c;执行对应的python命令&#xff0c;显示You requested GPUs: [0] But your machine only…

【深度学习】P1 单层神经网络 - 线性回归(待完成)

单层神经网络 - 线性回归 线性回归基本要素1. 模型2. 模型训练3. 训练数据4. 损失函数5. 优化算法6. 模型预测 线性回归与神经网络1. 神经网络图 以一个简单的房屋价格预测为例&#xff0c;介绍解释线性回归这一单层神经网络。无需纠结于什么是单层神经网络&#xff0c;在本文的…

Hadoop_02

hadoop相比于传统文件系统的优点&#xff1a; 1.无限扩展 2.传统文件元数据分布在不同的机器上难以寻找&#xff0c;通过将元数据统一存放在一个服务器上解决 3.传统文件太大导致上传下载慢&#xff0c;通过分块并行上传到服务器解决 4.副本机制数据不容易丢失&#xff0c;解决…

JavaScript里面的二进制

概述 最近在做IOT设备配网开发的时候&#xff0c;处理了很多跟二进制、字节相关的事情&#xff0c;总结了一下JavaScript中有关二进制方面的一些知识点。 二进制和字节 首先&#xff0c;现代计算机是基于二进制的&#xff0c;从现代计算机电路来说&#xff0c;只有高电平/低电平…

数据在内存中的存储——练习3

题目&#xff1a; 3.1 #include <stdio.h> int main() {char a -128;printf("%u\n",a);return 0; }3.2 #include <stdio.h> int main() {char a 128;printf("%u\n",a);return 0; }思路分析&#xff1a; 首先二者极其相似%u是无符号格式进行…

【Linux】—— 在Linux上进行读写文件操作

前言&#xff1a; 在之前&#xff0c;我已经对进程的相关知识进行了详细的介绍。本期开始&#xff0c;我们将要学习的是关于 “基础I/O”的知识&#xff01;&#xff01;&#xff01; 目录 &#xff08;一&#xff09;C文件接口 &#xff08;二&#xff09;系统文件I/O 1、接…

WebDAV之π-Disk派盘 + BubbleUPnP

BubbleUPnP是一款功能强大的Android播放器,支持UPnP/DLNA多屏互动。它可以将手机内容投屏到电视大屏上,与家人和朋友一起共享。此外,BubbleUPnP还提供了丰富的音乐和影视资源,您可以在线搜索并播放喜欢的内容。 以下是BubbleUPnP的一些主要特点: 1. 支持Chromecast和转码…

WebGL 绘制矩形

上一节绘制了圆点&#xff0c;调用的绘制方法如下&#xff1a;gl.drawArrays(gl.POINTS, 0, 1); 第一个参数明显是个枚举类型&#xff0c;肯定还有其他值&#xff0c;如下所示&#xff1a; POINTS 可视的点LINES 单独线段LINE_STRIP 线条LINE_LOOP 闭合线条TRIANGLES 单独三…

【题解】2596. 检查骑士巡视方案

题解&#xff1a; class Solution {int n,m;bool st[100][100];int flag;int dx[8]{-1,-2,-2,-1,1,2,2,1};int dy[8]{-2,-1,1,2,2,1,-1,-2}; public:bool checkValidGrid(vector<vector<int>>& grid) {m grid.size();n grid[0].size();dfs(grid,0,0,0);ret…

vue3中的吸顶导航交互实现 | VueUse插件

目的&#xff1a;浏览器上下滚动时&#xff0c;若距离顶部的滚动距离大于78px&#xff0c;吸顶导航显示&#xff0c;小于78px隐藏。使用vueuse插件中的useScroll方法​​​​​​​和动态类名控制进行实现 1. 安装 npm i vueuse/core 2. 获得滚动距离 项目中导入&#xff0…