JVM 垃圾回收机制和常见的垃圾回收器

  • 开篇
  • 垃圾回收
    • 标记可达对象(Marking Reachable Objects)
    • 删除不可达对象(Removing Unused Objects)
      • 清除(Sweeping)
      • 整理(Compacting)
      • 复制(Copying)
    • 分代假设
      • 年轻代
        • TLAB
      • 老年代
    • 卡片标记(card marking)
  • HotSpot 垃圾回收器
    • 年轻代垃圾回收器
      • Serial 垃圾收集器
      • ParNew 垃圾收集器
      • Parallel Scavenge 垃圾收集器
    • 老年代垃圾收集器
      • Serial Old 垃圾收集器
      • Parallel Old
      • CMS 垃圾收集器
    • G1
  • 配置参数
  • STW
    • Stop the World 造成的系统停顿
    • 参考资料

开篇

前面的文章里,我们介绍了 JVM 的组成结构与垃圾回收算法等知识点。今天我们来讲讲 JVM 最重要的堆内存是如何使用垃圾回收器进行垃圾回收,并且如何使用命令去配置使用这些垃圾回收器。

再这之前,我们对垃圾回收机制做一个简单的回顾。

垃圾回收

按照语义上的意思,垃圾回收,首先就需要找到这些垃圾,然后回收掉。但是 GC 过程正好相反,它是先找到活跃的对象,然后把其他不活跃的对象判定为垃圾,然后删除。

所以垃圾回收只与活跃的对象有关,和堆的大小无关。

各种垃圾收集器的实现细节虽然并不相同,但总体而言,垃圾收集器都专注于两件事情:

  1. 查找所有存活对象
  2. 抛弃其他的部分,即死对象,不再使用的对象

标记可达对象(Marking Reachable Objects)

现代 JVM 中所有的 GC 算法,第一步都是找出所有存活的对象。如下图所示:

  1. 首先,有一些特定的对象被指定为 Garbage Collection Roots(GC 根元素)。
  2. 其次,GC 遍历(traverses)内存中整体的对象关系图(object graph),从 GC 根元素开始扫描,到直接引用,以及其他对象(通过对象的属性域)。所有 GC 访问到的对象都被标记为存活对象。
  3. 标记阶段完成后,所有存活对象都被标记了(存活对象在上图中用蓝色表示)。而其他对象(上图中灰色的数据结构)就是从 GC 根元素不可达的,也就是说程序不能再使用这些不可达的对象(unreachable object)。这样的对象被认为是垃圾,GC 会在接下来的阶段中清除它们。

在标记阶段有个需要注意的地方:在标记阶段,需要暂停所有应用线程,以遍历所有对象的引用关系。因为不暂停就没法跟踪一直在变化的引用关系图。这种情景叫做 Stop The World(简称 STW,全线停顿),而可以安全地暂停线程的点叫做安全点(safe point),然后,JVM 就可以专心执行清理工作。安全点可能有多种因素触发,当前,GC 是触发安全点最常见的原因。

此阶段暂停的时间,与堆内存大小,对象的总数没有直接关系,而是由存活对象(alive objects)的数量来决定。所以增加堆内存的大小并不会直接影响标记阶段占用的时间。

标记阶段完成后,GC 进行下一步操作,删除不可达对象。

删除不可达对象(Removing Unused Objects)

各种 GC 算法在删除不可达对象时略有不同,但总体可分为三类:清除(sweeping)、整理(compacting)和复制(copying)。

清除(Sweeping)

Mark and Sweep(标记—清除) 算法的概念非常简单:直接忽略所有的垃圾。也就是说在标记阶段完成后,所有不可达对象占用的内存空间,都被认为是空闲的,因此可以用来分配新对象。

这种算法需要使用空闲表(free-list),来记录所有的空闲区域,以及每个区域的大小。

维护空闲表增加了对象分配时的开销。此外还存在另一个弱点 —— 那就是碎片问题

比如我申请了 1k、2k、3k、4k、5k 的内存。


由于某种原因 2k 和 4k 的内存,不再使用,就需要交给垃圾回收器回收。


这个时候,我应该有足足 6k 的空闲空间。接下来,我打算申请一个 5k 的空间,结果系统告诉我内存不足了。

而且系统运行时间越长,这种碎片就越多。

整理(Compacting)

标记—清除—整理算法(Mark-Sweep-Compact),将所有被标记的对象(存活对象),迁移到内存空间的起始处,消除了「标记—清除算法」的缺点。

相应的缺点就是 GC 暂停时间会增加,因为需要将所有对象复制到另一个地方,然后修改指向这些对象的引用。

此算法的优势也很明显,碎片整理之后,分配新对象就很简单,只需要通过「指针碰撞」(pointer bumping)即可。使用这种算法,内存空间剩余的容量一直是清楚的,不会再导致内存碎片问题。

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

复制(Copying)

标记—复制算法(Mark and Copy) 和「标记—整理算法」(Mark and Compact)十分相似:两者都会移动所有存活的对象。

区别在于,「标记—复制算法」是将内存移动到另外一个空间:存活区。

「标记—复制方法」的优点在于:标记和复制可以同时进行。缺点则是需要一个额外的内存区间,来存放所有的存活对象。

分代假设

我们简要介绍了一些常见的内存回收算法,目前,JVM 的垃圾回收器,都是对几种朴素算法的发扬光大。简单看一下它们的特点:

  • 标记-清除(Mark-Sweep):效率一般,缺点是会造成内存碎片问题。
  • 复制算法(Copy):复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费。
  • 标记-整理(Mark-Compact):效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。

所以,没有最优的算法,只有最合适的算法。

JVM 是计算节点,而不是存储节点。最理想的情况,就是对象在用完之后,它的生命周期立马就结束了。而那些被频繁访问的资源,我们希望它能够常驻在内存里。

研究表明,大部分对象,可以分为两类:

  • 大部分对象的生命周期都很短
  • 其他对象则很可能会存活很长时间

大部分死的快。这个假设我们称之为弱分代假说(weak generational hypothesis)。

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

现在的垃圾回收器,都会在物理上或者逻辑上,把这两类对象进行区分:

  • 我们把死的快的对象所占的区域,叫作年轻代(Young generation)
  • 把其他活的长的对象所占的区域,叫作老年代(Old generation),老年代在有些地方也会叫作 Tenured Generation

年轻代

年轻代使用的垃圾回收算法是复制算法。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。

我们前面也了解到复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域。


如图所示,年轻代分为:Eden 区,两个 Survivor 区。

当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GC(Minor GC)。具体过程如下:

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会通过复制算法进入到其中一个 Survivor 分区(以下简称 from);
  • Eden 区再次 GC,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;接下来,只需要清空 from 区就可以了。

这个过程中,总会有一个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。这个比例,是由参数 -XX:SurvivorRatio 进行配置的(默认为 8)。

一般情况下,我们只需要了解到这一层面就 OK 了。但是在平常的面试中,还有一个点会经常提到,虽然频率不太高,它就是 TLAB,我们在这里也简单介绍一下。

TLAB

TLAB 的全称是 Thread Local Allocation Buffer,JVM 默认给每个线程开辟一个 buffer 区域,用来加速对象分配。这个 buffer 就放在 Eden 区中。

在 Java 程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速 GC,所以对于小对象通常 JVM 会优先分配在 TLAB 上,并且 TLAB 上的分配由于是线程私有,所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。

这个道理和 Java 语言中的 ThreadLocal 类似,避免了对公共区的操作,以及一些锁竞争。

对象的分配优先在 TLAB 上分配,但 TLAB 通常都很小,所以对象相对比较大的时候,会在 Eden 区的共享区域进行分配。

TLAB 是一种优化技术,类似的优化还有对象的栈上分配(这可以引出逃逸分析的话题,默认开启)。

老年代

老年代一般使用「标记-清除」、「标记-整理」算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。

那么,对象是怎么进入老年代的呢?有多种途径。

(1)提升(Promotion)

如果对象够老,会通过「提升」进入老年代。

关于对象老不老,是通过它的年龄(age)来判断的。每当发生一次 Minor GC,存活下来的对象年龄都会加 1。直到达到一定的阈值,就会把这些“老顽固”给提升到老年代。

这些对象如果变的不可达,直到老年代发生 GC 的时候,才会被清理掉。

这个阈值,可以通过参数 ‐XX:+MaxTenuringThreshold 进行配置,最大值是 15,因为它是用 4bit 存储的。

(2)分配担保

看一下年轻代的图,每次存活的对象,都会放入其中一个幸存区,这个区域默认的比例是 10%。

但是我们无法保证每次存活的对象都小于 10%,当 Survivor 空间不够,就需要依赖其他内存(指老年代)进行分配担保。这个时候,对象也会直接在老年代上分配。

(3)大对象直接在老年代分配

超出某个大小的对象将直接在老年代分配。这个值是通过参数 -XX:PretenureSizeThreshold 进行配置的。默认为 0,意思是全部首选 Eden 区进行分配。

(4)动态对象年龄判定

有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或等于 age 的对象将会直接进入老年代。

卡片标记(card marking)

我们可以看到,对象的引用关系是一个巨大的网状。有的对象可能在 Eden 区,有的可能在老年代,那么这种跨代的引用是如何处理的呢?

由于 Minor GC 是单独发生的,如果一个老年代的对象引用了它,如何确保能够让年轻代的对象存活呢?

对于是、否的判断,我们通常都会用 Bitmap(位图)和布隆过滤器来加快搜索的速度。

JVM 也是用了类似的方法。其实,老年代是被分成众多的卡页(card page)的(一般数量是 2 的次幂)。

卡表(Card Table)就是用于标记卡页状态的一个集合,每个卡表项对应一个卡页。

如果年轻代有对象分配,而且老年代有对象指向这个新对象, 那么这个老年代对象所对应内存的卡页,就会标识为 dirty,卡表只需要非常小的存储空间就可以保留这些状态。

垃圾回收时,就可以先读这个卡表,进行快速判断。

HotSpot 垃圾回收器

下面开始介绍 HotSpot 的几个垃圾回收器,每种回收器都有各自的特点。我们在平常的 GC 优化时,一定要搞清楚现在用的是哪种垃圾回收器。

在此之前,我们把上面的分代垃圾回收整理成一张大图,在介绍下面的收集器时,你可以对应一下它们的位置。

在这里我们简单介绍几个概念:

  • Minor GC:发生在年轻代的 GC。
  • Major GC:发生在老年代的 GC。
  • Full GC:全堆垃圾回收。比如 Metaspace 区引起年轻代和老年代的回收。

年轻代垃圾回收器

Serial 垃圾收集器

Serial(翻译为串行)是最基础的垃圾收集器,使用复制算法,曾经是 JDK1.3.1 之前新生代唯一的垃圾收集器。

Serial 是一个单线程工作的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在垃圾回收的过程中暂停一切用户线程,直到垃圾收集结束。这种现象称之为 STW(Stop-The-World)。

STW 是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。

Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial 垃圾收集器依然是 Java 虚拟机运行在 Client 模式下默认的年轻代垃圾收集器。

ParNew 垃圾收集器

ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。清理过程依然要停止用户线程。

ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。

ParNew 收集器是许多运行在 Server 模式下的虚拟机,尤其是 JDK 7 之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但很重要的原因是,除了Serial 收集器外,目前只有 ParNew 能与 CMS 收集器配合工作。

Parallel Scavenge 垃圾收集器

Parallel Scavenge 收集器是新生代收集器,它也是使用复制算法的收集器,是并行的多线程收集器,Java1.8 默认的新生代垃圾收集器

它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),即吞吐量优先。

高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多与用户交互的任务。

它与 ParNew 的主要区别是:

  • Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,适合没有交互的后台计算。弱交互强计算。
  • ParNew:追求降低用户停顿时间,适合交互式应用。强交互弱计算。

老年代垃圾收集器

Serial Old 垃圾收集器

与年轻代的 Serial 垃圾收集器对应,都是单线程版本,同样适合客户端使用。

年轻代的 Serial,使用复制算法。

老年代的 Serial Old ,使用标记-整理算法。

Parallel Old

Parallel Scavenge 的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

在注重吞吐量或者处理器资源较为稀缺的场合,可以优先考虑 Parallel Scavenge 加Parallel Old 收集器这个组合。

CMS 垃圾收集器

CMS(Concurrent Mark Sweep)收集器是以获取最短 GC 停顿时间为目标的收集器,它在垃圾收集时使得用户线程和 GC 线程能够并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

从名字上看就知道,CMS 是基于标记-清除算法实现的。它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为七个步骤,包括:

  1. 初始标记(Initial Mark)
  2. 并发标记(Concurrent Mark)
  3. 并发预清理(Concurrent Preclean)
  4. 并发可取消的预清理(Concurrent Abortable Preclean)
  5. 最终标记(Final Remark)
  6. 并发清除(Concurrent Sweep)
  7. 并发重置(Concurrent Reset)

长期来看,CMS 垃圾回收器,是要被 G1 等垃圾回收器替换掉的。在 Java8 之后,使用它将会抛出一个警告。

Java HotSpot™ 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.

G1

G1 的全称是 Garbage­First GC,G1 的目标是用来干掉 CMS 的,它同样是一款软实时垃圾回收器。相比 CMS,G1 的使用更加人性化。比如,CMS 垃圾回收器的相关参数有 72 个,而 G1 的参数只有 26 个。

G1 在 JDK1.7 版本正式启用,是 JDK 9 以后的默认垃圾收集器,取代了 CMS 回收器。

其他的回收器,都是对某个年代的整体收集,收集时间上自然不好控制。G1 把堆切成了很多份,把每一份当作一个小目标,部分上目标很容易达成。

那又有一个面试题来啦:G1 有年轻代和老年代的区分吗?

如图所示,G1 也是有 Eden 区和 Survivor 区的概念的,只不过它们在内存上不是连续的,而是由一小份一小份组成的。

这一小份区域的大小是固定的,名字叫作小堆区(Region)。小堆区可以是 Eden 区,也可以是 Survivor 区,还可以是 Old 区。所以 G1 的年轻代和老年代的概念都是逻辑上的。

每一块 Region,大小都是一致的,它的数值是在 1M 到 32M 字节之间的一个 2 的幂值数。

但假如我的对象太大,一个 Region 放不下了怎么办?注意图中有一块面积很大的黄色区域,它的名字叫作 Humongous Region,大小超过 Region 50% 的对象,将会在这里分配。

Region 的大小,可以通过参数进行设置:-XX:G1HeapRegionSize=32M

那么,回收的时候,到底回收哪些小堆区呢?是随机的么?

这当然不是。事实上,垃圾最多的小堆区,会被优先收集。这就是 G1 名字的由来。

配置参数

通过 -XX:+PrintCommandLineFlags 参数,可以查看当前 Java 版本默认使用的垃圾回收器。比如,我的系统中 Java8 默认的收集器如下:

java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=265277248 -XX:MaxHeapSize=4244435968 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_321"
Java(TM) SE Runtime Environment (build 1.8.0_321-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.321-b07, mixed mode)

以下是一些配置参数:

  • -XX:+UseSerialGC 参数指定新生代和老年代都是用串行垃圾收集器,即 Serial + Serial Old
  • -XX:+UseParNewGC 新生代使用 ParNew,老年代使用 Serial Old
  • -XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代默认使用 Serial Old
  • -XX:+UseParallelOldGC 新生代使用 Parallel Scavenge,老年代使用 Parallel Old
  • -XX:+UseConcMarkSweepGC,表示年轻代使用 ParNew,老年代的用 CMS
  • -XX:+UseG1GC 使用 G1 垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

为了让大家有个更好的印象,请看下图。它们的关系还是比较复杂的。尤其注意 -XX:+UseParNewGC 这个参数,已经在 Java9 中就被抛弃了。

有这么多垃圾回收器和参数,那我们到底用什么?在什么地方优化呢?

目前,虽然 Java 的版本比较高,但是使用最多的还是 Java8。从 Java8 升级到高版本的 Java 体系,是有一定成本的,所以 CMS 垃圾回收器还会持续一段时间。

线上使用最多的垃圾回收器,就有 CMS 和 G1,以及 Java8 默认的 Parallel Scavenge。

  • CMS 的设置参数:-XX:+UseConcMarkSweepGC
  • Java 8 的默认参数:-XX:+UseParallelGC
  • Java 13 的默认参数:-XX:+UseG1GC

STW

我们上面提到了 STW,如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?

为了保证程序不会乱套,最好的办法就是暂停用户的一切线程。也就是在这段时间,你是不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿,什么都干不了。这个头疼的现象,就叫作 Stop the world。简称 STW。

这里衍生一个面试题: GC过程中,还能创建新对象吗?
答案是不能的。

标记阶段,大多数是要 STW 的。如果不暂停用户进程,在标记对象的时候,有可能有其他用户线程会产生一些新的对象和引用,造成混乱。

现在的垃圾回收器,都会尽量去减少这个过程。但即使是最先进的 ZGC,也会有短暂的 STW 过程。我们要做的就是在现有基础设施上,尽量减少 GC 停顿。

Stop the World 造成的系统停顿

你可能对 STW 的影响没有什么概念,我举个例子来说明下。

某个高并发服务的峰值流量是 10 万次/秒,后面有 10 台负载均衡的机器,那么每台机器平均下来需要 1w/s。假如某台机器在这段时间内发生了 STW,持续了 1 秒,那么本来需要 10ms 就可以返回的 1 万个请求,需要至少等待 1 秒钟。

在用户那里的表现,就是系统发生了卡顿。如果我们的 GC 非常的频繁,这种卡顿就会特别的明显,严重影响用户体验。

虽然说 Java 为我们提供了非常棒的自动内存管理机制,但也不能滥用,因为它是有 STW 硬伤的。

归根结底,各色的垃圾回收器就是为了解决头疼的 STW 问题,让 GC 时间更短,停顿更小,吞吐量更大。

参考资料

  • 周志明《深入理解Java虚拟机》
  • 拉勾教育 《深入浅出 Java 虚拟机》

JVM 垃圾回收机制和常见的垃圾回收器相关推荐

  1. jvm垃圾回收机制和常见算法

    这是朋友给的面试题里边的,具体地址已经找不到,只能对原作者说声抱歉了: 理论上来讲sun公司只定义了垃圾回收机制规则,而步局限于其实现算法,因此不同厂商生产的虚拟机采用的算法也不尽相同. GC(Gar ...

  2. JVM 垃圾回收机制和常见算法

    理论上来讲 Sun 公司只定义了垃圾回收机制规则而不局限于其实现算法,因此不同厂商生产的虚拟机采用的算法也不尽相同. GC(Garbage Collector)在回收对象前首先必须发现那些无用的对象, ...

  3. Java语言垃圾回收机制是什么_Java垃圾回收机制简述

    Java垃圾回收机制简述 发布时间:2020-08-22 19:50:29 来源:脚本之家 阅读:64 作者:海子 说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它 ...

  4. java垃圾回收机制_笔记 | Java垃圾回收机制

    本文经授权转载自程序员杂货铺(ID:speakFramework) 垃圾回收 最近上海的小伙伴是不是要被强垃圾分类搞疯了???哈哈哈哈 上海是个走在前列的城市啊,不光骑自行车闯红灯要被罚钱,垃圾不分类 ...

  5. java垃圾回收机制串行_Java垃圾回收机制

    Java语言是一门自动内存管理的语言,不再需要的对象可以通过垃圾回收自动进行内存释放. Java运行时内存区域划分 JVM将Java程序运行时内存区域划分成以下几个部分: 程序计数器(Program ...

  6. 简述python垃圾回收机制_python中的垃圾回收机制简述

    2020年12月5日21:47:35 王凯玉 python中的垃圾回收机制 引用计数 # 引用计数 引用计数是编程语言中的一中内存管理技术,可以将资源的被引用次数保存起来. 当引用计数为0时,资源将被 ...

  7. java垃圾回收机制标记_Java的垃圾回收机制-垃圾收集算法(一)

    当需要排查各种内存溢出,内存泄漏等问题时,当垃圾收集成为系统达到更高并发的瓶颈时,我们有必要深入GC的原理. image.png 常见垃圾回收算法 在查看垃圾回收具体过程的时候,运行程序加上: -XX ...

  8. javascript 垃圾回收机制--分代式垃圾回收机制

    以前对 javascript 的垃圾回收机制没有深入了解过.以为只是简单的标记清除法.即从根对象开始找它的引用,然后依次往后找它引用的引用,依次递归,将所有被引用的变量打上标记.然后在遍历完后,清除没 ...

  9. php7垃圾回收机制l_php5和php7垃圾回收上的区别是什么?

    php5和php7垃圾回收上的区别 PHP5和PHP7的垃圾回收机制都属于引用计数,但是在复杂数据类型的算法处理上:PHP7中zval有了新的实现方式. 最基础的变化就是 *zval 需要的内存不再是 ...

  10. Java 垃圾回收机制与几种垃圾回收算法

    一.如何确定某个对象是"垃圾"? 这一小节先了解一个最基本的问题:如果确定某个对象是"垃圾"?既然垃圾收集器的任务是回收垃圾对象所占的空间供新的对象使用,那么垃 ...

最新文章

  1. mysql主从以及读写分离(科普)
  2. Python网络爬虫与信息提取(二):网络爬虫之提取
  3. mdpi is ok
  4. 双边分支网络:兼顾特征和分类的长尾问题解决方案
  5. WordCount 实例
  6. 阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
  7. 如何从JavaScript数组中获取多个随机唯一元素?
  8. kafka依赖_kafka的简单学习
  9. day10 Python 形参顺序
  10. kernel——make menuconfig的实现原理【转】
  11. [SDOI2011]染色 BZOJ2243 树链剖分+线段树
  12. (4)数据结构-线性表补充
  13. msm8937+android7.1.1显示驱动解决显示残影问题
  14. crt 生成pem_linux下pem转crt命令_crt转pem方法
  15. nats streaming订阅
  16. 电脑按键坏掉之后的解决办法
  17. i7运行linux虚拟机会卡吗,i7 7200 linux 虚拟机
  18. 腾讯云GPU服务器NVIDIA P40 GPU、P4、T4和GPU自由卡详解
  19. Vmware虚拟机无法联网?
  20. 养生:拔火罐有什么好处?

热门文章

  1. 屏幕中间html滚动字幕,Gom引擎屏幕中间滚动大字及屏幕其他信息滚动条脚本实例...
  2. Linux操作命令提示符
  3. 2021年高压电工考试内容及高压电工考试报名
  4. 无线扩音器CE指令分析
  5. VS2003版本Jsoncpp的配置和使用
  6. 通过一个html简单实现下载功能
  7. 【1stopt】1stOpt的编程模式
  8. ML Note 3.4 - 数据降维算法 PCA / t-SNE
  9. K均值聚类算法(K-Means)
  10. matlab控制信号发生器,Matlab 跳频信号发生器