C# 面试手册

内存管理

部份内容参考自:https://www.cnblogs.com/anding/p/5260319.html

C# 中堆和栈的区别?

对比维度栈(Stack)堆(Heap)
存储内容值类型(intstruct)、方法调用的局部变量、参数引用类型(classstring数组)的对象实例
分配与释放自动分配/释放(方法结束时立即回收) 速度快(移动栈指针)手动分配/GC自动回收 速度较慢(需内存管理)
生命周期与方法调用绑定,作用域结束立即失效由GC管理,可能存活多轮垃圾回收(分代机制)
访问速度极快(直接操作内存,CPU缓存友好)较慢(需通过引用间接访问)
大小限制固定大小(默认1MB/线程) 栈溢出引发 StackOverflowException仅受虚拟内存限制(GB级) 溢出引发 OutOfMemoryException
典型场景临时变量、方法参数、线程独占数据对象实例、集合、共享数据

using() 语法有用吗?什么是IDisposable?

有用,实现了IDisposiable的类在using中创建,using结束后会自定调用该对象的Dispose方法,释放资源。

using 语句会被编译成什么代码?

// 编译前
using (var resource = new FileStream(...)) { ... }
 
// 编译后(等价于)
FileStream resource = null;
try {
    resource = new FileStream(...);
    ...
} finally {
    resource?.Dispose();
}

创建下面对象实例,需要申请多少内存空间?

public class User
{
    public int Age { get; set; }
    public string Name { get; set; }
    public string _Name = "123" + "abc";
    public List<string> _Names;
}
  • 对象大小估算,共计40个字节
    • 属性Age值类型Int,4字节;
    • 属性Name,引用类型,初始为NULL,4个字节,指向空地址;
    • 字段_Name初始赋值了,由前面的文章(.NET面试题解析(03)-string与字符串操作)可知,代码会被编译器优化为_Name=”123abc”。一个字符两个字节,字符串占用2×6+8(附加成员:4字节TypeHandle地址,4字节同步索引块)=20字节,总共内存大小=字符串对象20字节+_Name指向字符串的内存地址4字节=24字节;
    • 引用类型字段List<string> _Names初始默认为NULL,4个字节;
    • User对象的初始附加成员(4字节TypeHandle地址,4字节同步索引块)8个字节;
  • 内存申请: 申请44个字节的内存块,从指针NextObjPtr开始验证,空间是否足够,若不够则触发垃圾回收。
  • 内存分配: 从指针NextObjPtr处开始划分44个字节内存块。
  • 对象初始化: 首先初始化对象附加成员,再调用User对象的构造函数,对成员初始化,值类型默认初始为0,引用类型默认初始化为NULL;
  • 托管堆指针后移: 指针NextObjPtr后移44个字节。
  • 返回内存地址: 返回对象的内存地址给引用变量。

CTS、CLS、CLR分别作何解释?

CTS:通用语言系统。CLS:通用语言规范。CLR:公共语言运行库。

什么是受管制的代码?

unsafe:非托管代码。不经过CLR运行。

什么是托管代码、非托管代码托管代码 (managed code)

托管代码

由公共语言运行库环境(而不是直接由操作系统)执行的代码。托管代码应用程序可以获得公共语言运行库服务,例如自动垃圾回收、运行库类型检查和安全支持等。这些服务帮助提供独立于平台和语言的、统一的托管代码应用程序行为。有关内存管理(内存申请,内存释放,垃圾回收之类的)全部都是.net 的CLR来管理。

非托管代码 (unmanaged code)

在公共语言运行库环境的外部,由操作系统直接执行的代码。非托管代码必须提供自己的垃圾回收、类型检查、安全支持等服务;它与托管代码不同,后者从公共语言运行库中获得这些服务。内存回收要继承IDisposable 接口手动回收。

GC垃圾回收?

GC是垃圾回收(Garbage Collect)的缩写,是.NET核心机制的重要部分。它的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(那些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。

首先,需要再次强调一下托管堆内存的结构,如下图,很明确的表明了,只有GC堆才是GC的管辖区域,关于加载堆在前面文中有提到过(.NET面试题解析(04)-类型、方法与继承)。GC堆里面为了提高内存管理效率等因素,有分成多个部分,其中 两个主要部分:

  • 0/1/2代:代龄(Generation)在后面有专门说到;
  • 大对象堆(Large Object Heap),大于85000字节的大对象会分配到这个区域,这个区域的主要特点就是:不会轻易被回收;就是回收了也不会被压缩(因为对象太大,移动复制的成本太高了);

解释一下一个引用对象在GC的生命周期?

  • new创建对象并分配内存
  • 对象初始化
  • 对象操作、使用
  • 资源清理(非托管资源)
  • GC垃圾回收

GC是什么? 为什么要有GC?

  1. GC是垃圾收集器。程序员不用担心内存管理,因为垃圾收集器会自动进行管理.
  2. .NET的GC机制有这样两个问题:首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。

GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。

GC就是对“不可达“的对象进行回收,释放内存。

谈谈.net中的GC,垃圾回收策略,如何回收非托管资源

手动强制回收:GC.Collect() GC.Collect(0/1/2)0:新生代,1:旧生代,2:持久代

托管:可借助GC从内存中释放的数据对象(以下要描述的内容点)

非托管:必须手工借助Dispose释放资源(实现自IDisposable)的对象

根据实例的生命周期和对象的调用频率来判断堆中的数据是否为新生代、旧生代、持久代(静态)来选择回收内存。

如何解决.net中的内存泄漏问题?用到过哪些检测工具?

.NET内存泄漏,更准确的说应该是对象超过生命周期而不能被GC回收。

常见的内存泄露有:

  • a、静态引用;
  • b、控件不使用后未销毁;
  • c、调用非托管资源而未释放;
  • d、事件注册后未解除注册,等。

解决方案:

  • (1) Dispose()的使用 如果使用的对象提供Dispose()方法,那么当你使用完毕或在必要的地方(比如Exception)调用该方法,特别是对非托管对象,一定要加以调 用,以达到防止泄露的目的。
  • (2) using的使用 using除了引用Dll的功用外,还可以限制对象的适用范围,当超出这个界限后对象自动释放,比如using语句的用途定义一个范围,将在此范围之外释放一个或多个对象。
  • (3) 事件的卸载 这个不是必须的,推荐这样做。之前注册了的事件,关闭画面时应该手动注销,有利于GC回收资源。
  • (4) API的调用 一般的使用API了就意味着使用了非托管资源,需要根据情况手动释放所占资源,特别是在处理大对象时。4.5继承 IDisposable实现自己内存释放接口 Net 如何继承IDisposable接口,实现自己的Dispose()函数
  • (5)弱引用(WeakReference ) 通常情况下,一个实例如果被其他实例引用了,那么他就不会被GC回收,而弱引用的意思是,如果一个实例没有被其他实例引用(真实引用),而仅仅是被弱引用,那么他就会被GC回收。

诊断工具:

  • a、大多使用windows自带的perfmon.msc,
  • b、用过的工具里面CLRProfiler 和dotTrace还行,windbg也还行。不过坦白的说,准确定位比较费劲,最好还是按常规的该Dispose的加Dispose,也可以加 GC.Collect()

内存泄漏和内存溢出的区别是什么?

简单来说,操作系统就像资源分配人员,你要使用内存的时候分给你,你用完了还给它。如果你使用了没有分配给你的内存就是内存溢出,如果你用完了没有了就是内存泄漏。

会引起的问题:

内存溢出存在的问题是你用了没有分配给你的内存,系统是不知道的,他又把内存分配给了其他程序,结果就是别人也写了或者读了这个内存。程序可能崩溃。当然也可能没问题,所以内存溢出往往不好查。

内存泄漏的问题就比理解,你没有还给系统,系统的内存就越来越少。直到没有可用内存。

泄漏是占着不用了,溢出是用不该用的地方;溢出一般会出事,泄漏在内存无限时不会出事。

泄漏是说你的程序有BUG 导致内存不释放。溢出是指内存不够用了 导致不够用的原因很多 泄漏只是其中一种。

GC在哪些情况下回进行回收工作?

  • 内存不足溢出时(0代对象充满时)
  • Windwos报告内存不足时,CLR会强制执行垃圾回收
  • CLR卸载AppDomian,GC回收所有
  • 调用GC.Collect
  • 其他情况,如主机拒绝分配内存,物理内存不足,超出短期存活代的存段门限

using() 语法是如何确保对象资源被释放的?如果内部出现异常依然会释放资源吗?

using() 只是一种语法形式,其本质还是try…finally的结构,可以保证Dispose始终会被执行。

了解过非托管资源回收?

.NET中提供释放非托管资源的方式主要是:Finalize() 和 Dispose()。

  • Dispose需要手动调用,在.NET中有两中调用方式:

    //方式1:显示接口调用
    SomeType st1=new SomeType();
    //do sth
    st1.Dispose();
     
    //方式2:using()语法调用,自动执行Dispose接口
    using (var st2 = new SomeType())
    {
        //do sth
    }
  • Finalize()可以确保非托管资源会被释放,但需要很多额外的工作(比如终结对象特殊管理),而且GC需要执行两次才会真正释放资源。听上去好像缺点很多,她唯一的优点就是不需要显示调用。

Finalize() 和 Dispose() 之间的区别?

Finalize() 和 Dispose()都是.NET中提供释放非托管资源的方式,他们的主要区别在于执行者和执行时间不同:

  • finalize由垃圾回收器调用;dispose由对象调用。
  • finalize无需担心因为没有调用finalize而使非托管资源得不到释放,而dispose必须手动调用。
  • finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;而dispose一调用便释放非托管资源。
  • 只有class类型才能重写finalize,而结构不能;类和结构都能实现IDispose。

另外一个重点区别就是终结器会导致对象复活一次,也就说会被GC回收两次才最终完成回收工作,这也是有些人不建议开发人员使用终结器的主要原因。

.NET中的托管堆中是否可能出现内存泄露的现象?

是的,可能会。比如:

  • 不正确的使用静态字段,导致大量数据无法被GC释放;
  • 没有正确执行Dispose(),非托管资源没有得到释放;
  • 不正确的使用终结器Finalize(),导致无法正常释放资源;
  • 其他不正确的引用,导致大量托管对象无法被GC释放;

在托管堆上创建新对象有哪几种常见方式?

  • new一个对象;
  • 字符串赋值,如string s1=”abc”;
  • 值类型装箱;

C#可否对内存进行直接的操作?

C#在Unsafe 模式下可以使用指针对内存进行操作, 但在托管模式下不可以使用指针。

  1. 在 Visual Studio 开发环境中设置/unsafe(启用不安全模式)编译器选项 打开项目的“属性”页。 单击“生成”属性页。 选中“允许不安全代码”复选框。

  2. unsafe关键字表示不安全上下文,该上下文是任何涉及指针的操作所必需的。 可以在类型或成员的声明中使用 unsafe修饰符。 因此,类型或成员的整个正文范围均被视为不安全上下文。例如,以下是用 unsafe 修饰符声明的方法:

    unsafe static void FastCopy(byte[] src, byte[] dst, int count)
    {
    	// Unsafe context: can use pointers here.
    }

    不安全上下文的范围从参数列表扩展到方法的结尾,因此指针在以下参数列表中也可以使用:

    unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}

    还可以使用不安全块从而能够使用该块内的不安全代码。例如:

    unsafe
    {
    	// Unsafe context: can use pointers here.
    }

    若要编译不安全代码,必须指定 /unsafe编译器选项。 无法通过公共语言运行库验证不安全代码。