GC 基础入门

news2025/2/20 5:22:27

什么是GC(Garbage Collection)?

内存管理方式通常分为两种:

  • 手动内存管理(Manual Memory Management)
  • 自动内存管理(Garbage Collection, GC)
手动内存管理

手动内存管理是指开发者直接管理内存的分配和释放。典型的语言如C、C++使用malloc/freenew/delete等来手动分配和释放内存。

优点:

  • 精确控制内存的分配和释放。
  • 可预测的性能,没有GC带来的额外开销。
  • 不需要的内存可以立即释放,从而节省资源。

缺点:

  • 存在**内存泄漏(Memory Leak)**问题。
  • 存在**悬挂指针(Dangling Pointer)**问题。
  • 开发复杂度增加。

内存泄漏: 分配的内存未被释放,导致程序运行期间内存占用持续增加,可能导致系统内存耗尽而崩溃。 悬挂指针: 已释放的内存被引用时,可能会引发不可预测的行为。

近年来,Rust通过所有权系统(Ownership System)提供了一种安全的内存管理方式,这并不是完全的手动内存管理,而是手动内存管理的一种新替代方案。

GC(垃圾回收)

GC是编程语言中自动回收不再使用的内存的功能。接下来我们将详细解释这一机制。

自由存储列表(Free-Storage List)

在任何时刻,只有一部分为列表结构预留的内存实际上用于存储S表达式(S-expressions)。其余的寄存器(在我们的系统中大约有15,000个)组成了一个称为**自由存储列表(free-storage list)**的单一列表。程序中的某个特定寄存器FREE包含该列表的第一个寄存器位置。

当需要额外的列表结构时,自由存储列表的第一个单词将被使用,并且寄存器FREE的值会更新为自由存储列表的第二个单词位置。用户不需要编程将寄存器返回到自由存储列表中。

——《Recursive Functions of Symbolic Expressions Their Computation by Machine, Part I》John McCarthy

GC的概念始于1960年,约翰·麦卡锡(John McCarthy)在其论文《Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I》中介绍了自由存储列表(Free-Storage List)的概念,这是后来GC(垃圾回收)的基础思想。

为什么需要这个概念?

LISP(LISt Processor)是一种高级语言,支持符号处理(Symbolic Processing)和递归调用(Recursion)。然而,当时的计算机技术难以应对LISP复杂的内存管理问题。LISP在运行过程中不断生成对象,未使用的对象(垃圾)堆积会导致严重的内存泄漏问题。由于程序员很难完全追踪所有的内存分配和释放,因此引入了自动内存管理技术。

自由存储列表(Free-Storage List)是早期LISP系统中实现自动内存管理的基础概念。当时LISP采用动态生成列表结构的方式,程序员显式释放内存非常困难。为了解决这个问题,引入了跟踪未使用内存块的Free-Storage List概念,这最终发展为自动内存管理的思想。其基本原理是使用特定寄存器(FREE)存储可用内存列表的起始位置,按需获取内存并在不再使用时将其返回。这种思想成为了后来Mark-and-Sweep方式GC的基础。

Newell-Shaw-Simon 的列表存储方式的一个重要特点是,即使相同的数据多次出现,也可以只在计算机内存中存储一次。也就是说,列表可以“重叠(overlapped)”。
然而,这种重叠会在删除(erasure)过程中引发问题。当不再需要某个列表时,必须选择性地删除那些没有与其他列表重叠的部分。在LISP中,麦卡锡(McCarthy)提出了一个优雅但低效的解决方案。
本文则描述了一种能够实现高效删除的通用方法。
——《A Method for Overlapping and Erasure of Lists》 (1963),George E. Collins

尽管约翰·麦卡锡的论文奠定了Mark-and-Sweep方法的基础,但在执行GC时会导致程序暂停(Stop-the-World)现象。这使得实现实时GC变得困难。因此,Collins提出了引用计数(Reference Counting)的概念作为GC方法。
该方法通过为每个对象保存一个引用计数(Reference Count),并在引用计数变为0时立即回收内存。

Collins(1960年)提出的引用计数GC虽然实现了即时内存回收,但由于循环引用问题,并不是一个完美的解决方案。
尽管如此,现代语言如Python和Swift仍然使用这种方法,但加入了一些补充技术来解决其缺陷。

(顺便提一句,“优雅但低效”这样的评价让我觉得有趣。程序员这个职业似乎总有一种特点,他们喜欢用“优雅”的方式讽刺对方。也许是因为讨论的是抽象概念,缺乏实体暴力的存在感?在韩国,如果有人这样说话,可能会被拳头教训吧。)

《A LISP Garbage Collector for Virtual-Memory Computer Systems》论文摘要

本文提出了一种适用于非常大的虚拟内存环境的列表处理系统的垃圾回收算法。
该算法的主要目的不是“释放空闲内存”,而是压缩活动内存(compaction)
在虚拟内存系统中,由于空闲内存实际上并不会耗尽,因此很难决定何时触发垃圾回收。因此,本文讨论了触发垃圾回收的各种条件。
——《A LISP Garbage Collector for Virtual-Memory Computer Systems》 (Fenichel & Yochelson, 1969)

(虚拟内存(Virtual Memory):一种逻辑上管理超出物理内存大小的大容量内存的技术。)

这篇论文改进了Minsky的复制式GC(Copying GC),使其能够在虚拟内存环境中高效运行。
本文实际实现了Minsky的垃圾回收器,它利用深度优先搜索(DFS, Depth-First Search)将可达数据复制到辅助存储器中,分配到新的连续地址后重新加载回内存。尽管有评论认为其实现完成度较低(因为稍后会详细解释,所以这里不需要完全理解),这篇论文是首次将Minsky的方法应用于现代虚拟内存环境中的“Copying GC”,如果没有这篇论文,Java和C#中的分代GC(Generational GC)概念可能就不会出现。

“提出了一种简单的非递归(nonrecursive)列表结构压缩方法或垃圾收集器。该算法适用于紧凑(compact)结构和LISP风格的列表结构。通过逐步利用部分结构来追踪复制的列表,从而消除了对递归的依赖。” — (C. J. Cheney, 1970)

随后,使用广度优先搜索(BFS, Breadth-First Search)而非深度优先搜索(DFS),设计了一种“非递归(Nonrecursive)”的GC,使其能够在没有栈的情况下运行。
这项研究对Java、C#、Python的Copying GC方式产生了重要影响,基于BFS的设计避免了栈溢出问题,并具有缓存友好的结构。

为什么BFS可以以非递归的方式实现,而DFS不能?

首先,大多数读者应该知道,栈是一种**后进先出(LIFO, Last In, First Out)的数据结构。最后添加的元素会最先被移除。
队列则是一种
先进先出(FIFO, First In, First Out)**的数据结构。也就是说,最先添加的元素会最先被移除。

DFS的工作原理

DFS采用一种沿着某条路径深入探索到底,然后回溯(backtracking)的方式。为了实现这一点,DFS需要使用栈(Stack)或递归函数(Recursive Call)。
DFS基于栈的原理如下:

  1. 将当前节点存储在栈中(或通过递归调用)。
  2. 如果有下一个要访问的节点,则继续通过栈调用(或递归调用)进行处理。
  3. 当没有更多可前进的地方时,执行回溯(返回上一步)。
BFS的工作原理

BFS则是基于队列的原理:

  1. 将起始节点添加到队列中(Enqueue)。
  2. 从队列中取出一个节点(Dequeue),访问它,并将其相邻节点重新添加到队列中。
  3. 重复上述过程,直到访问完所有节点。

由于BFS使用队列,因此不需要额外的递归调用。


以洞穴探险为例

DFS

在洞穴探险时,DFS会选择一条路一直走到尽头,如果路被堵住,就会回溯并尝试新的路径。
当路被堵住时,必须记住(存储)返回的路径,否则无法回到原点。

BFS

BFS会在洞口布置一支队伍,同时探索所有的岔路。先确认第一层的所有路径,然后再进入下一层。
为了合理分配队伍,必须优先安排最早到达的岔路。

GC的发展历程

年代

研究者

GC算法

改进点

问题

1960

John McCarthy

Mark-and-Sweep

首次引入GC概念

Stop-the-World,碎片化问题

1963

Marvin L. Minsky

Copying GC(基于DFS)

解决内存碎片化,优化缓存

循环引用问题,磁盘使用

1969

Fenichel & Yochelson

Copying GC(虚拟内存应用)

使用两个半空间(Semispaces)

内存不足时程序变慢

1970

C. J. Cheney

非递归Copying GC(基于BFS)

无需栈即可进行GC

BFS基础,内存重定位优化较少

GC算法发展的意义是什么?

通过减少“Stop-the-World”问题,并逐步向实时(Concurrent)GC发展,我们可以看到GC的进步过程。

那么现在让我们总结一下垃圾回收器有哪些算法,以及这些算法的概述。

在进入主题之前,先简单介绍一些基础知识:

  • 我们通常将内存管理分为两个池:堆内存和栈内存。
  • 栈内存主要用于存储小型(短生命周期)的数据(主要是值类型)。
  • 堆内存则用于存储更大、更持久的数据(主要是引用类型)。

GC算法大致可以分为两类:

1. Tracing GC(追踪型GC)

这是最常见的GC类型。
垃圾回收通过可达性(Reachability)来判断某个对象是否为垃圾。
从根(Root,如全局变量、栈变量等)开始查找可达对象(Reachable),然后销毁不可达对象(Unreachable)。
如果没有有效的引用,对象会被标记为unreachable并被回收。
常用的算法主要有以下三种(附带代码仅为作者学习核心概念实现的伪代码,不保证实际运行效果)。

1-1. Mark-and-Sweep(标记-清除)

(ref: Demystifying memory management in modern programming languages | Technorage)

#include <iostream>
#include <vector>
#include <algorithm>

// Mark-and-Sweep GC 实现(使用原始指针)
class Object {
public:
    bool marked;                       // GC 标记状态
    std::vector<Object*> children;     // 子对象列表
    Object() : marked(false) {}
    
    // 添加子对象的方法
    void addChild(Object* child) {
        children.push_back(child);
    }
    
    // 析构函数:对象被删除时打印日志(用于调试)
    ~Object() {
        std::cout << "Deleting Object at " << this << std::endl;
    }
};

// 全局堆和根对象容器
std::vector<Object*> heap;
std::vector<Object*> roots;

// 递归标记对象及其子对象
void mark(Object* obj) {
    if (obj == nullptr || obj->marked) {
        return;
    }
    obj->marked = true;
    for (Object* child : obj->children) {
        mark(child);
    }
}

// 清除阶段:删除未标记的对象并从堆中移除
void sweep() {
    auto it = heap.begin();
    while (it != heap.end()) {
        Object* obj = *it;
        if (!obj->marked) {
            // 释放无法到达的对象
            delete obj;
            it = heap.erase(it);
        } else {
            // 复位标记以便下次 GC 运行
            obj->marked = false;
            ++it;
        }
    }
    
    // 更新根对象列表,仅保留仍在堆中的对象
    std::vector<Object*> newRoots;
    for (Object* root : roots) {
        if (std::find(heap.begin(), heap.end(), root) != heap.end()) {
            newRoots.push_back(root);
        }
    }
    roots = newRoots;
}

// 执行完整的 GC 过程:标记并清除
void gc() {
    for (Object* root : roots) {
        mark(root);
    }
    sweep();
}

int main() {
    // 创建对象并加入堆中
    Object* a = new Object();
    Object* b = new Object();
    Object* c = new Object();
    
    // 设定对象间的引用关系
    a->addChild(b);
    b->addChild(c);
    
    // 将所有对象加入堆
    heap.push_back(a);
    heap.push_back(b);
    heap.push_back(c);
    
    // 设定根对象(此处 a 作为根对象)
    roots.push_back(a);
    
    std::cout << "Heap size before GC: " << heap.size() << std::endl;
    
    // 执行垃圾回收
    gc();
    
    std::cout << "Heap size after GC: " << heap.size() << std::endl;
    
    // 释放剩余的对象(实际系统中最终 GC 运行时会处理)
    for (Object* obj : heap) {
        delete obj;
    }
    heap.clear();
    roots.clear();
    
    return 0;
}

(如果在代码中使用C++的RAII特性并通过shared_ptr管理内存,则很难观察到GC的实际操作,因此需要使用原始指针。)

  1. Mark阶段 :识别正在使用的对象。
  2. Sweep阶段 :删除不可达对象。
  3. 出现内存碎片化(Fragmentation)问题。

简而言之,从Root Space找到连接的对象,并删除未连接的对象。
优点是实现相对简单,且能有效保留必要的对象,避免浪费内存。
缺点是会出现“Stop-the-World”现象,并产生内存碎片化问题。

内存碎片化 :当内存单元在堆中分配时,其大小取决于存储变量的大小。回收内存时,堆内存会分裂成碎片。

  • 内部碎片化 :已分配内存块中未使用的空间(例如分配4KB但只使用1KB)。
  • 外部碎片化 :可用内存分散,无法分配大块内存。

(img ref: Visualizing memory management in V8 Engine (JavaScript, NodeJS, Deno, WebAssembly) | Technorage )

1-2. Mark-Compact(标记-整理)

在Mark阶段之后,将存活对象移动到内存的一侧以进行整理(Compaction),从而解决碎片化问题。
缺点是整理过程中会产生额外的开销,GC时间较长。

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

constexpr std::size_t HEAP_SIZE = 1024;

// 对象类 (简单链表结构)
class Object {
public:
    int value;
    Object* next;
    explicit Object(int val) : value(val), next(nullptr) {}

    // 析构函数 (调试用途)
    ~Object() {
        std::cout << "Deleting Object with value " << value << " at " << this << std::endl;
    }
};

// 使用两个半空间 (Semispace) 进行内存管理
std::vector<char> fromSpace(HEAP_SIZE);
std::vector<char> toSpace(HEAP_SIZE);

std::size_t fromIndex = 0;
std::size_t toIndex = 0;

// 在给定的空间中分配对象 (使用 placement new)
Object* allocate(int val, std::vector<char>& space, std::size_t& idx) {
    if (idx + sizeof(Object) > space.size()) {
        return nullptr; // 空间不足
    }
    Object* obj = new (&space[idx]) Object(val);
    idx += sizeof(Object);
    return obj;
}

// 递归复制对象及其链接
Object* copy(Object* obj, std::vector<char>& targetSpace, std::size_t& targetIdx) {
    if (obj == nullptr) {
        return nullptr;
    }
    Object* newObj = allocate(obj->value, targetSpace, targetIdx);
    if (newObj == nullptr) {
        return nullptr;
    }
    newObj->next = copy(obj->next, targetSpace, targetIdx);
    return newObj;
}

// 复制垃圾回收 (Copying GC) - 复制所有可达对象到新的空间
void gc(Object*& root) {
    toIndex = 0;
    Object* newRoot = copy(root, toSpace, toIndex);

    // 交换空间,使新分配的空间成为新的堆
    std::swap(fromSpace, toSpace);
    fromIndex = toIndex;
    root = newRoot;
}

int main() {
    // 在 fromSpace 中分配对象
    Object* a = allocate(10, fromSpace, fromIndex);
    if (a == nullptr) {
        std::cerr << "Allocation failed for object a." << std::endl;
        return 1;
    }
    Object* b = allocate(20, fromSpace, fromIndex);
    if (b == nullptr) {
        std::cerr << "Allocation failed for object b." << std::endl;
        return 1;
    }
    a->next = b;

    std::cout << "Before GC, root object value: " << a->value << std::endl;

    // 运行 GC
    gc(a);

    std::cout << "After GC, copied root object value: " << a->value << std::endl;

    return 0;
}
1-3. Copying GC(复制型GC)

将内存分为两个半空间(Semi-Space),分别称为From-Space和To-Space。
仅将可达对象复制到新空间(To-Space),然后销毁旧空间。
优点是没有内存碎片化,分配速度快;缺点是内存使用效率低(仅使用一半的内存)。

2. Reference Counting(引用计数)
#include <iostream>
#include <unordered_map>

// 对象类:存储简单整数值
class Object {
public:
    int value;
    explicit Object(int val) : value(val) {}

    // 析构函数: 当对象被释放时打印日志
    ~Object() {
        std::cout << "Deleting Object with value " << value << " at " << this << std::endl;
    }
};

// 全局引用计数表
std::unordered_map<Object*, int> refTable;

// 创建对象并初始化其引用计数
Object* createObject(int val) {
    Object* obj = new Object(val);
    refTable[obj] = 1;
    return obj;
}

// 增加对象的引用计数
void addRef(Object* obj) {
    if (obj != nullptr) {
        auto it = refTable.find(obj);
        if (it != refTable.end()) {
            ++(it->second);
        }
    }
}

// 释放对象的引用:当引用计数为0时删除对象
void releaseRef(Object* obj) {
    if (obj != nullptr) {
        auto it = refTable.find(obj);
        if (it != refTable.end()) {
            if (--(it->second) == 0) {
                refTable.erase(it);
                delete obj;
            }
        }
    }
}

int main() {
    // 创建对象
    Object* a = createObject(10);
    Object* b = createObject(20);

    // 增加 b 的引用计数 (模拟多个地方使用)
    addRef(b);

    // 释放 b 的引用:两次调用后计数变为0,对象被释放
    releaseRef(b);
    releaseRef(b);

    // 释放 a
    releaseRef(a);

    std::cout << "Remaining objects in refTable: " << refTable.size() << std::endl;
    return 0;
}

(使用全局引用表unordered_map,在创建对象时将引用计数初始化为1,并通过addRefreleaseRef调整引用计数。当引用计数为0时调用delete。)

每个对象存储一个引用计数(Reference Count),当引用计数为0时立即释放内存。
由于可以立即释放内存,因此没有“Stop-the-World”现象,且对象的使用量清晰可见。
缺点是存在循环引用问题,并且在处理大型对象时性能下降。
Swift中使用了改进版的ARC(Automatic Reference Counting)。

核心总结

分类

Tracing GC (Mark & Sweep)

Reference Counting GC

基本概念

从根开始追踪并清理对象

根据对象的引用计数释放内存

内存泄漏可能性

可能存在循环引用(GC自动解决)

循环引用导致内存泄漏

GC执行成本

与对象数量和引用图大小成正比

对象越多计算负担越大

性能

对象越多性能越可能下降

引用计数立即归零时快速释放

Stop-the-World(STW)

GC执行时暂停

立即释放,无需暂停

典型应用场景

C#, Java, Python

Objective-C (ARC), Swift

此外,除了基本的GC算法外,还有一些优化策略。

GC优化策略有哪些?

最常见的是Generational GC(分代GC)

1. Generational GC(分代GC)

根据对象的生存周期将其分为Young Generation和Old Generation,并对新生代(Young)快速GC,对老年代(Old)缓慢GC。
接下来我们看看Young和Old是如何划分的。

1-1. Young Generation(Eden + Survivor Space)
  • Eden(伊甸园)区域
    新创建的对象首先分配到这里。大多数对象在这里生成后很快就会被销毁,因此GC频繁发生。
  • Survivor(幸存者)区域
    Eden区域中经过GC后存活的对象会移动到这里。通常有两个Survivor区域。对象多次经历GC后最终晋升到Old Generation。
    Survivor区域通常分为两个:Survivor0(From Space)和Survivor1(To Space)。
    在这里,新创建的对象存储在此,大部分对象会快速销毁。此区域使用Copying GC快速移除对象。
    当该区域中的对象被移除时,称为Minor GC。
1-2. Old Generation(老年代)
  • Tenured(老年)区域
    多次在Young Generation中经历GC后存活的对象会移动到这里。
    此区域GC较少发生,但一旦发生,涉及的对象较多,处理成本较高。
    老年代对象存储在这里,使用Mark-Sweep或Mark-Compact方式进行清理。
    利用对象生命周期模式进行性能优化。当此区域中的对象被移除时,称为Major GC(或Full GC)。
    Young GC快速执行,Old GC较慢执行,但在Old Generation中发生GC时可能会出现“Stop-the-World”。

通常,Old区域分配较大,因此GC发生的频率低于Young区域。

可以用人类一生的例子来说明:

  1. 在Eden(伊甸园)中以灵魂状态存在;
  2. 接受世界的召唤出生;
  3. 经历世间的风雨洗礼(Survivor),无事故地幸存下来;
  4. 成为老人,获得社会尊重(Promotion);
  5. 受人尊敬的老人去世后,社会上更多的人哀悼(比年轻人更大的哀悼成本)。

2. Parallel GC(并行GC)

使用多个线程(Thread)并行执行GC,提升GC速度。适用于大型应用程序,但可能存在线程同步开销。Java HotSpot JVM实现了Parallel GC。
需要注意的是,分代GC和并行GC并不互斥,因此可以同时使用,实际上JVM HotSpot和.NET中也确实如此。

(Time Freeze!!!)

那么GC是如何执行的呢?

1. Stop-the-World(STW)

GC执行时程序完全停止。
这是传统的GC方式,实现相对简单,能够准确回收内存。
但会导致响应时间增加,用户体验下降。

(img ref:An attempt at visualizing the Go GC)

2. Incremental GC(增量GC)

将GC分成多个小步骤(Small Steps)执行,基于Dijkstra的三色标记算法(Tri-Color Marking Algorithm)。

这个算法的核心是高效地标记三种颜色:

  • 白色 :未处理(Unprocessed)
  • 灰色 :处理中(Processing)
  • 黑色 :已处理(Processed)

节点着色的规则如下:

  1. 所有节点初始为白色。
  2. 访问对象时标记为灰色。
  3. 访问完成后标记为黑色。

这样就形成了三个集合,对象从白色→灰色→黑色移动。重要的是,白色和黑色之间不会直接连接。

3. Concurrent GC(并发GC)

GC与应用程序同时运行,尽量减少“Stop-the-World”时间。
GC线程独立运行,并与其他应用程序线程同时进行内存标记(mark)或复制等操作。
这使得GC执行期间应用程序仍能运行,并利用多核优势,但内存管理变得更加复杂。

“过早优化是万恶之源。”——Donald Knuth

本文介绍了GC的历史、当前使用的GC算法、优化策略及执行方法。后续还提到了避免GC的ZoC(Zero Allocation)等内容,但由于本文面向初学者,部分内容(如某些优化策略)被省略了。

喜欢手动管理内存的人可能会将GC视为罪恶,认为它增加了编程的不确定性。
但正如Donald Knuth所说,与其沉迷于内存管理这样的“过早优化”,不如让GC帮助开发者专注于解决问题。
程序员的角色不仅是掌握技术,更是解决眼前的问题。
GC为许多程序员减轻了“内存管理”的负担,是一项重要的工具。

重要的是,不是“GC好还是不好”,而是思考“哪种工具最适合解决我的问题”。

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

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

相关文章

UE求职Demo开发日志#32 优化#1 交互逻辑实现接口、提取Bag和Warehouse的父类

1 定义并实现交互接口 接口定义&#xff1a; // Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "UObject/Interface.h" #include "MyInterActInterface.generated.h…

如何在 Mac 上解决 Qt Creator 安装后应用程序无法找到的问题

在安装Qt时&#xff0c;遇到了一些问题&#xff0c;尤其是在Mac上安装Qt后&#xff0c;发现Qt Creator没有出现在应用程序中。通过一些搜索和操作&#xff0c;最终解决了问题。以下是详细的记录和解决方法。 1. 安装Qt后未显示Qt Creator 安装完成Qt后&#xff0c;启动应用程…

多线程基础面试题剖析

一、线程的创建方式有几种 创建线程的方式有两种&#xff0c;一种是继承Thread&#xff0c;一种是实现Runable 在这里推荐使用实现Runable接口&#xff0c;因为java是单继承的&#xff0c;一个类继承了Thread将无法继承其他的类&#xff0c;而java可以实现多个接口&#xff0…

Android设备 网络安全检测

八、网络与安全机制 6.1 网络框架对比 volley&#xff1a; 功能 基于HttpUrlConnection;封装了UIL图片加载框架&#xff0c;支持图片加载;网络请求的排序、优先级处理缓存;多级别取消请求;Activity和生命周期的联动&#xff08;Activity结束生命周期同时取消所有网络请求 …

神经网络的学习 求梯度

import sys, ossys.path.append(os.pardir) import numpy as npfrom common.functions import softmax, cross_entropy_error from common.gradient import numerical_gradient# simpleNet类 class simpleNet:def __init__(self):self.W np.random.rand(2, 3) # 随机形状为2*…

AI向量数据库之LanceDB快速介绍

LanceDB LanceDB 是一个开源的向量搜索数据库&#xff0c;具备持久化存储功能&#xff0c;极大地简化了嵌入向量的检索、过滤和管理。 LanceDB的主要特点 LanceDB 的主要特点包括&#xff1a; 生产级向量搜索&#xff1a;无需管理服务器。 存储、查询和过滤向量、元数据以…

CentOS7 安装配置FTP服务

CentOS7 安装配置FTP服务 CentOS7 安装配置FTP服务1. FTP简介2. 先行准备2.1 关闭防火墙2.2 关闭 SELinux 3.安装FTP软件包4. 创建 FTP 用户及目录4.1 创建 FTP 目录并设置权限4.2 防止 FTP 用户登录 Linux 终端4.3 创建 FTP 用户组及用户4.4 创建 FTP 可写目录 5. 配置ftp服务…

【设计模式】03-理解常见设计模式-行为型模式(专栏完结)

前言 前面我们介绍完创建型模式和创建型模式&#xff0c;这篇介绍最后的行为型模式&#xff0c;也是【设计模式】专栏的最后一篇。 一、概述 行为型模式主要用于处理对象之间的交互和职责分配&#xff0c;以实现更灵活的行为和更好的协作。 二、常见的行为型模式 1、观察者模…

编程题-最大子数组和(中等-重点【贪心、动态规划、分治思想的应用】)

题目&#xff1a; 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组是数组中的一个连续部分。 解法一&#xff08;枚举法-时间复杂度超限&#xff09;&#xff1a; …

本地通过隧道连接服务器的mysql

前言 服务器上部署了 mysql&#xff0c;本地希望能访问该 mysql&#xff0c;但是又不希望 mysql 直接暴露在公网上 那么可以通过隧道连接 ssh 端口的方式进行连接 从外网看&#xff0c;服务器只开放了一个 ssh 端口&#xff0c;并没有开放 3306 监听端口 设置本地免密登录 …

2. grafana插件安装并接入zabbix

一、在线安装 如果不指定安装位置&#xff0c;则默认安装位置为/var/lib/grafana/plugins 插件安装完成之后需要重启grafana 命令在上一篇讲到过 //查看相关帮助 [rootlocalhost ~]# grafana-cli plugins --help //从列举中的插件过滤zabbix插件 [rootlocalhost ~]# grafana…

Linux第107步_Linux之PCF8563实验

使用PCF8563代替内核的RTC&#xff0c;可以降低功耗&#xff0c;提高时间的精度。同时有助于进一步熟悉I2C驱动的编写。 1、了解rtc_time64_to_tm()和rtc_tm_to_time64() 打开“drivers/rtc/lib.c” /* * rtc_time64_to_tm - Converts time64_t to rtc_time. * Convert seco…

功能说明并准备静态结构

功能说明并准备静态结构 <template><div class"card-container"><!-- 搜索区域 --><div class"search-container"><span class"search-label">车牌号码&#xff1a;</span><el-input clearable placeho…

[免费]SpringBoot公益众筹爱心捐赠系统【论文+源码+SQL脚本】

大家好&#xff0c;我是老师&#xff0c;看到一个不错的SpringBoot公益众筹爱心捐赠系统&#xff0c;分享下哈。 项目介绍 公益捐助平台的发展背景可以追溯到几十年前&#xff0c;当时人们已经开始通过各种渠道进行公益捐助。随着互联网的普及&#xff0c;本文旨在探讨公益事业…

ML.Net二元分类

ML.Net二元分类 文章目录 ML.Net二元分类前言项目的创建机器学习模型的创建添加模型选择方案训练环境的选择训练数据的添加训练数据的选择训练数据的格式要预测列的选择模型评估模型的使用总结前言 ‌ML.NET‌是由Microsoft为.NET开发者平台创建的免费、开源、跨平台的机器学习…

visutal studio 2022使用qcustomplot基础教程

编译 下载&#xff0c;2.1.1版支持到Qt6.4 。 拷贝qcustomplot.h和qcustomplot.cpp到项目源目录&#xff08;Qt project&#xff09;。 在msvc中将它俩加入项目中。 使用Qt6.8&#xff0c;需要修改两处代码&#xff1a; L6779 # if QT_VERSION > QT_VERSION_CHECK(5, 2, …

本地搭建自己的专属客服之OneApi关联Ollama部署的大模型并创建令牌《下》

这里写目录标题 OneApi1、渠道设置2、令牌创建 配置文件修改修改配置文件docker-compose.yml修改config.json到此结束 上文讲了如何本地docker部署fastGtp&#xff0c;相信大家也都已经部署成功了&#xff01;&#xff01;&#xff01; 今天就说说怎么让他们连接在一起 创建你的…

【C】初阶数据结构4 -- 双向循环链表

之前学习的单链表相比于顺序表来说&#xff0c;就是其头插和头删的时间复杂度很低&#xff0c;仅为O(1) 且无需扩容&#xff1b;但是对于尾插和尾删来说&#xff0c;由于其需要从首节点开始遍历找到尾节点&#xff0c;所以其复杂度为O(n)。那么有没有一种结构是能使得头插和头删…

小爱音箱控制手机和电视听歌的尝试

最近买了小爱音箱pro&#xff0c;老婆让我扔了&#xff0c;吃灰多年的旧音箱。当然舍不得&#xff0c;比小爱还贵&#xff0c;刚好还有一台红米手机&#xff0c;能插音箱&#xff0c;为了让音箱更加灵活&#xff0c;买了个2元的蓝牙接收模块Type-c供电3.5接口。这就是本次尝试起…

Kotlin Lambda

Kotlin Lambda 在探索Kotlin Lambda之前&#xff0c;我们先回顾下Java中的Lambda表达式&#xff0c;Java 的 Lambda 表达式是 Java 8 引入的一项强大的功能&#xff0c;它使得函数式编程风格的代码更加简洁和易于理解。Lambda 表达式允许你以一种更简洁的方式表示实现接口&…