GC总结

内存基础知识

下面的列表总结了重要的 CLR内存概念。

·        每个进程都有其自己单独的虚拟地址空间。同一台计算机上的所有进程共享相同的物理内存,如果有页文件,则也共享页文件。

·        默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式虚拟地址空间。

·                    作为一名应用程序开发人员,您只能使用虚拟地址空间,请勿直接操控物理内存。垃圾回收器为您分配和释放托管堆上的虚拟内存。如果您编写的是本机代码,请使用Win32 函数处理虚拟地址空间。 这些函数为您分配和释放本机堆上的虚拟内存。

·        虚拟内存有三种状态:

o   可用。 该内存块没有引用关系,可用于分配。

o   保留。 内存块可供您使用,并且不能用于任何其他分配请求。但是,在该内存块提交之前,您无法将数据存储到其中。

o   提交。 内存块已指派给物理存储。

·        可能会存在虚拟地址空间碎片。就是说地址空间中存在一些被称为孔的可用块。 当请求虚拟内存分配时,虚拟内存管理器必须找到满足该分配请求的足够大的单个可用块。 即使您具有 2 GB 的可用空间,2 GB 的分配请求也有可能会不成功,除非所有这些空间必须位于单个的地址块中。

·        如果用完保留的虚拟地址空间或提交的物理空间,则可能会用尽内存。

进程初始化期间,CLR保留两个区段(segment)的虚拟地址空间:一个区段是普通堆,另一个区段是大对象堆。每个区段的大小是不同的。对于客户端应用程序,每个区段约为16MB;对于服务器应用程序,每个区段大约为64MB。还有一些其他因素会影响区段的大小,例如是32位还是64位操作系统上运行,以及机器安装的CPU数量,在CPU较多的机器上,区段会小一些。随着区段装满非垃圾对象,CLR会分配更多的区段,这个操作一直继续,直到整个进程都满了为止。

垃圾回收是在第0代满的时候发生的。使用代(generation)的机制的唯一目的就是提高性能。基本思路是,第0代是最近分配的对象,从未被垃圾回收算法检查过。在一次垃圾回收中存活下来的对象被提升到另一代。如经过一次垃圾回收,第0代被提升为第1代;第1代被提升为第2代。

GC会检查托管堆中是否有应用程序不再使用的对象。如果有,他们使用的内存就可以被回收,如果回收完毕后,仍然没有可用内存,那么new操作符会抛出一个OutOfMemoryException。

垃圾回收

有5种事件导致垃圾回收:

1.     第0代满

2.     显式调用GC.Collect

3.     Windows报告内存不足

4.     CLR卸载AppDomain

5.     CLR关闭(进程关闭)

CLR使用一个高优先级,专用的线程来调用Finalize方法。对于前四种情况,如果一个Finalize方法进入了无限循环,那么这个特殊的线程就会被阻塞,其他Finalize方法就得不到调用。因为应用程序永远都不能回这些可终结对象的内存,只要程序还在运行,内存就会一直泄露。

对于第五种情况,每个Finalize方法大约有2秒的时间返回。如果2秒之内没有返回,CLR将直接杀死该进程。另外调用所有Finalize方法的时间超过了40秒,CLR也会杀死进程。这些值在将来有可能会发生改变。

即使实例的构造函数抛出了异常,实例的Finalize方法也会被调用,所以不应在Finalize方法中假定对象处于一致的状态。

对于一个可终结对象,在其构造函数被调用之前,此对象将被放入终结列表(finalization list)中。该列表是一个由GC控制的数据结构,它包含了所有可被终结的对象。在垃圾回收开始后,GC扫描终结列表,将垃圾对象从列表中移除(因为非垃圾对象会被标记,所以垃圾对象就是那些没有被标记的对象),然后将其添加到Freachable队列中,该队列是CLR的另一个内部数据结构。其中的每一个对象的Finalize方法都已经准备好被调用。当Freachable队列为空时,负责调用Finalize方法的线程会睡眠。当队列进入记录项,线程被唤醒,每一项都会被移除并调用Finalize方法。因此在Finalize方法中不应该对执行线程做任何假设。

虽然System.Object类型定义了Finalize方法,但是CLR知道忽略它。

当对象被添加到Freachable队列时,这个对象(及其引用的对象)复活了,因为对象现在是可达的了。现在GC不再认为对象是垃圾,所以GC不会回收这些对象的内存。此时特殊的线程清空Freachable队列并调用Finalize方法。当下一次垃圾回收的时候,会发现被终结的对象是垃圾对象,将会进行内存回收。整个过程中,需要两次垃圾回收才能收回这些对象占有的内存。实际情况下可终结对象会被提升代,所以需要不止两次回收才能收回这些对象占有的内存。

调用GC.SupressFinalize方法,将会打开当前对象的一个标志位,次标志位被打开后,CLR就不会把对象添加到Freachable队列。

大对象堆

任何大于或等于85000字节的对象都被视为大对象,大对象从大对象堆中分配。大对象和小对象一样终结和释放,但是大对象永不压缩。但是此行为在4.5.1中可以更改。大对象总是属于第二代,所以大对象很难被回收,所以尽量不要分配临时大对象。

在4.5.1中,使用如下代码压缩大对象堆,每次压缩完毕,GCSettings.LargeObjectHeapCompactionMode被重置为默认值。还要注意的是,后台垃圾回收器永远不会压缩大对象堆。

GCSettings.LargeObjectHeapCompactionMode =GCLargeObjectHeapCompactionMode.CompactOnce;

GC.Collect(); 

预测大量内存操作是否成功

实现一个算法时,可能需要事先知道该算法需要大量内存。如果直接执行算法,有可能抛出OutOfMemoryException,这种情况下,之前的工作算是白做了。.NetFramework提供System.Runtime.MemoryFailPoint类,它允许在消耗内存的算法开始之前检测是否有充足内存。

创建 MemoryFailPoint 对象并指定下操作 (MB)需要使用MB 的内存位数。 如果没有足够的内存不可用, 将引发InsufficientMemoryException 异常。 MemoryFailPoint 运行在 16 MB 粒度。 所有值小于 16MB 视为 16 MB,同时,其他值视为 16 MB 的下一个更大的多个。

以下代码保留了1G内存用于执行算法,

try

{

    using (MemoryFailPoint mfp =new MemoryFailPoint(1000))

    {

        //Do Anything needs 1G memory

    }

}

catch (InsufficientMemoryException)

{

    Console.WriteLine("InsufficientMemory");

}

如果MemoryFailPoint类的构造器没有抛出异常,表明逻辑上已经保留了请求的内存。但是请求的物理内存尚未分配。这表示,算法只是更有可能获得所需的内存并允许成功,并不表示一定会分配到所需的物理内存。此类只是帮助你写更健壮的程序。

线程劫持

当GC开始内测回收的时候,所有线程都必须挂起,因为这些线程不能再访问内存中的对象。这是因为GC线程要压缩内存,压缩过程中,对象的引用可能变得无效,所以只能在GC完成后,其他线程才能继续执行。

GC开始的时候,CLR会对其他线程进行劫持,使其进入一个特殊的函数,而这个函数将会进入一个临界区。所以如果在GC线程正在垃圾回收的时候,使用WinDbg查看进程中所有其他线程的堆栈,那么回发现,所有这些线程都进入了同一个方法,并且都试图获取同一个临界区。

并不是任何时刻都可以劫持线程,必须等待线程进入一个安全点。如果CLR创建了一个新对象,但是还没有将对象赋值给变量,此时劫持线程就不可行的,或者说线程不在一个安全点上。若是此时劫持线程并进行垃圾回收,那么新对象将被垃圾回收,而变量指向一个无效地址。

垃圾回收模式

垃圾回收模式包括工作站模式和服务器模式,使用如下配置节来配置模式,默认情况下使用工作站模式。

<runtime>

   <gcServer enabled="true"/>

 </runtime>

还可以在运行时获取垃圾回收模式是否为服务器模式,如下:

GCSettings.IsServerGC{get;}

该模式不能在运行时修改。

GC延迟模式

有四种延迟模式可供选择

·        Batch

·        Interactive

·        LowLatency

·        SustainedLowLatency

该模式可以在运行时获取或者设置,如下:

GCSettings.LatencyMode= GCLatencyMode.SustainedLowLatency;

这里面有一些版本问题,第四种模式只被.Net Framework4.0支持,而且只能用于工作站模式,但是到了.Net Framework 4.5,也可以用于服务器模式。

.Net Framework提供了配置节用于配置延迟模式,如下:

<gcConcurrentenabled="true"/>

要注意的是,这是个bool值,而且名称也不是gcLatencyMode。所以不能完全对应到四种延迟模式,具体的对应关系如下:

 

gcServer=true

gcServer=false

gcConcurrent =true

·         .Net4.5之前:Batch

·         .Net 4.5之后:SustainedLowLatency

 

·         .Net4.0之前:Interactive

·         .Net 4.0之后:SustainedLowLatency

 

gcConcurrent =false

Batch

Batch

对于服务器模式Interactive和LowLatency模式是无效的,但是在运行却能看到LatencyMode是Interactive。这也许是.Net的Bug。

总结一下,工作站GC支持所有四种模式,服务器GC支持Batch和SustainedLowLatency模式。

对于四种延迟模式的具体作用,解释如下:

·        Batch

Batch模式是全阻塞模式,一旦GC开始运行,所有非GC线程都会被挂起,直到垃圾回收结束。

·        Interactive

此模式是工作站GC的默认模式(在.Net 4.0之前)。GC会使用多个线程进行并发垃圾回收,但是此并发只针对第二代对象。第零代和第一代对象永远使用全阻塞模式回收。具体的回收场景是,某个对象的创建引发了垃圾回收,GC使用创建此对象的线程回收第0,1代垃圾,同时有一个专用线程用于在后台回收第二代垃圾。

·        LowLatency

此模式用于对时间敏感的进程。GC会全力避免回收第二代,所以应该在短时间内应用此模式,并在不需要此模式的时候设置回原来的模式。

·        SustainedLowLatency

作用域Interactive基本相同,但是可以应用在服务器GC模式。

区别是:Interactive模式的专用线程在GC的过程中,不允许发起另外一个GC过程,而且只能在内存段中剩余的空间中分配内存。

SustainedLowLatency模式允许在后台GC运行中启动另一次针对第0和1代的GC过程,甚至允许创建另一个新段来进行内存分配。

工作站和服务器垃圾回收比较

以下是工作站垃圾回收的线程处理和性能注意事项:

·        回收发生在触发垃圾回收的用户线程上,并保留相同优先级。因为用户线程通常以普通优先级运行,所以垃圾回收器(在普通优先级线程上运行)必须与其他线程竞争 CPU 时间。

·        不会挂起运行本机代码的线程。(针对后台回收且只限于标记过程)

·        工作站垃圾回收始终用在只有一个处理器的计算机上,而不管 <gcServer> 设置如何。 如果您指定服务器垃圾回收,则CLR 会使用工作站垃圾回收,并禁用并发。

以下是服务器垃圾回收的线程处理和性能注意事项:

·        回收发生在以 THREAD_PRIORITY_HIGHEST 优先级运行的多个专用线程上。

·        为每个 CPU(逻辑CPU)提供一个用于执行垃圾回收的专用线程和一个堆,并将同时回收这些堆。 每个堆都包含一个小对象堆和一个大对象堆,并且所有的堆都可由用户代码访问。 不同堆上的对象可以相互引用。

·        因为多个垃圾回收线程一起工作,所以对于相同大小的堆,服务器垃圾回收比工作站垃圾回收更快一些。

·        服务器垃圾回收通常具有更大的段。

·        服务器垃圾回收会占用大量资源。例如,如果在一台具有 4 个处理器的计算机上运行了12 个进程,则在它们都使用服务器垃圾回收的情况下,将有 48 个专用垃圾回收线程。在高内存加载的情况下,如果所有进程开始执行垃圾回收,则垃圾回收器将要计划 48 个线程。如果运行应用程序的数百个实例,请考虑使用工作站垃圾回收并禁用并发垃圾回收。这可以减少上下文切换,从而提高性能。如果启用后台回收,启用的线程将会更多。

如果要手动回收内存,那么就可以指定是否使用后台回收功能,如下:

public static void Collect(int generation,GCCollectionMode mode, bool blocking);

blocking参数为false,则表示使用后台回收功能。

如果要分配超过2G内存的对象,需要使用如下配置:

<gcAllowVeryLargeObjects  enabled="true|false" />

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 要解决Full GC的问题,可以采取以下几个方法。首先,避免手动调用System.gc()方法,因为这可能会增加Full GC的次数,导致系统性能下降。可以通过禁止RMI调用System.gc来实现,可以使用-XX:DisableExplicitGC参数来禁用该调用。其次,可以查看GC日志并关注Full GC触发时老年代中对象回收前和回收后的情况,以及执行回收消耗的时间。这可以帮助我们确定是什么原因触发了Full GC。如果发现是System.gc()导致的,就说明代码中存在调用执行Full GC的情况。另外,可以使用jstat -gcutil命令来监测Full GC的次数、执行时间以及老年代占比的变化情况。如果发现老年代的占比增加快,说明可能存在大对象较多或者年轻代配置过小导致对象进入老年代过快。如果发现老年代持续高占用并且执行Full GC后占比基本无变化,说明可能存在对象一直存在引用未释放的情况。通过以上方法,我们可以对Full GC进行定位和解决。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [FullGC问题分析及解决办法总结](https://blog.csdn.net/KevinChen2019/article/details/125354796)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值