GC

在.NET框架包含一个托管堆,所有的.NET语言在分配引用类型对象都要使用它,像值类型这样的轻量级别对象始终分配在栈中,但是所有的类实例和数组都被生成在一个内存池中,这个内存池就是托管堆

垃圾收集器的托管的基本算法很简单:

1、将所有的托管内存标记为垃圾

2、寻找正在使用的内存快,并将他们标记为有效

3、释放所有没有被使用的内存块

4、整理堆以减少碎片

看上去很简单,但是垃圾回收器实际采用的步骤和堆管理系统的其他部分并非微不足道,其中常常涉及为提高性能而作的优化设计。举例来说,垃圾收集遍历整个内存池具有很高的开销。然而,研究表明大部分在托管堆上分配的对象只有很短的生存期,因此堆被分成三个段,称作generations。新分配的对象被放在generation 0中。这个generation是最先被回收的----在这个generation中最可能找到不再使用的内存,由于它的尺寸很小(小到足以放进处理器L2cache中),因此在它里面的回收将是最快和最高效的。

托管堆的另外一种优化操作于locality of reference股则有关。该规则表明,一起分配的对象经常被一起使用。如果对象们在堆中位置很紧凑的话,高速缓存的性能将会得到提高。由于托管堆的天性,对象们总是被分配在连续的地址上,托管堆总是保持紧凑,结果使得对象们始终彼此靠近,永远不会分的很远。这一点于标准堆提供的非托管代码形成了鲜明的对比,在标准堆中,堆很容易变成碎片,而且一起分配的对象经常分得很远。

还有一种优化是与大对象有关的,通常,大对象具有很长的生存期。当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。因为移动大对象所带来的开销超过了整理这部分堆所提高的性能。

关于外部资源(External  Resource)的问题

   垃圾收集器能够有效的管理从托管堆中释放的资源,但是资源回收操作只有在内存紧张而触发一个回收动作时才执行。那么,类是怎样来管理像数据库连接或者窗口句柄这样有限资源的呢?等待,直到垃圾回收被触发之后再清理数据库连接或者文件句柄并不是一个好方法,这回严重降低系统的性能。

所有拥有外部资源的类,在这些资源已经不再用到的时候,都应当执行Close或者Dispose方法。从Beta2(译注:本文所有的Beta2)开始,Dispose模式通过IDisposable接口来实现。

需要清理外部资源的类还应当一个终止操作(finalizer)。在c#中,创建终止操作的首选方式是在析构函数中实现,在Framwork层,终止操作的实现则是通过重载System.Object.Finalize方法。以下两种实现终止操作的方法是等效的:

~OverdueBookLocator()

{

    Dispose(false);

}

public  void Finalize()

{

  base.Finalize();

  Dispose(false);

}

在c#中,同时在Finalize方法和析构函数实现终止操作将会导致错误的产生。

除非你有足够的理由,否则你不应该创建析构函数或者Finalize方法。终止操作会降低系统的性能,并且增加执行期的内存开销。同时,由于终止操作被执行的方式,你并不能保证何时一个终止操作会被执行。

内存分配和垃圾回收的细节

对GC有了一个总体印象之后,让我们来讨论关于托管堆中的分配于回收工作的细节。托管堆看起来与我们已经收悉的c++编程中的传统堆一点都不像。在传统的堆中,数据结构习惯于大块的空闲内存。在其中查找特定大小的内存快是一件很耗时的工作,尤其是当内存中充满碎片的时候。与此不同,在托管堆中,内存被组制成连续的数组,指针总是巡着已经被使用的内存和未被使用的内存之间的边界移动。当内存被分配的时候,指针只是简单递增——有此而来的一个好处是,分配操作的效率得到了很大的提升。

当对象被分配的时候,他们一开始被放在generation 0中。当generation 0的大小快要达到它的上限的时候,一个只在generation 0中执行的回收操作被触发。由于generation 0的大小很小,因此这将是一个非常快得GC过程。这个GC过程的结果是将generation 0彻底的刷新了一遍。不再使用的对象被释放,确实正被使用的对象被整理并移入generation 1中。

    当generation 1的大小随着generation 0中移动的对象数量的增加而接近他的上限的时候,一个回收动作被触发来在generation 0和generation 1中执行GC过程,如同generation 0中一样,不再使用的对象被释放,正被使用的对象被整理并移入了下一个generation中。大部分GC过程的主要目标是generation 0,因为在generation 0中最可能存在大量的已不再使用的临时变量。堆generation 2的回收过程具有很高的开销,并且此过程只有在generation 0和generation 1的GC过程不能释放足够的内存时才会被触发。如果对generation 2的过程GC过程仍然不能释放足够的内存,那么系统就抛出OutOfMemoryException异常(内存溢出)

   带有终止操作的对象的垃圾收集过程要稍微复杂一些。当一个带有终止操作的对象被标记为垃圾时,它并不会被立即释放。相反,它会被放置在一个终止队列(finalization  queue)中,此队列为这个对象建立一个引用,来避免这个对象被回收。后台线程为队列中的每个对象执行他们各自的终止操作,并且将已经执行终止操作的对象从终止队列中删除。只有那些已经执行过终止操作的对象才会在下一次垃圾回收过程中被内存中删除。这样做的一个后果是,等待被终止的对象有可能在它被清空之前,被移入更高一级generaton中,从而增加它被清除的延迟时间。

    需要执行终止操作的对象应当实现IDisposeable接口,以便客户程序通过此接口快速执行终止动作。IDispoable接口包含一个方法---Dispose.这个被Beta2引入的接口,采用一种在Beta2之前就已经被广泛使用的模式实现。从本质上讲,一个需要终止操作的对象暴漏出Dispose方法。这个方法被用来释放外部资源并抑制终止操作,就像下面这个程序片段演示的那样:

public class OverdueBookLocator:IDisposeable

{

   ~OverdueBookLocator()

   {

         InternalDispose(false);

   }

public void Dispose()

   {

        InternalDispose(true);

   }

protected void InternalDispose(bool diposing)

{

  if(isposing)

    {

        GC.SuppressFinalize(this);

    }

}

}

使用基于CLR的语言编译器开发的代码都成为托管代码。

托管堆是CLR中自动内存管理的基础。初始化新进程时,运行会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。

以下代码说明的很形象:

//引用类型(‘class’类类型)

class SomeRef

{

   public int32 x;

}

//值类型(‘struct’)

struct SomeVa

{

    public Int32 x;

}

static void Value TypeDemo()

{

   SomeRef r1=new SomeRef();//分配在托管堆

   SomeVal v1=new SomeVal();//分配堆栈上

   r1.x=5;//解析指针

   v1.x=5;//在堆栈上修改

  SomeRef r2=r1;//仅拷贝引用(指针)

  SomerVal v2=v1;//先在堆栈上分配,然后拷贝成员

   r1.x=8;//改变了r1,r2的值

   v1.x=9;//改变了v1,没有改变v2


}

-------------------------------------------------------------------------------------------------------

栈是内存中完全用于存储局部变量或成员(值类型数据)的高效区域,但其大小有限制。

托管堆占内存比栈大得多,当访问速度较慢。托管堆只用于分配内存,一般有CLR来出来释放问题。

当创建值类型数据时,在栈上分配内存;

当创建引用型数据时,在托管堆上分配内存并返回对象的引用。注意这个对象的引用,像其他局部变量一样也是保存在栈中,该引用指向的值则位于托管堆中。

如果创建一个包含值类型的引用类型,比如数组,其元素的值也是存放在托管堆中的某个地方,由使用该实体的变量引用;而值类型存储在使用它们的地方,有几处在使用,就有几个副本存在。

对于引用类型,如果在声明变量的时候没有使用new运算符,运行时不会给它分配托管堆的内存空间,而是在栈伤给她分配一个包含null值的引用。对于值类型,运行时会给它分配栈上的空间,并且调用构造函数来初始化对象的状态。

------------------------------------------------------------------------------------------------------

一、栈和托管堆

    通用类型系统(CTS)区分两种基本类型:值类型和引用类型。它们之间的根本区别在于它们在内存中的存储方式。.NET使用两种不同的物理内存快来存储数据------栈和托管堆:

   值类型总是在内存中占用一个预定义的字节数(例如,int类型占4个字节,而string类型占用的字节数会根据字符串的长度而不同)。当生命一个值类型变量时,会在栈中分配适当大小的内存(除了引用类型的值类型成员外,如类的int字段)。内存中的这个空间用来存储变量所包含的值。.NET维护一个栈指针,它包含栈中下一个可用的内存空间的地址。当一个变量离开作用域时,栈指针指向下移动被释放变量所占有的字节数。所以它仍指向下一个可用地址。

    引用变量也利用栈,但这时候栈包含的只是对另一个内存位置的引用,而不是实际值。这个位置是托管堆中的一个地址,和栈一样,他也维护一个指针,包含堆中下一个可用的内存地址。但是,堆不是先入后出的,因为对对象的引用可在我们的程序中传递(例如,作为参数传递给方法调用)。堆中的对象不会在程序的一个预定点离开作用域。为了在不适用在堆中分配的内存时将它释放。.NET定期执行垃圾回收集,垃圾收集递归检查应用程序中所有对象的引用。引用不再有效的对象使用的内存无法从程序中访问,该内存就可以回收。

二、类型层次结构

CTS定义了一种类型层次结构,该结构不仅仅描述了不同预定义类型,还指出了用户定义类型的层次结构中的

 

三、引用类型

引用类型包含一个指针,指向堆中存储对象本身的位置。因为引用类型只包含实际的值,对方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。

下图显示了声明一个字符串变量并把它作为参数传递给一个方法时所发生的事情。

string  s1="something";

DoSomething(s1);

   //.....

DoSomething(string  s2)

{

   //....

}

当声明一个字符变量s1时,一个值被压入栈中,它指向栈中的一个位置,在上图中,引用存放在地址1243044中,而实际的字符串存放在堆地址12662032中,当该字符串传递给一个方法中,在栈伤对应输入参数声明了一个新德变量(这次是在地址1243032上),保存在引用变量,即堆中内存位置中的值被传递给这个新德变量。

委托是引用方法的一种引用类型,类似于c++中的函数指针(两者的主要区别于委托包括调用其方法的对象)。

四、预定义的引用类型

有两种引用类型在c#中受到了特别的重视,他们的c#别名和预定义值类型的c#别名很相像。第一种是object(c#别名是object,o小写)。这是所有值类型和引用类型的最终基类。因为所有的类型派生object,所以可以把任何类型转换成Object,甚至值类型也可以转换。这个把值类型转换为Object的过程称为装箱。所有的值类型都派生自引用类型,在这件事看似矛盾的事情背后,装箱的作用不可或缺。

第二种是String类,字符串代表一个固定不变的Unicode字符序列,这种不变性意味着,一旦在堆中分配了一个字符串,他的值将永远不会改变,如果值该改变了,.NET就创建一个全新的String对象,并把它赋值给该变量,这意味着,字符串在很多方面都像值类型,而不像引用类型。如果把一个字符串传递给方法。然后在方法体内改变参数的值,这不会影响最初的字符串(当然,除非参数按引用传递的)。c#提供了别名String(s小写)来代表Ststem.String类,如果代码中使用String,必须在代码一开始添加Using System;这一行。使用内建的别名string则不需要using System; 

<think>我们正在讨论Java的垃圾回收机制,特别是Full GC。根据用户的要求,我需要解释Full GC的含义、触发原因以及影响。同时,我需要参考之前提供的引用内容,并在回答中自然地添加引用标识。 回顾引用内容: 引用[1]详细说明了Full GC的触发条件,包括: - 显式调用System.gc() - 老年代空间不足 - 方法区(Metaspace)空间不足 - 大对象分配失败 - 晋升失败(Minor GC后Survivor区对象晋升到老年代时空间不足) 引用[2]则提到如何避免Full GC,例如通过合理设置新生代和老年代的比例(使用-XX:NewRatio参数)。 因此,我的回答结构如下: 1. **Full GC的含义**:Full GC是Java虚拟机(JVM)中的一种垃圾回收过程,它会清理整个堆内存(包括新生代和老年代)以及方法区(或元空间)。与只针对新生代的Minor GC不同,Full GC的范围更广,耗时更长。 2. **触发Full GC的原因**:基于引用[1],我将详细列出触发条件,每个条件都附带解释,并在末尾添加引用标识[^1]。 3. **Full GC的影响**:主要影响是性能,因为Full GC会暂停所有应用线程(Stop-The-World),导致应用程序停顿。频繁的Full GC会严重影响系统响应时间和吞吐量。 4. **如何避免Full GC**:参考引用[2],可以调整JVM参数(如-XX:NewRatio)来优化内存分配,减少Full GC的发生。同时,还可以建议其他方法,如增大堆大小、优化代码等。 最后,按照用户要求,生成相关问题。 注意:在回答中,如果有数学表达式(比如在解释内存比例时),我需要按照系统级指令使用$...$或$$...$$格式。例如,当提到新生代和老年代的比例时,可以写成比例参数设置为$2$(即-XX:NewRatio=2)。 现在,构建回答内容: ### Full GC的含义 Full GC(完全垃圾回收)是Java虚拟机(JVM)中一种全局性的垃圾回收过程,它会同时清理**新生代(Young Generation)**、**老年代(Old Generation)** 以及**元空间(Metaspace,Java 8+)** 中的无用对象[^1]。与仅针对新生代的Minor GC相比,Full GC的回收范围更广,执行时间更长,通常会导致明显的应用程序停顿(Stop-The-World)。 ### 触发Full GC的原因 根据JVM的内存管理机制,Full GC的触发条件主要包括: 1. **老年代空间不足** 当老年代剩余空间无法容纳从新生代晋升的对象时,会触发Full GC[^1]。例如: $$ \text{老年代剩余空间} < \text{晋升对象总大小} $$ 2. **显式调用`System.gc()`** 代码中调用`System.gc()`会建议JVM执行Full GC,但JVM可能忽略该请求(取决于`-XX:+DisableExplicitGC`参数配置)[^1]。 3. **元空间(Metaspace)不足** 在Java 8及以上版本,如果元空间(存放类元数据)的内存耗尽,且无法再扩展,会触发Full GC[^1]。 4. **大对象分配失败** 尝试分配大对象(如超大数组)时,若新生代无法容纳且老年代空间也不足,会直接触发Full GC[^1]。 5. **晋升失败(Promotion Failure)** Minor GC后,Survivor区中的存活对象需要晋升到老年代,但老年代空间不足,此时会触发Full GC[^1]。 --- ### Full GC的影响 Full GC对应用程序性能有显著负面影响: - **长时间停顿(Stop-The-World)**:Full GC会暂停所有应用线程,停顿时间可达秒级甚至分钟级,影响系统响应速度。 - **吞吐量下降**:频繁Full GC会占用大量CPU资源,降低有效业务处理能力。 - **内存碎片问题**:Full GC可能引发内存碎片化,进一步加剧后续GC压力。 --- ### 避免Full GC的优化策略 1. **调整堆内存比例** 通过`-XX:NewRatio`参数合理设置新生代与老年代的比例(例如$2$表示老年代是新生代的2倍),减少对象过早晋升到老年代的概率[^2]。 ```bash java -XX:NewRatio=2 -jar app.jar ``` 2. **增大堆内存总量** 适当增加`-Xmx`(最大堆内存)和`-Xms`(初始堆内存)的值,为老年代和元空间预留更多空间。 3. **避免代码中调用`System.gc()`** 可通过添加JVM参数`-XX:+DisableExplicitGC`禁用显式GC调用。 4. **元空间监控与扩展** 设置`-XX:MaxMetaspaceSize`限制元空间上限,避免无限制增长触发Full GC。 5. **选择低停顿垃圾回收器** 如G1(Garbage-First)或ZGC,通过增量回收减少Full GC频率。 --- §§相关问题§§ 1. 如何通过JVM日志分析Full GC的触发原因? 2. G1垃圾回收器如何减少Full GC的发生? 3. 元空间(Metaspace)的内存管理机制是怎样的? 4. 哪些代码写法会导致频繁触发Full GC? [^1]: Full GC触发条件的详细技术说明 [^2]: JVM内存比例调整对Full GC的影响 </think>### Full GC的含义与机制 **Full GC**(完全垃圾回收)是 Java 虚拟机(JVM)中一种全局性的垃圾回收过程,涉及清理整个堆内存(包括**新生代**、**老年代**)以及**方法区**(Java 8+ 为 Metaspace)[^1]。其核心目标是释放不再使用的对象内存,避免内存溢出(OOM)。与仅针对新生代的 **Minor GC** 不同,Full GC 的回收范围更广、耗时更长(通常可达秒级),且会暂停所有应用线程(Stop-The-World)。 --- ### Full GC 的触发原因 基于 JVM 内存管理机制,触发条件主要包括: 1. **老年代空间不足** - 当老年代剩余空间无法容纳从新生代晋升的对象时触发[^1],数学表示为: $$ \text{老年代剩余空间} < \text{晋升对象总大小} $$ - 常见诱因:长期存活对象积累(如缓存未清理)或内存泄漏。 2. **显式调用 `System.gc()`** - 开发者主动调用 `System.gc()` 可能建议 JVM 执行 Full GC,但 JVM 可忽略该请求(取决于 `-XX:+DisableExplicitGC` 配置)[^1]。 3. **Metaspace 空间不足** - Java 8+ 中,Metaspace(存放类元数据)耗尽时可能触发 Full GC[^1]。 - 常见于频繁动态生成类(如反射、CGLIB 代理)。 4. **大对象分配失败** - 对象大小超过新生代单块区域(如 Eden 区)容量,且老年代无法容纳该对象时触发[^1]。 5. **晋升失败(Promotion Failure)** - Minor GC 后,Survivor 区存活对象需晋升至老年代,但老年代空间不足[^1]。 --- ### Full GC 的影响 | **影响维度** | **具体表现** | |--------------------|-----------------------------------------------------------------------------| | **性能** | 应用线程暂停(Stop-The-World),响应延迟骤增,吞吐量下降。停顿时间与堆大小成正比。 | | **系统稳定性** | 频繁 Full GC 导致服务超时,甚至引发雪崩故障。 | | **内存碎片** | Full GC 可能加剧老年代内存碎片化,进一步降低后续内存分配效率。 | | **资源消耗** | 高 CPU 占用(GC 线程全力工作),挤压业务计算资源。 | --- ### 避免 Full GC 的关键优化策略 1. **调整内存区域比例** - 通过 `-XX:NewRatio` 合理分配新生代与老年代比例(例如 `-XX:NewRatio=2` 表示老年代占堆的 $2/3$)[^2],延缓对象晋升速度。 - 示例: ```bash java -XX:NewRatio=2 -Xmx4g -jar app.jar ``` 2. **增大堆空间或 Metaspace** - 适当提高 `-Xmx`(最大堆内存)和 `-XX:MaxMetaspaceSize`(元空间上限)。 3. **选择低停顿垃圾回收器** - 使用 G1(Garbage-First)或 ZGC,通过**分区回收**和**并发标记**减少 Full GC 频率。 - 示例: ```bash java -XX:+UseG1GC -jar app.jar ``` 4. **代码层面优化** - 避免调用 `System.gc()`。 - 减少大对象分配(如拆分大数组)。 - 及时清理无引用对象(如缓存设置 TTL)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值