浅析垃圾回收

news2025/4/6 1:01:32

大家好,我是易安!

Java虚拟机的自动内存管理,将原本需要由开发人员手动回收的内存,交给垃圾回收器来自动回收。不过既然是自动机制,肯定没法做到像手动回收那般精准高效 ,而且还会带来不少与垃圾回收实现相关的问题。今天我们来简单剖析下垃圾回收的概念。

引用计数法与可达性分析

垃圾回收,顾名思义,便是将已经分配出去的,但却不再使用的内存回收回来,以便能够再次分配。在Java虚拟机的语境下,垃圾指的是死亡的对象所占据的堆空间。这里便涉及了一个关键的问题:如何辨别一个对象是存是亡?

我们先来讲一种古老的辨别方法:引用计数法(reference counting)。它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为0,则说明该对象已经死亡,便可以被回收了。

它的具体实现是这样子的:如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器+1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器-1。也就是说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。

除了需要额外的空间来存储计数器,以及繁琐的更新操作,引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象。

举个例子,假设对象a与b相互引用,除此之外没有其他引用指向a或者b。在这种情况下,a和b实际上已经死了,但由于它们的引用计数器皆不为0,在引用计数法的心中,这两个对象还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄露。

alt

目前Java虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列GC Roots作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。

那么什么是GC Roots呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots包括(但不限于)如下几种:

  1. Java方法栈桢中的局部变量;
  2. 已加载类的静态变量;
  3. JNI handles;
  4. 已启动且未停止的Java线程。

可达性分析可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象a和b相互引用,只要从GC Roots出发无法到达a或者b,那么可达性分析便不会将它们加入存活对象合集之中。

虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。

比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为null)或者漏报(将引用设置为未被访问过的对象)。

误报并没有什么伤害,Java虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致Java虚拟机崩溃。

Stop-the-world以及安全点

怎么解决这个问题呢?在Java虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。

Java虚拟机中的Stop-the-world是通过安全点(safepoint)机制来实现的。当Java虚拟机收到Stop-the-world请求,它便会等待所有的线程都到达安全点,才允许请求Stop-the-world的线程进行独占的工作。

我曾经看到过一种比较另类的解释:安全词。一旦垃圾回收线程喊出了安全词,其他非垃圾回收线程便会一一停下。

当然,安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。

举个例子,当Java程序通过JNI执行本地代码时,如果这段代码不访问Java对象、调用Java方法或者返回至原Java方法,那么Java虚拟机的堆栈不会发生改变,也就代表着这段本地代码可以作为同一个安全点。

只要不离开这个安全点,Java虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。

由于本地代码需要通过JNI的API来完成上述三个操作,因此Java虚拟机仅需在API的入口处进行安全点检测(safepoint poll),测试是否有其他线程请求停留在安全点里,便可以在必要的时候挂起当前线程。

除了执行JNI本地代码外,Java线程还有其他几种状态:解释执行字节码、执行即时编译器生成的机器码和线程阻塞。阻塞的线程由于处于Java虚拟机线程调度器的掌控之下,因此属于安全点。

其他几种状态则是运行状态,需要虚拟机保证在可预见的时间内进入安全点。否则,垃圾回收线程可能长期处于等待所有线程进入安全点的状态,从而变相地提高了垃圾回收的暂停时间。

对于解释执行来说,字节码与字节码之间皆可作为安全点。Java虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测。

执行即时编译器生成的机器码则比较复杂。由于这些代码直接运行在底层硬件之上,不受Java虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测。

那么为什么不在每一条机器码或者每一个机器码基本块处插入安全点检测呢?原因主要有两个。

第一,安全点检测本身也有一定的开销。不过HotSpot虚拟机已经将机器码中安全点检测简化为一个内存访问操作。在有安全点请求的情况下,Java虚拟机会将安全点检测访问的内存所在的页设置为不可读,并且定义一个segfault处理器,来截获因访问该不可读内存而触发segfault的线程,并将它们挂起。

第二,即时编译器生成的机器码打乱了原本栈桢上的对象分布状况。在进入安全点时,机器码还需提供一些额外的信息,来表明哪些寄存器,或者当前栈帧上的哪些内存空间存放着指向对象的引用,以便垃圾回收器能够枚举GC Roots。

由于这些信息需要不少空间来存储,因此即时编译器会尽量避免过多的安全点检测。

不过,不同的即时编译器插入安全点检测的位置也可能不同。以Graal为例,除了上述位置外,它还会在计数循环的循环回边处插入安全点检测。其他的虚拟机也可能选取方法入口而非方法出口来插入安全点检测。

不管如何,其目的都是在可接受的性能开销以及内存开销之内,避免机器码长时间不进入安全点的情况,间接地减少垃圾回收的暂停时间。

除了垃圾回收之外,Java虚拟机其他一些对堆栈内容的一致性有要求的操作也会用到安全点这一机制。

垃圾回收的三种方式

当标记完所有的存活对象时,我们便可以进行死亡对象的回收工作了。主流的基础回收方式可分为三种。

第一种是清除(sweep),即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

alt

清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于Java虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。

另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。

第二种是压缩(compact),即把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。

alt

第三种则是复制(copy),即把内存区域分为两等分,分别用两个指针from和to来维护,并且只是用from指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到to指针指向的内存区域中,并且交换from指针和to指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。

alt

当然,现代的垃圾回收器往往会综合上述几种回收方式,综合它们优点的同时规避它们的缺点。

垃圾回收具体实现

大部分的Java对象只存活一小段时间,而存活下来的小部分Java对象则会存活很长一段时间。

alt

(pmd中Java对象生命周期的直方图,红色的表示被逃逸分析优化掉的对象)

Java虚拟机的分代回收思想,简单来说,就是将堆空间划分为两代,分别叫做新生代和老年代。新生代用来存储新建的对象。当对象存活时间够长时,则将其移动到老年代。

Java虚拟机可以给不同代使用不同的回收算法。对于新生代,我们猜测大部分的Java对象只存活一小段时间,那么便可以频繁地采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉。

对于老年代,我们猜测大部分的垃圾已经在新生代中被回收了,而在老年代中的对象有大概率会继续存活。当真正触发针对老年代的回收时,则代表这个假设出错了,或者堆的空间已经耗尽了。

这时候,Java虚拟机往往需要做一次全堆扫描,耗时也将不计成本。(当然,现代的垃圾回收器都在并发收集的道路上发展,来避免这种全堆扫描的情况。)

我们先来看看Java虚拟机中的堆具体是怎么划分的。

Java虚拟机的堆划分

前面提到,Java虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为Eden区,以及两个大小相同的Survivor区。

默认情况下,Java虚拟机采取的是一种动态分配的策略(对应Java虚拟机参数-XX:+UsePSAdaptiveSurvivorSizePolicy),根据生成对象的速率,以及Survivor区的使用情况动态调整Eden区和Survivor区的比例。

当然,你也可以通过参数-XX:SurvivorRatio来固定这个比例。但是需要注意的是,其中一个Survivor区会一直为空,因此比例越低浪费的堆空间将越高。

alt

通常来说,当我们调用new指令时,它会在Eden区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在这里边划空间是需要进行同步的。

否则,将有可能出现两个对象共用一段内存的事故。如果你还记得前两篇我用“停车位”打的比方的话,这里就相当于两个司机(线程)同时将车停入同一个停车位,因而发生剐蹭事故。

Java虚拟机的解决方法是为每个司机预先申请多个停车位,并且只允许该司机停在自己的停车位上。那么当司机的停车位用完了该怎么办呢(假设这个司机代客泊车)?

答案是:再申请多个停车位便可以了。这项技术被称之为TLAB(Thread Local Allocation Buffer,对应虚拟机参数-XX:+UseTLAB,默认开启)。

具体来说,每个线程可以向Java虚拟机申请一段连续的内存,比如2048字节,作为线程私有的TLAB。

这个操作需要加锁,线程需要维护两个指针(实际上可能更多,但重要也就两个),一个指向TLAB中空余内存的起始位置,一个则指向TLAB末尾。

接下来的new指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。

为什么不把bump the pointer翻译成指针碰撞。这里先解释一下,在英语中我们通常省略了bump up the pointer中的up。在这个上下文中bump的含义应为“提高”。另外一个例子是当我们发布软件的新版本时,也会说bump the version number。

如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的TLAB。

当Eden区的空间耗尽了怎么办?这个时候Java虚拟机便会触发一次Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到Survivor区。

前面提到,新生代共有两个Survivor区,我们分别用from和to来指代。其中to指向的Survivior区是空的。

当发生Minor GC时,Eden区和from指向的Survivor区中的存活对象会被复制到to指向的Survivor区中,然后交换from和to指针,以保证下一次Minor GC时,to指向的Survivor区还是空的。

Java虚拟机会记录Survivor区中的对象一共被来回复制了几次。如果一个对象被复制的次数为15(对应虚拟机参数-XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果单个Survivor区已经被占用了50%(对应虚拟机参数-XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。

总而言之,当发生Minor GC时,我们应用了标记-复制算法,将Survivor区中的老存活对象晋升到老年代,然后将剩下的存活对象和Eden区的存活对象复制到另一个Survivor区中。理想情况下,Eden区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记-复制算法的效果极好。

Minor GC的另外一个好处是不用对整个堆进行垃圾回收。但是,它却有一个问题,那就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为GC Roots。

这样一来,岂不是又做了一次全堆扫描呢?

卡表

HotSpot给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。

在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。

由于Minor GC伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的同时,我们又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代对象的引用。

在Minor GC之前,我们并不能确保脏卡中包含指向新生代对象的引用。其原因和如何设置卡的标识位有关。

首先,如果想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。

这个操作在解释执行器中比较容易实现。但是在即时编译器生成的机器码中,则需要插入额外的逻辑。这也就是所谓的写屏障(write barrier,注意不要和volatile字段的写屏障混淆)。

写屏障需要尽可能地保持简洁。这是因为我们并不希望在每条引用型实例变量的写指令后跟着一大串注入的指令。

因此,写屏障并不会判断更新后的引用是否指向新生代中的对象,而是宁可错杀,不可放过,一律当成可能指向新生代对象的引用。

这么一来,写屏障便可精简为下面的伪代码。这里右移9位相当于除以512,Java虚拟机便是通过这种方式来从地址映射到卡表中的索引的。最终,这段代码会被编译成一条移位指令和一条存储指令。

CARD_TABLE [this address >> 9] = DIRTY;

虽然写屏障不可避免地带来一些开销,但是它能够加大Minor GC的吞吐率( 应用运行时间/(应用运行时间+垃圾回收时间) )。总的来说还是值得的。不过,在高并发环境下,写屏障又带来了虚共享(false sharing)。

Java对象内存布局中的虚共享问题,讲的是几个volatile字段出现在同一缓存行里造成的虚共享。这里的虚共享则是卡表中不同卡的标识位之间的虚共享问题。

在HotSpot中,卡表是通过byte数组来实现的。对于一个64字节的缓存行来说,如果用它来加载部分卡表,那么它将对应64张卡,也就是32KB的内存。

如果同时有两个Java线程,在这32KB内存中进行引用更新操作,那么也将造成存储卡表的同一部分的缓存行的写回、无效化或者同步操作,因而间接影响程序性能。

为此,HotSpot引入了一个新的参数-XX:+UseCondCardMark,来尽量减少写卡表的操作。其伪代码如下所示:

if (CARD_TABLE [this address >> 9] != DIRTY)
  CARD_TABLE [this address >> 9] = DIRTY;

Java对象的生命周期对垃圾回收的影响

前面提到,Java虚拟机的分代垃圾回收是基于大部分对象只存活一小段时间,小部分对象却存活一大段时间的假设的。

然而,现实情况中并非每个程序都符合前面提到的假设。如果一个程序拥有中等生命周期的对象,并且刚移动到老年代便不再使用,那么将给默认的垃圾回收策略造成极大的麻烦。

下面这段程序将生成64G的Java对象。并且,我通过ALIVE_OBJECT_SIZE这一变量来定义同时存活的Java对象的大小。这也是一种对于垃圾回收器来说比较直观的生命周期。

当我们使用Java 8的默认GC,并且将新生代的空间限制在100M时,试着估算当ALIVE_OBJECT_SIZE为多少时,这段程序不会触发Full GC(提示一下,如果Survivor区没法存储所有存活对象,将发生什么。)。实际运行情况又是怎么样的?

// Run with java -XX:+PrintGC -Xmn100M -XX:PretenureSizeThreshold=10000 LifetimeTest
// You may also try with -XX:+PrintHeapAtGC,-XX:-UsePSAdaptiveSurvivorSizePolicy or -XX:SurvivorRatio=N
public class LifetimeTest {
  private static final int K = 1024;
  private static final int M = K * K;
  private static final int G = K * M;

  private static final int ALIVE_OBJECT_SIZE = 32 * M;

  public static void main(String[] args) {
    int length = ALIVE_OBJECT_SIZE / 64;
    ObjectOf64Bytes[] array = new ObjectOf64Bytes[length];
    for (long i = 0; i < G; i++) {
      array[(int) (i % length)] = new ObjectOf64Bytes();
    }
  }
}

class ObjectOf64Bytes {
  long placeholder0;
  long placeholder1;
  long placeholder2;
  long placeholder3;
  long placeholder4;
  long placeholder5;
}

Java虚拟机中的垃圾回收器

针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge和Parallel New。这三个采用的都是标记-复制算法。其中,Serial是一个单线程的,Parallel New可以看成Serial的多线程版本。Parallel Scavenge和Parallel New类似,但更加注重吞吐率。此外,Parallel Scavenge不能与CMS一起使用。

针对老年代的垃圾回收器也有三个:刚刚提到的Serial Old和Parallel Old,以及CMS。Serial Old和Parallel Old都是标记-压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。

CMS采用的是标记-清除算法,并且是并发的。除了少数几个操作需要Stop-the-world之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于G1的出现,CMS在Java 9中已被废弃。

G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当Eden区、Survivor区或者老年代中的一个。它采用的是标记-压缩算法,而且和CMS一样都能够在应用程序运行过程中并发地进行垃圾回收。

G1能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是G1名字的由来。

总结

今天我介绍了垃圾回收的一些基础知识:

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。它从一系列GC Roots出发,边标记边探索所有被引用的对象。

  • 为了防止在标记过程中堆栈的状态发生改变,Java虚拟机采取安全点机制来实现Stop-the-world操作,暂停其他非垃圾回收线程。

  • 回收死亡对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。

然后我介绍了Java虚拟机中垃圾回收具体实现的一些通用知识:

  • Java虚拟机将堆分为新生代和老年代,并且对不同代采用不同的垃圾回收算法。其中,新生代分为Eden区和两个大小一致的Survivor区,并且其中一个Survivor区是空的。

  • 在只针对新生代的Minor GC中,Eden区和非空Survivor区的存活对象会被复制到空的Survivor区中,当Survivor区中的存活对象复制次数超过一定数值时,它将被晋升至老年代。

  • 因为Minor GC只针对新生代进行垃圾回收,所以在枚举GC Roots的时候,它需要考虑从老年代到新生代的引用。为了避免扫描整个老年代,Java虚拟机引入了名为卡表的技术,大致地标出可能存在老年代到新生代引用的内存区域。

本文由 mdnice 多平台发布

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

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

相关文章

《MongoDB入门教程》 - MongoDB基础介绍

前言 时间也过太快了嘛&#xff0c;一晃上次更新都又过去6个月了&#xff0c;再不更新就变成年更博客了。 对了&#xff0c;第一次疫情也过去了&#xff0c;最近开始二阳了&#xff0c;希望大家能继续挺过去吧 本篇文章初尝试使用ChatGPT进行&#xff0c;博主主要负责排版和总…

mysql使用xtrabackup方式复制过滤某一个数据库

需求&#xff1a; 搭建一个新的从库&#xff0c;只复制过滤源端数据库里的其中一个数据库workflow到新实例上。 一 操作步骤 1.1 在目标端新建一个数据库实例 略 1.2 在源端做备份 /home/urman-agent/bin/xtrabackup --defaults-file/data/mysql/etc/13314/my.cnf --targ…

redis缓存穿透、缓存击穿、缓存雪崩

一、缓存 缓存是数据交换的缓冲区&#xff0c;是存储数据的临时地方&#xff0c;一般读写性能较高。 如浏览器会把静态资源先加载到浏览器缓存中&#xff0c;tomcat中有应用层缓存等&#xff0c;则数据库也有数据库缓存。 缓存的作用&#xff1a; 降低后端负载提高读写效率…

ES8基本命令

ElasticSearch是面向文档型数据库 构造类似于数据库&#xff1a;indexes(索引库数据库)-->types(类型表)-->documents(文档行)-->field(字段字段) 但是在7.X开始&#xff0c;Type已经开始被废弃。 正排索引&#xff1a;根据主键id关联内容&#xff0c;然后再找关键字…

微服务—Redis实用篇-黑马头条项目-达人探店功能(使用set与zset实现)

微服务—Redis实用篇-黑马头条项目-达人探店功能(使用set与zset实现) 1、达人探店 1.1、达人探店-发布探店笔记 发布探店笔记 探店笔记类似点评网站的评价&#xff0c;往往是图文结合。对应的表有两个&#xff1a; tb_blog&#xff1a;探店笔记表&#xff0c;包含笔记中的标…

Liunx基础命令 - mkdir命令

mkdir命令 – 创建目录文件 mkdir命令来自英文词组”make directories“的缩写&#xff0c;其功能是用来创建目录文件。使用方法简单&#xff0c;但需要注意若要创建的目标目录已经存在&#xff0c;则会提示已存在而不继续创建&#xff0c;不覆盖已有文件。而目录不存在&#…

路径规划算法:基于蜉蝣优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于蜉蝣优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于蜉蝣优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法蜉蝣…

Linux 无网环境下离线安装rpm

概述 搭建了私有yum仓库&#xff0c;想实现无网环境下Docker rpm离线安装的方法 1. 使用和配置清华源 2. 免安装下载rpm包及其依赖 3. 寻找特定的rpm包并补全依赖 清华的清华源 名称连接帮助文档备注主页清华大学开源软件镜像站 | Tsinghua Open Source MirrorAOSP | 镜像站使…

Swagger 3.0 与 Springboot 集成

springboot版本:2.3.12.RELEASE swagger版本:3.0.0 1&#xff1a;pom文件添加如下代码: <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version>&…

离线安装 Docker + Docker registry(Docker仓库)

概述 通过私有yum仓库安装docker 具体内容&#xff1a; 1. 构建私有镜像仓库 2. 准备rpm包 3. 安装部署 4. ansible 安装 卸载 5. shell 脚本安装 卸载 6. 网页连接 7. 构建docker 私有仓库 1. 构建私有镜像仓库 构建简单的yum私有仓库请参考如下文档 http://t.csdn.cn/22bmb…

用chatGPT写chatGPT教学方案

最近从机器学习的原理、数学推理、主流模型架构、chatGPT的Prompt策略&#xff0c;也实际体验了各种AIGC工具&#xff0c;算是较为系统的学习了chatGPT的前世今身&#xff0c;想着有很多人应该跟我一样&#xff0c;没有编程基础、也没有成为chatGPT专家的希望&#xff0c;仅仅通…

LeetCode_Day3 | 反转链表/移除链表的元素/设计个链表

LeetCode_链表 203.移除链表元素1. 题目描述2. 直接使用原表删除1. 思路2. 代码实现 3. 使用虚拟头节点删除1. 思路2. 代码实现 707.设计链表1.题目描述2.单链表&#xff1a;虚拟头节点设计1. 思路2. 代码实现及部分逻辑解释3. 需要注意的点 206.反转链表1.题目描述2. 双指针法…

BIO阻塞模型

作者&#xff1a;V7 博客&#xff1a;https://www.jvmstack.cn 一碗鸡汤 少年辛苦终身事&#xff0c;莫向光阴惰寸功。 —— 杜荀鹤 同步阻塞IO 在介绍阻塞和非阻塞之前先说明一下同步和异步。我们可以将同步和异步看做是发起IO请求的两种方式。同步IO指的是用户空间&…

项目美术部门敏捷开发流程及工具

前言 在项目开发中&#xff0c;针对美术部门的特性和工作风格&#xff0c;合理使用多种工具进行项目管理和进度控制非常必要。如果项目执行中有异地协作、美术内/外包、多项目并行的情况&#xff0c;正确的规划和管理的优势会更加凸显。 而合理使用整套的综合管理工具会让项目…

Day969.如何拆分代码 -遗留系统现代化实战

如何拆分代码 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于如何拆分代码的内容。 当完成了项目的战略设计&#xff0c;大体设计出目标架构&#xff0c;又根据系统的现状&#xff0c;决定采用“战术分叉”的方式进行微服务拆分之后&#xff0c;接下来的难点就变成…

用GPT-4 写2022年天津高考作文能得多少分?

正文共 792 字&#xff0c;阅读大约需要 3 分钟 学生必备技巧&#xff0c;您将在3分钟后获得以下超能力&#xff1a; 积累作文素材 Beezy评级 &#xff1a;B级 *经过简单的寻找&#xff0c; 大部分人能立刻掌握。主要节省时间。 推荐人 | Kim 编辑者 | Linda ●图片由Lexica …

vue工程搭建

1&#xff1a;查看vue及npm版本&#xff1a; 2&#xff1a;执行npm init nuxt-app <project-name>语句&#xff1a; 若出现npm ERR! code ENOLOCAL 请执行如下语句&#xff1a; npm cache verify npm cache clean --force npm i -g npm npm install -g cnpm --regis…

数据库(mysql语句)循环语句

例题1&#xff1a; 20到50之间能被5除余1的所有自然数的和 EDECLARE i int DECLARE s int SET s0 SET i20 白WHILE i <50 BEGIN IF(i%51) SET s s i SET ii1 END PRINT20到50之间能被5除余1的所有自然数的和是cast(s as varchar(20)) 例题2&#xff1a; 实现如下图 代码…

设计模式之门面模式(Facade Pattern 外观模式)

一、模式定义 门面模式(Facade Pattern)&#xff1a;外部与一个子系统的通信必须通过一个统一的外观对象进行&#xff0c;为子系统中的一组接口提供一个一致的界面&#xff0c;外观模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用。门面模式又称为外观…

中国南方Oracle用户组沙龙活动:大环境下的Oracle数据库的机遇与挑战

2023年03月12日(周日)在杭州索菲特西湖大酒店 (浙江省杭州市上城区西湖大道333 号)&#xff0c;中国南方Oracle用户组创始人之一&#xff1a;周亮&#xff08;zhou liang&#xff09;组织举办了主题为《大环境下的Oracle数据库的机遇与挑战》活动&#xff0c;大约有50名左右的人…