关于C#的一些基础知识点汇总
1.C#结构体可以继承接口吗?会不会产生GC?
在 C# 中,结构体不能继承类,但可以实现接口。
代码:
interface IMyInterface
{void MyMethod();
}struct MyStruct : IMyInterface
{public void MyMethod(){Console.WriteLine("MyMethod implemented in struct");}
}
当结构体实现接口并被使用时,通常不会产生垃圾回收(GC)。
因为结构体是值类型,在栈上分配内存(当作为局部变量或方法参数时)或者作为引用类型中的字段时是内联存储的。然而,如果结构体包含引用类型的字段,并且对这些引用类型进行了一些复杂的操作,可能会间接导致垃圾回收。但这不是因为结构体实现接口本身导致的,而是由于对引用类型的操作。
2.什么是内联存储?
在 C# 中,“内联存储” 通常指值类型(如结构体)在被使用时,其内存是直接分配在当前的执行环境中,而不是在托管堆上。例如,当一个结构体作为方法的局部变量或参数时,它的内存会在栈上分配,这种分配和释放的效率通常比较高,因为不需要进行垃圾回收的操作。相比之下,引用类型(如类)的实例通常是在托管堆上分配内存的。
代码:
void SomeMethod()
{MyStruct myStruct = new MyStruct(); // 结构体在栈上内联存储// 对 myStruct 进行操作
}
3.结构体实现接口和类继承接口有什么区别?
结构体实现接口和类继承接口有以下一些区别:
内存分配:
结构体是值类型,通常在栈上分配内存(当作为局部变量或方法参数时)或者作为引用类型中的字段时内联存储。
类是引用类型,在托管堆上分配内存。
继承行为:
结构体不能从类或其他结构体继承,但可以实现接口。
类可以从其他类继承,也可以实现接口。
装箱与拆箱:
当结构体作为接口类型进行操作时,如果需要转换为引用类型(如 object 或接口类型),会发生装箱操作。将引用类型转换回结构体时会进行拆箱操作。装箱和拆箱会带来一定的性能开销。
类作为接口类型使用时不存在装箱和拆箱,因为类本身就是引用类型。
构造函数:
结构体不能有显式的无参数构造函数,会有一个默认的隐式无参构造函数,它将结构体的所有字段初始化为默认值。
类可以有多个自定义的构造函数。
传递方式:
结构体作为参数传递时,通常是按值传递,会复制整个结构体的值。
类作为参数传递时,默认是按引用传递。
可变性:
结构体通常被设计为不可变(immutable),以避免意外的修改导致难以追踪的错误。虽然可以修改结构体的成员,但这可能不符合最佳实践。
类可以更灵活地处理成员的修改。
4.结构体实现接口时,是否可以在接口中定义方法?
当结构体实现接口时,接口中是可以定义方法的。
代码:
interface IMyInterface
{void MyMethod();
}struct MyStruct : IMyInterface
{public void MyMethod(){Console.WriteLine("MyMethod implemented in struct");}
}
5.堆和栈的区别
在计算机内存管理中,堆(Heap)和栈(Stack)有以下一些主要区别:
内存分配方式:
栈:由编译器自动管理,分配和释放内存。当一个函数被调用时,为其分配栈空间,函数结束时自动释放。
堆:由程序员手动分配和释放(使用 new、malloc 等),若不手动释放,会在程序结束或垃圾回收时释放。
内存增长方向:
栈:向低地址方向增长。
堆:向高地址方向增长。
内存分配效率:
栈:分配效率高,因为其操作简单,空间连续。
堆:分配效率相对较低,因为涉及复杂的内存管理机制。
存储内容:
栈:主要存储局部变量、函数参数、返回值等。
堆:用于存储对象、动态分配的数组等较大且生存期不确定的数据。
内存大小:
栈:空间一般较小。
堆:空间较大,但不是无限的。
数据生存周期:
栈:变量的生存周期与所在的函数或代码块相关,函数结束时变量销毁。
堆:直到程序员手动释放或垃圾回收时才销毁。
碎片问题:
栈:不会产生碎片。
堆:频繁的分配和释放可能导致内存碎片。
6.C#直接执行IEnumerator方法,不使用StartCoroutine会执行吗?会产生GC吗?
在 C# 中,如果直接执行一个返回IEunmerator的方法,而不使用 StartCoroutine ,它会像普通方法一样执行,但不会按照协程的逻辑进行迭代和暂停。至于垃圾回收(GC),这取决于方法内部的具体实现。如果方法中没有创建大量的临时对象或者引用类型,并且没有导致内存泄漏,通常不会直接因为这种方法的执行而引发垃圾回收。然而,如果方法内部创建了很多对象并且没有被正确释放,或者存在一些长期持有的引用导致对象无法被回收,就可能会产生 GC 压力。
代码:
internal class Program{static IEnumerator MyEnumeratorMethod(){Console.WriteLine("开始生成数字");yield return 1;Console.WriteLine("继续生成数字");yield return 2;Console.WriteLine("结束生成数字");}static void Main(string[] args){MyEnumeratorMethod(); // 直接执行,不会按照协程逻辑暂停Console.WriteLine();}}
如果执行上述代码会发现,控制台什么都没有输出,如下图:

原因:
迭代器的延迟执行特性:
当一个函数返回 IEnumerator 时,它实际上是在定义一个迭代器。迭代器的设计理念是实现延迟执行,即函数体中的代码不会在函数被调用时立即执行,而是在需要逐个获取值时才执行。
以 C# 的迭代器实现为例,编译器会对包含 yield return 语句的函数进行特殊处理。当你调用返回 IEnumerator 的函数时,编译器会生成一个状态机类。这个状态机类实现了 IEnumerator 接口,并且保存了函数执行的状态信息。
状态机的工作原理:
当函数第一次被调用时,编译器生成的状态机对象被创建,但函数体中的代码并未执行。状态机的初始状态表示函数尚未开始执行。
当调用 IEnumerator 的 MoveNext 方法时,状态机开始执行函数体中的代码,直到遇到第一个 yield return 语句。此时,yield return 语句返回一个值(如果有),并且暂停函数的执行,将状态机的状态保存下来。下次调用 MoveNext 时,状态机从上次暂停的位置继续执行,直到遇到下一个 yield return 或函数结束。
internal class Program{static IEnumerator MyEnumeratorMethod(){Console.WriteLine("开始生成数字");yield return 1;Console.WriteLine("继续生成数字");yield return 2;Console.WriteLine("结束生成数字");}static void Main(string[] args){//FrameSyncServer server = new FrameSyncServer();//server.Start();IEnumerator enumerator = MyEnumeratorMethod(); // 直接执行,不会按照协程逻辑暂停// 第一次调用MoveNextbool hasNext = enumerator.MoveNext();if (hasNext){Console.WriteLine($"获取到的值: {enumerator.Current}");}// 第二次调用MoveNexthasNext = enumerator.MoveNext();if (hasNext){Console.WriteLine($"获取到的值: {enumerator.Current}");}// 第三次调用MoveNext,此时函数结束,MoveNext返回falsehasNext = enumerator.MoveNext();Console.WriteLine();}}
结果:

与普通函数执行的区别:
普通函数在调用时,其栈帧被创建,函数体内的代码按照顺序依次执行,直到遇到 return 语句返回结果,然后栈帧被销毁。而对于返回 IEnumerator 的函数,调用它只是创建了一个状态机对象,函数体的执行被延迟,并且可以通过 MoveNext 方法分阶段执行,这种机制使得代码可以更灵活地处理复杂的迭代或异步操作。
7.除了直接执行和使用 StartCoroutine,还有哪些方式可以在 C# 中使用协程?
在 C# 中,除了直接执行和使用 StartCoroutine 来处理协程外,还可以通过以下方式使用协程:
在异步方法中使用 yield return :可以在 async 异步方法中使用 yield return 与协程结合,例如在 async Task 方法中。
结合自定义的调度器(Scheduler):创建自定义的调度器来控制协程的执行逻辑和时机。
利用第三方库或框架提供的扩展和机制:有些第三方库可能提供了特定的方式来更灵活地处理协程。
代码:
class Program
{static async Task Main(){await MyAsyncMethod();}static async Task MyAsyncMethod(){Console.WriteLine("Method started");await Task.Delay(1000); Console.WriteLine("After delay");}
}
8.结合自定义调度器使用协程的优缺点是什么?会带来安全风险吗?
结合自定义调度器使用协程具有以下优点:
优点:
精细控制执行时机:可以根据具体的业务需求精确地控制协程的执行顺序、暂停和恢复时机,更好地适配特定的应用场景。
优化资源利用:能够根据系统资源的情况,如 CPU 负载、内存使用等,更有效地安排协程的执行,提高资源利用率。
处理复杂逻辑:适用于一些复杂的并发和同步逻辑,使代码更具可读性和可维护性。
跨平台适配:有助于在不同的平台或环境中实现一致的协程行为,增强代码的可移植性。
缺点:
增加开发复杂度:需要开发者自行实现调度器的逻辑,这增加了开发的难度和出错的可能性。
调试困难:自定义的调度器可能导致调试过程变得复杂,因为协程的执行不再遵循默认的规则。
潜在的性能开销:如果调度器的实现不够高效,可能会引入额外的性能开销。
维护成本高:自定义的调度器需要持续的维护和优化,以适应项目的变化和新的需求。
结合自定义调度器使用协程可能会带来一些潜在的安全风险,例如:
竞态条件:如果调度器的逻辑不正确,可能导致多个协程同时访问共享资源,从而引发竞态条件,导致数据不一致或错误的结果。
死锁:不当的调度顺序或资源管理可能导致死锁情况,即协程相互等待对方释放资源,造成程序停滞。
异常处理不当:在自定义调度器中,如果对协程执行过程中抛出的异常处理不当,可能导致整个系统的不稳定或错误传播。
内存泄漏:如果协程没有被正确地清理或释放,可能会导致内存泄漏,尤其是在复杂的调度逻辑中容易出现。
并发安全问题:自定义调度器可能打破了默认的并发安全机制,如果没有额外的防护措施,可能会引发并发访问的错误。
9.接口与抽象类的区别
成员定义:
接口只能包含方法、属性、事件和索引器的声明,不能包含字段和方法的实现。
抽象类可以包含抽象方法(只有声明,没有实现)和普通方法(有声明也有实现),还可以包含字段。
继承限制:
一个类可以实现多个接口。
一个类只能继承一个抽象类(但可以同时实现多个接口)。
继承实现:
实现接口的类必须实现接口中定义的所有成员。
继承抽象类的类可以选择实现抽象类中的抽象方法,也可以继承抽象方法但仍然保持为抽象类。
访问修饰符:
接口成员默认是公共的,不能使用访问修饰符。
抽象类中的成员可以使用各种访问修饰符。
实例化:
接口不能被实例化。
抽象类也不能被实例化,但可以有构造函数,用于在派生类的对象创建时被调用。
10.值类型与引用类型的区别
内存分配:
值类型的变量直接包含其数据,通常在栈上分配内存(如果是作为引用类型的成员,则在堆上内联存储)。
引用类型的变量存储的是对实际数据的引用(内存地址),数据存储在托管堆上。
传递方式:
值类型在作为参数传递给方法时,默认是按值传递,即创建一个副本。
引用类型在作为参数传递时,默认是按引用传递,对参数的修改会影响到原始对象。
比较方式:
值类型通过比较其值来确定相等性。
引用类型默认通过比较引用(即是否指向同一个对象)来确定相等性,除非重写 Equals 方法进行自定义的相等性比较。
垃圾回收:
值类型不受垃圾回收器直接管理,因为它们的内存分配和释放随着其作用域结束而自动处理。
引用类型由垃圾回收器管理其内存的回收。
继承:
值类型不能继承自其他类型(除了 System.ValueType)。
引用类型可以继承自其他类或接口。
11.什么是装箱和拆箱?
在 C# 中,装箱(Boxing)是将值类型转换为引用类型的过程,拆箱(Unboxing)则是将引用类型转换回值类型的过程。装箱和拆箱操作可能会带来一些性能开销,因为涉及到内存的重新分配和数据的复制。
代码:
int num = 10;
object obj = num; // 装箱
而当从一个已装箱的对象中把值类型提取出来时,就会发生拆箱操作:
代码:
int numAgain = (int)obj; // 拆箱
12.C#中的GC优化:
减少对象创建:
避免在频繁执行的代码段中创建不必要的对象。例如,使用对象池来重复利用对象,而不是频繁创建和销毁。
字符串操作优化:
对于大量的字符串连接操作,考虑使用 StringBuilder 类,因为频繁创建新的字符串对象可能导致内存压力和 GC 开销。
缓存常用对象:
对于经常使用但创建成本较高的对象,进行缓存以减少重复创建。
避免大对象的频繁创建和销毁:
大对象(通常大于 85000 字节)被分配在大对象堆(LOH)上,LOH 的垃圾回收相对不频繁且成本较高。
代码:
class ObjectCache
{private static Dictionary<int, MyObject> cache = new Dictionary<int, MyObject>();public static MyObject GetObject(int key){if (!cache.ContainsKey(key)){cache[key] = new MyObject(key);}return cache[key];}
}class MyObject
{public int Key { get; }public MyObject(int key){Key = key;}
}
非托管资源管理:
对于使用非托管资源(如文件句柄、数据库连接等)的对象,及时释放非托管资源。实现 IDisposable 接口,并在使用完后正确调用 Dispose 方法或使用 using 语句。
控制对象的生命周期:
尽量使对象的生命周期与使用它们的逻辑范围相匹配,避免对象长时间存活但不再被使用。
选择合适的集合类型:
根据实际需求选择合适的集合类型。例如,如果需要频繁添加和删除元素,LinkedList 可能比 ArrayList 更合适,因为 ArrayList 的扩容可能导致大量的内存重新分配和对象移动。
避免短时间内大量临时对象的创建:
例如,在循环中,如果可能,尽量复用对象而不是每次循环都创建新对象。
优化数据结构:
对于大型的数据结构,如数组,如果可能的话,预先分配足够的空间,避免频繁的扩容操作。
减少引用类型字段的使用:
特别是在值类型(如结构体)中,过多的引用类型字段可能会增加内存管理的复杂性。
考虑使用弱引用(WeakReference):
在某些情况下,使用弱引用可以在内存紧张时允许垃圾回收器回收被弱引用的对象,同时提供一种在对象未被回收时访问的方式。
分代优化:
了解垃圾回收的分代机制,对于生命周期较短的对象尽量放在新生代,以便更快速地回收。
避免不必要的装箱和拆箱操作:
除了前面提到的基本注意事项外,在一些复杂的数据转换和接口调用中要特别小心。
异步和并发中的内存管理:
在多线程或异步操作中,确保正确处理共享对象的内存访问,避免竞态条件和内存泄漏。
13.C#中大对象堆是什么?
在 C# 中,大对象堆(Large Object Heap,简称 LOH)是托管堆的一部分,用于存储大于 85000 字节的对象。
与普通的托管堆(用于存储较小的对象)相比,大对象堆具有以下特点:
较少的垃圾回收:大对象堆上的垃圾回收不像普通堆那么频繁,通常在内存压力较大时才会触发。
不进行内存压缩:由于大对象的移动成本较高,在垃圾回收时一般不会对大对象堆进行内存压缩操作。
内存分配策略:大对象的分配不是连续的,可能会有一些碎片。
如果应用程序频繁地创建和销毁大对象,可能会导致性能问题和内存碎片。
代码:
class Program
{static void Main(){byte[] largeArray = new byte[100000]; // 可能分配在大对象堆上}
}
14.string 是引用类型吗?可以继承吗?
在 C# 中,string 是引用类型。但是 string 是密封类(sealed),不能被继承。
15.为什么string被设计为密封类?
字符串的不变性:string 对象在创建后其值是不可变的。将其密封可以确保这种不变性不被意外打破,有助于提高程序的稳定性和可预测性。
性能和优化:.NET 框架对字符串的操作和存储进行了大量的优化。密封类可以让这些优化得以保证,避免因继承导致的意外行为影响性能。
安全性:防止恶意或错误的继承导致字符串的行为不符合预期,从而提高了整个系统的安全性。
一致性和通用性:字符串在各种编程语言中通常都被视为基本且不可变的数据类型。将 string 密封有助于保持 C# 中字符串处理的一致性和与其他语言的兼容性。
避免错误和混淆:防止开发者在继承 string 时可能引入的错误,因为对字符串的自定义继承往往不是常见和必要的需求,反而容易导致复杂和难以理解的代码。
16.List 和 Dictionary有什么区别?
数据存储方式:
List 按照元素添加的顺序存储元素。
Dictionary 以键值对的形式存储数据,通过键来快速查找对应的值。
访问方式:
访问 List 中的元素通常通过索引。
访问 Dictionary 中的元素通过指定的键。
元素唯一性:
List 可以包含重复的元素。
Dictionary 的键必须是唯一的,但值可以重复。
查找效率:
查找特定元素时,如果知道索引,List 的查找速度较快。但如果要查找某个特定值,需要遍历整个列表,效率较低。
Dictionary 通过键进行查找,通常具有接近 O (1) 的平均查找时间,效率很高。
存储结构:
List 内部通常基于数组实现。
Dictionary 常见的实现方式如哈希表。
17.List和ArrayList有什么区别?
类型安全:
List 是强类型的,在创建时需要指定元素的类型。
ArrayList 可以存储不同类型的对象,不是类型安全的。
性能:
由于 List 的强类型特性,在操作时通常比 ArrayList 具有更好的性能,特别是在涉及类型转换时。
泛型支持:
List 是泛型集合,充分利用了泛型的优势,减少了类型转换的开销和潜在的运行时错误。
ArrayList 不是泛型的。
18.List、ArrayList、Dictionary的使用场景是什么?
List 的使用场景:
当需要存储同一种类型的元素,并且对类型安全性有要求时,比如存储一组整数、字符串等。
当性能和效率较为重要,需要避免不必要的类型转换和运行时错误检查时。
ArrayList 的使用场景:
在一些旧的代码中,如果需要存储不同类型的对象,且类型在运行时才能确定。
当需要与不支持泛型的旧代码进行交互时。
不过,在现代 C# 编程中,一般更推荐使用 List ,因为它提供了更好的类型安全性和性能。
19.List和Dictionary的使用场景是什么?
List 的使用场景:
存储有序的同类型元素集合,例如存储学生的成绩列表、商品列表等。
当需要按照元素的添加顺序进行遍历和处理时。
当需要频繁地在末尾添加或删除元素时。
Dictionary 的使用场景:
快速根据键来查找对应的值,例如根据学生的学号查找学生信息。
存储键值对形式的数据,其中键是唯一的,例如存储单词及其释义。
构建映射关系,例如将城市名称映射到对应的人口数量。
20.Dictionary的实现原理是什么?
Dictionary 在 C# 中的常见实现原理通常基于哈希表(Hash Table)。
当向 Dictionary 中添加键值对时,会计算键的哈希值。哈希值用于确定键值对在内部存储结构中的位置。
如果多个键计算出的哈希值相同(这被称为哈希冲突),Dictionary 通常会通过某种处理冲突的方法(如链表法或开放地址法)来存储这些键值对。
在查找元素时,同样先计算键的哈希值,然后快速定位到可能存储该键值对的位置,再进行比较确认是否为要查找的键。
21.在使用Dictionary时,如何处理可能出现的哈希冲突?
在 C# 的 Dictionary 中,处理哈希冲突通常采用以下几种方式:
链表法:当发生哈希冲突时,将具有相同哈希值的元素存储在一个链表中。在查找时,通过哈希值定位到链表位置,然后遍历链表来查找具体的键。
开放地址法:如果发生冲突,通过一定的探测策略(如线性探测、二次探测等)在哈希表中寻找其他空闲的位置来存储冲突的元素。
在使用 Dictionary 时,一般不需要手动处理哈希冲突,.NET 框架已经在内部实现了高效的处理机制。
但要注意以下几点来减少哈希冲突的影响:
选择合适的键类型:尽量选择具有良好哈希分布特性的键类型,以减少冲突的可能性。
控制字典的大小:避免过度填充字典,根据预期的元素数量合理调整初始容量
22.哈希冲突的解决方式有哪些?
开放定址法:
线性探测:发生冲突时,依次检查下一个位置,直到找到空闲位置。
二次探测:冲突时,按照一定的步长(如 1²、2²、3² 等)进行探测。
链地址法:将哈希值相同的元素存储在一个链表中。
再哈希法:当发生冲突时,使用另一个哈希函数再次计算哈希值,直到找到空闲位置。
建立公共溢出区:将发生冲突的元素存储在另外一个区域。
23.Redis中是字典是如何实现的?如何解决哈希冲突的?
在 Redis 中,字典(Dictionary)通常是通过哈希表实现的。
Redis 解决哈希冲突主要使用的是链地址法。当出现哈希冲突时,具有相同哈希值的键值对会被组织成一个链表。
在 Redis 中,为了优化性能,还会采取一些措施,比如当链表过长时,会将哈希表进行扩容,重新计算哈希值并重新分布元素,以减少冲突和提高查找效率。
24.Redis中字典的扩容机制是怎样的?
在 Redis 中,字典的扩容机制主要遵循以下规则:
当负载因子(已使用的节点数 / 哈希表大小)超过一定阈值时,就会触发扩容操作。
具体来说:
负载因子的计算:负载因子 = 哈希表已保存节点数量 / 哈希表大小。
触发扩容的条件:默认情况下,当负载因子大于 1 时,会进行扩容。
扩容策略:
新的哈希表大小通常是原哈希表大小的两倍。
然后逐步将原哈希表中的键值对重新计算哈希值,并迁移到新的哈希表中。
模拟扩容代码:
class RedisDictionary
{private Dictionary<int, string> dict = new Dictionary<int, string>();private int capacity = 10; // 初始容量private int size = 0; // 已存储的元素数量public void Add(int key, string value){if ((float)size / capacity > 1) // 检查负载因子{Resize(); // 进行扩容}dict.Add(key, value);size++;}private void Resize(){capacity *= 2; // 新容量为原来的两倍Dictionary<int, string> newDict = new Dictionary<int, string>(capacity);foreach (var pair in dict){newDict.Add(pair.Key, pair.Value); // 重新计算哈希并迁移元素}dict = newDict;}
}
25.一些名词解释
内存泄露(Memory Leak):指程序中动态分配的内存,在使用完毕后没有被正确释放,导致这些内存无法被再次使用,随着时间的推移,积累的不可用内存越来越多,最终可能导致系统性能下降甚至崩溃。
内存溢出(Memory Overflow):当程序向内存申请的空间超过了系统所能提供的最大内存时,就会发生内存溢出。例如,一个数组申请的空间超过了可用内存的大小。
内存雪崩:一般是指由于某些原因(比如大量缓存同时过期、缓存服务器宕机等)导致大量请求无法从缓存中获取数据,从而直接访问数据库,造成数据库压力瞬间增大,甚至可能导致数据库崩溃。
缓存命中(Cache Hit):当从缓存中成功获取到所需的数据,就称为缓存命中。缓存命中意味着可以快速获取数据,提高系统的性能。
缓存穿透:指查询一个根本不存在的数据,缓存中没有,数据库中也没有。这样每次请求都会直接打到数据库,给数据库带来压力。
缓存击穿:一个非常热点的数据,在缓存失效的瞬间,大量的并发请求直接访问数据库来获取数据。
内存抖动:频繁地进行内存的分配和回收,导致系统性能下降。
堆内存和栈内存:堆内存用于动态分配较大的对象和数据结构,由垃圾回收器管理;栈内存用于存储局部变量和方法调用信息,自动分配和释放。
直接内存:不是由 Java 虚拟机管理的内存,通过 ByteBuffer 类的 allocateDirect 方法分配,使用不当可能导致内存泄漏。
内存碎片:内存被分配和释放后,可能会产生一些不连续的、无法被有效利用的小内存区域。
弱引用和软引用:弱引用对象在垃圾回收时,只要发现就会被回收;软引用对象只有在内存不足时才会被回收。
分页内存:将内存划分为固定大小的页,便于内存管理和交换。
虚拟内存:通过将部分内存数据存储在磁盘上,扩展了程序可用的内存空间。
内存对齐:为了提高内存访问效率,数据在内存中的存储位置通常需要按照一定的字节边界对齐
26.内存碎片如何产生的?
动态内存分配和释放:当程序频繁地申请和释放不同大小的内存块时,可能会导致内存空间出现不连续的空闲区域。例如,先分配一块较大的内存,然后释放中间的一部分,就会在两端形成较小的空闲块。
内存分配策略:某些内存分配算法可能导致碎片的产生。比如首次适应算法,总是从内存的起始位置开始查找适合的空闲块,可能会使后面的较大空闲块被分割成小的、不连续的部分。
不同大小的内存请求:如果程序中既有对小内存块的请求,又有对大内存块的请求,并且它们的分配和释放顺序不规则,容易产生碎片。
27.如何解决内存碎片问题?
内存池技术:
预先分配一块较大的连续内存作为内存池。当需要内存时,从内存池中分配固定大小的块,而不是每次动态分配。减少了频繁的小内存分配和释放导致的碎片。
压缩和整理内存:
定期对内存进行扫描,将已使用的内存块移动到一起,合并空闲的碎片。
采用合适的内存分配算法:
例如,伙伴系统算法、最佳适应算法等,在一定程度上减少碎片的产生。
限制内存分配和释放的频率:
尽量复用已分配的内存,避免频繁的申请和释放。
对象复用:
对于一些经常创建和销毁的对象,使用对象池进行复用。
28.内存分配算法有哪些?怎么实现?
首次适应算法(First Fit):从内存的起始位置开始,顺序查找第一个能满足需求的空闲分区进行分配。
简单实现代码:
class FirstFitAllocator
{private int[] memoryBlocks; // 表示内存块的大小private bool[] isAllocated; // 标记是否已分配public FirstFitAllocator(int[] blockSizes){memoryBlocks = blockSizes;isAllocated = new bool[blockSizes.Length];}public int Allocate(int size){for (int i = 0; i < memoryBlocks.Length; i++){if (!isAllocated[i] && memoryBlocks[i] >= size){isAllocated[i] = true;return i;}}return -1; // 表示分配失败}
}
最佳适应算法(Best Fit):扫描整个空闲分区表,选择能满足需求且大小最小的空闲分区进行分配。
简单实现代码:
class BestFitAllocator
{private int[] memoryBlocks;private bool[] isAllocated;public BestFitAllocator(int[] blockSizes){memoryBlocks = blockSizes;isAllocated = new bool[blockSizes.Length];}public int Allocate(int size){int bestFitIndex = -1;int minDifference = int.MaxValue;for (int i = 0; i < memoryBlocks.Length; i++){if (!isAllocated[i] && memoryBlocks[i] >= size && (memoryBlocks[i] - size) < minDifference){bestFitIndex = i;minDifference = memoryBlocks[i] - size;}}if (bestFitIndex!= -1){isAllocated[bestFitIndex] = true;}return bestFitIndex;}
}
最坏适应算法(Worst Fit):选择最大的空闲分区进行分配。
简单实现代码:
class WorstFitAllocator
{private int[] memoryBlocks;private bool[] isAllocated;public WorstFitAllocator(int[] blockSizes){memoryBlocks = blockSizes;isAllocated = new bool[blockSizes.Length];}public int Allocate(int size){int worstFitIndex = -1;int maxSize = 0;for (int i = 0; i < memoryBlocks.Length; i++){if (!isAllocated[i] && memoryBlocks[i] >= size && memoryBlocks[i] > maxSize){worstFitIndex = i;maxSize = memoryBlocks[i];}}if (worstFitIndex!= -1){isAllocated[worstFitIndex] = true;}return worstFitIndex;}
}
伙伴系统算法:将内存按 2 的幂次大小进行划分和合并。
29.值类型的内存分配位置
在 C# 中,值类型(如 int、float、struct 等)的分配取决于它们的使用上下文。
如果值类型是在方法内部声明的局部变量,那么它们通常被分配在栈上。栈的分配和释放速度非常快,因为它的管理方式相对简单。
如果值类型是作为类或结构体的成员,并且该类或结构体被实例化为对象,那么值类型成员会随着对象一起分配在堆上或者作为引用类型对象的一部分。
示例代码:
class ValueTypeAllocationExample
{struct Point{public int X;public int Y;}void Method(){// 这里的 num 分配在栈上int num = 10;// 这里的 p 分配在堆上,因为包含它的对象分配在堆上Point p = new Point { X = 5, Y = 5 };}
}
30.内存,外存,栈、堆
内存(也称为主存或随机存取存储器 - RAM):
速度快,能够快速读写数据。
是计算机在运行程序时用于临时存储数据和程序指令的地方。
容量相对较小,且断电后数据丢失。
外存(如硬盘、SSD、U 盘等):
速度相对较慢,但容量通常很大。
用于长期存储数据,即使断电数据也不会丢失。
栈(Stack):
位于内存中。
由编译器自动管理,存储局部变量、函数参数和返回地址等。
遵循 “后进先出”(Last In First Out,LIFO)的原则。
分配和释放内存的操作速度很快。
代码:
void Method()
{int num = 10; // num 存储在栈上
}
堆(Heap):
也在内存中。
由程序员手动管理(通过 new 等操作分配,delete 或 Dispose 等释放),或者由垃圾回收器自动管理(如在 C# 等语言中)。
用于存储对象和较大的数据结构。
代码:
class MyClass
{// 类的实例存储在堆上MyClass obj = new MyClass();
}
内存用于快速的临时数据存储,外存用于长期的数据保存,栈适合存储短期的、自动管理的小数据,堆适合存储由程序员或语言的垃圾回收机制管理的较大、更复杂的数据结构和对象。
31.CPU Cache
CPU Cache(CPU 高速缓存)是位于 CPU 与主内存之间的一种小而快速的存储器。
它的主要作用是减少 CPU 访问主内存的时间延迟,从而提高计算机系统的性能。
CPU Cache 通常分为多个级别,如 L1 Cache、L2 Cache 和 L3 Cache。L1 Cache 距离 CPU 核心最近,速度最快,但容量较小;L2 Cache 速度稍慢,容量较大;L3 Cache 则更大但速度相对较慢。
当 CPU 需要读取数据时,首先会在 Cache 中查找,如果找到(称为 “命中”),则直接从 Cache 中获取数据,速度很快;如果未找到(称为 “未命中”),则需要从主内存中读取数据,并将其存储到 Cache 中以便后续使用。
代码示例:
int[] data = new int[10000]; // 假设存储在主内存中void ProcessData()
{// 第一次访问可能未命中 Cache,从主内存读取int value = data[0]; // 后续对附近数据的访问可能命中 Cache,速度更快int nextValue = data[1];
}
32.外存通常是什么作用,存储什么的?
外存的作用主要是用于长期、大量地存储数据和程序,即使在计算机关机或断电的情况下,数据也不会丢失。
外存通常存储以下几类信息:
操作系统和系统文件:包括启动计算机所需的核心系统文件、驱动程序等。
应用程序和软件:如办公软件、游戏、图形设计工具等。
用户数据:
文档:如文字处理文档、电子表格、演示文稿等。
图片、音频和视频文件:照片、音乐、电影等多媒体内容。
数据库文件:用于存储大量结构化数据。
备份数据:为了防止数据丢失,用户和系统的重要数据的备份通常也存储在外存中。
常见的外存设备包括硬盘驱动器(HDD)、固态硬盘(SSD)、光盘、U 盘等。
未完待续。。。
相关文章:
关于C#的一些基础知识点汇总
1.C#结构体可以继承接口吗?会不会产生GC? 在 C# 中,结构体不能继承类,但可以实现接口。 代码: interface IMyInterface {void MyMethod(); }struct MyStruct : IMyInterface {public void MyMethod(){Console.Write…...
一文讲解Redis为什么读写性能高以及I/O复用相关知识点
Redis为什么读写性能高呢? Redis 的速度⾮常快,单机的 Redis 就可以⽀撑每秒十几万的并发,性能是 MySQL 的⼏⼗倍。原因主要有⼏点: ①、基于内存的数据存储,Redis 将数据存储在内存当中,使得数据的读写操…...
Hadoop-HA(高可用)机制
首先:在每个NAMENODE上都会有一个zkfc(zookeeper failover colltroller) ,负责这两个的状态管理。哪个是(active和standby)然后写入zk集群里面。同时监控自己所在的机器是否正常。一旦active上zkfc的发现异…...
51单片机-按键
1、独立按键 1.1、按键介绍 轻触开关是一种电子开关,使用时,轻轻按开关按钮就可使开关接通,当松开手时,开关断开。 1.2、独立按键原理 按键在闭合和断开时,触点会存在抖动现象。P2\P3\P1都是准双向IO口,…...
深度学习的力量:精准肿瘤检测从此不再遥远
目录 引言 一、医学图像分析的挑战与深度学习的优势 1.1 医学图像分析的挑战 1.2 深度学习的优势 二、肿瘤检测的深度学习模型设计 2.1 卷积神经网络(CNN)的基本原理 2.2 网络架构设计 2.3 模型训练 三、肿瘤检测中的挑战与解决方案 3.1 数据不…...
初尝git自结命令大全与需要理解的地方记录
常用命令 git init–初始化工作区touch 文件全称–在工作区创建文档rm 文件全称 --删除文档notepad 文件全称–在工作区打开文档cat 文件全称–在显示框显示文档的东西git status --显示工作区的文件冲突的文件 (git add 文件全称或者.) —将工作区文件…...
LangChain 技术入门指南:探索语言模型的无限可能
在当今的技术领域,LangChain 正逐渐崭露头角,成为开发语言模型应用的强大工具。如果你渴望深入了解并掌握这一技术,那么就跟随本文一起开启 LangChain 的入门之旅吧! (后续将持续输出关于LangChain的技术文章,有兴趣的同学可以关注…...
Nginx WebSocket 长连接及数据容量配置
WebSocket 协议是实现实时通信的关键技术。相比于传统的 HTTP 请求-响应模式,WebSocket 提供了双向、持久化的通信方式。Nginx 作为一个高性能的反向代理服务器,可以非常有效地处理 WebSocket 连接,但要正确处理 WebSocket 长连接和传输大数据…...
Pycharm+CodeGPT+Ollama+Deepseek
首先,体验截图: 接着: 1、下载Ollama: Download Ollama on macOS 2、下载模型 以1.5b为例,打开命令行,输入: ollama run deepseek-r1:1.5b 3、Pycharm安装Code GPT插件 打开PyCharm,找到文…...
k8s Container runtime network not ready
问题 k8s 3 控制节点,docker 运行时,后期踢掉其中一个节点,使用了 containerd 运行时,但是在加入集群的时候,node 状态 notready。查看 kubelet 的日志发现如下报错 Feb 20 11:28:14 bjm3 kubelet[144781]: E0220 11:28:14.506374 144781 kubelet.go:2475] "Conta…...
阿里云k8s服务部署操作一指禅
文章目录 DockerFile镜像操作阿里云k8s服务部署 DockerFile # 使用 JDK 17 官方镜像 # linux架构:FROM --platformlinux/amd64 openjdk:17-jdk-slim # arm架构:openjdk:17-jdk-slim FROM --platformlinux/amd64 openjdk:17-jdk-slim# 设置工作目录 WORK…...
pdf-extract-kit paddle paddleocr pdf2markdown.py(效果不佳)
GitHub - opendatalab/PDF-Extract-Kit: A Comprehensive Toolkit for High-Quality PDF Content Extraction https://github.com/opendatalab/PDF-Extract-Kit pdf2markdown.py 运行遇到的问题: 错误: -------------------------------------- C Tra…...
.NET + Vue3 的前后端项目在IIS的发布
目录 一、发布准备 1、安装 IIS 2、安装 Windows Hosting Bundle(.NET Core 托管捆绑包) 3、安装 IIS URL Rewrite 二、项目发布 1、后端项目发布 2、前端项目发布 3、将项目部署到 IIS中 三、网站配置 1、IP配置 2、防火墙配置 3、跨域配置…...
交互编程工具之——Jupyter
Jupyter 是什么? Jupyter 是一个开源的交互式编程和数据分析工具,广泛应用于数据科学、机器学习、教育和研究领域。其核心是 Jupyter Notebook(现升级为 JupyterLab),允许用户在一个基于浏览器的界面中编写代码、运行…...
微信小程序客服消息接收不到微信的回调
微信小程序客服消息,可以接收到用户进入会话事件的回调,但是接收不到用户发送消息的回调接口。需要在微信公众平台,把转发消息给客服的开关关闭。需要把这个开关关闭,否则消息会直接发送给设置的客服,并不会走设置的回…...
easyexcel 2.2.6版本导出excel模板时,标题带下拉框及其下拉值过多不显示问题
需求背景:有一个需求要做下拉框的值有100多条,同时这个excel是一个多sheet的导入模板 直接用easyexcel 导出,会出现下拉框的值过多,导致生成出来的excel模板无法正常展示下拉功能 使用的easyexcel版本:<depende…...
影视大数据分析新范式:亮数据动态代理驱动的实时数据采集方案
一、项目背景与挑战 在数据驱动决策的时代,影视数据分析对内容平台至关重要。但豆瓣等平台设有: 高频请求IP封禁机制User-Agent指纹检测请求频率阈值控制验证码验证系统 传统爬虫方案面临: 单一IP存活时间<5分钟采集成功率<30%数据更新…...
免费体验,在阿里云平台零门槛调用满血版DeepSeek-R1模型
一、引言 随着人工智能技术的飞速发展,各类AI模型层出不穷。其中,DeepSeek作为一款新兴的推理模型,凭借其强大的技术实力和广泛的应用场景,逐渐在市场中崭露头角。本文将基于阿里云提供的零门槛解决方案,对DeepSeek模…...
ok113i平台——多媒体播放器适配
1. 视频播放支持 1.1 在Linux平台交叉编译ffmpeg动态库,详情查看《ok113i平台——交叉编译音视频动态库》 提取如下动态库: libavcodec.so.58.134.100 libavdevice.so.58.13.100 libavfilter.so.7.110.100 libavformat.so.58.76.100 libavutil.so.56.…...
使用Python中的`gensim`库构建LDA(Latent Dirichlet Allocation)模型来分析收集到的评论
下面为你详细介绍如何使用Python中的gensim库构建LDA(Latent Dirichlet Allocation)模型来分析收集到的评论。LDA是一种主题模型,它可以将文档集合中的文本按照主题进行分类。 步骤概述 数据预处理:对收集到的评论进行清洗、分词…...
23种设计模式 - 策略模式
模式定义 策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列可互换的算法,并将每个算法封装成独立类,使得算法可以独立于客户端变化。该模式的核心思想是解耦算法的定义与使用,适用于需要动…...
Cursor 与团队协作:提升团队开发效率
引言 在团队开发中,代码质量参差不齐、重复错误频发、代码审查耗时过长是制约效率的三大痛点。据 GitHub 调查,开发者平均每周花费 4.3 小时修复他人代码问题,而 60% 的合并请求(PR)因风格或低级错误被驳回。Cursor 作…...
QT qbytearray转qString
qbytearray转qString 在Qt框架中,QByteArray和QString是常用的数据类型,它们用于处理不同类型的字符串数据。QByteArray用于存储原始字节数据,而QString用于存储Unicode字符串。在某些情况下,你可能需要将QByteArray转换为QStrin…...
激光工控机在自动化生产线中有什么关键作用?
激光工控机作为自动化生产线的核心设备,通过高精度控制、快速响应和智能化集成,在提升效率、保障质量、实现柔性制造等方面发挥着不可替代的作用。以下是其关键作用的具体分析: 一、实现高效连续生产: 1.高速加工能力࿱…...
深度解析应用层协议-----HTTP与MQTT(涵盖Paho库)
HTTP协议概述 1.1 HTTP的基本概念 HTTP是一种应用层协议,使用TCP作为传输层协议,默认端口是80,基于请求和响应的方式,即客户端发起请求,服务器响应请求并返回数据(HTML,JSON)。在H…...
Kubernetes的Ingress和Service有什么区别?
在Kubernetes中,Ingress和Service是两个不同的概念,它们在功能、作用范围、应用场景等方面存在明显区别,具体如下: 功能 Ingress:主要用于管理集群外部到内部服务的HTTP和HTTPS流量路由。它可以根据域名、路径等规则…...
WordPress“更新失败,响应不是有效的JSON响应”问题的修复
在使用WordPress搭建网站时,许多人在编辑或更新文章时,可能会遇到一个提示框,显示“更新失败,响应不是有效的JSON响应”。这个提示信息对于不了解技术细节的用户来说,太难懂。其实,这个问题并不复杂&#x…...
【回溯算法2】
力扣17.电话号码的字母组合 链接: link 思路 这道题容易想到用嵌套的for循环实现,但是如果输入的数字变多,嵌套的for循环也会变长,所以暴力破解的方法不合适。 可以定义一个map将数字和字母对应,这样就可以获得数字字母的映射了…...
【RabbitMQ业务幂等设计】RabbitMQ消息是幂等的吗?
在分布式系统中,RabbitMQ 自身不直接提供消息幂等性保障机制,但可通过业务逻辑设计和技术组合实现消息处理的幂等性。以下是 8 种核心实现方案及最佳实践: 一、消息唯一标识符 (Message Deduplication) 原理 每条消息携带全局唯一IDÿ…...
layui 远程搜索下拉选择组件(多选)
模板使用(lay-module/searchSelect),依赖于 jquery、layui.dist 中的 dropdown 模块实现(所以data 格式请参照 layui文档) <link rel"stylesheet" href"layui-v2.5.6/dist/css/layui.css" /&g…...
