C#中回收垃圾回收机制几种方式是那种方式回收的,是程序结束才会回收,还是方法结束后会回收。在方法中new了几个产量

C#内存回收机制_百度文库
您的浏览器Javascript被禁用,需开启后体验完整功能,
赠送免券下载特权
10W篇文档免费专享
部分付费文档8折起
每天抽奖多种福利
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
C#内存回收机制
&&C#内存回收机制
阅读已结束,下载本文需要
想免费下载本文?
定制HR最喜欢的简历
下载文档到电脑,同时保存到云知识,更方便管理
加入VIP
还剩3页未读,
定制HR最喜欢的简历
你可能喜欢面试之C#--垃圾回收器什么时候回收?
每个对象只有在该对象不存在任何引用才会被垃圾回收起回收。
可以调用静态方法System.GC.Collect()垃圾回收器,但是不建议这么做;
用using语句可以有效的自动释放掉资源。
实在没有办法才用从析构函数中调用Dispose方法。
class Example:IDisposable
~example(){
Dispose();
public virtual void Dispose();
if(!this.disposed){
//在此释放稀缺资源
this.disposed=
GC.SuppressFinalize(this);
public void SomeBehavior()//示例方法
checkifdisposed();
private void checkifdisposed(){
if(this.disposed)
throw new ObjectDisposedException("Example");
private bool disposed=
---------------------------------------------------------------------------------------------------------------------
附加C#垃圾回收器的分级回收机制:
C#中的回收器是分代的垃圾回收器(Gererational Garbage Collector) 它将分配的对象分为3个类别或代。(可用GC.GetGeneration方法返回任意作为参数的对象当前所处的代)最近被分配内存的对象被放置于第0代,因为第0代很小,小到足以放进处理器的二级(L2)缓存,所以它能够提供对对象的快速存取。经过一轮垃圾回收后,仍然保留在第
0代中的对象被移进第1代中,再经过一轮垃圾内存回收后,仍然保留在第1代中的对象则被移进第2代中,第2代中包含了生存期较长的对象。
在C#中值类型是在堆栈中分配内存,它们有自身的生命周期,所以不用对它们进行管理,会自动分配和释放。而引用类型是在堆中分配内存的。
所以它的分配和释放就需要像回收机制来管理。
C#为一个对象分配内存时,托管堆可以立即返回新对象所需的内存,因为托管堆类似于简单的字节数组,有一个指向第一个可用内存空间的指针,
指针像游标一样向后移动,一段段内存就分配给了正在运行的程序的对象。
在不需要太多垃圾回收的程序小,托管堆性能优于传统的堆。
当第0代中没有可以分配的有效内存时,就触发了第0代中的一轮垃圾回收,它将删除那些不再被引用的对象,并将当前正在使用的对象移至第1代。而当第0代垃圾回收后依然不能请求到充足的内存时,就启动第1代垃圾回收。如果对各代都进行了垃圾回收后仍没有可用的内存就会引发一个OutOfMemoryException异常。
在有些情况下,类可以提供一个终结器在对象被销毁时执行,终结器是一个名为
Finalize的受保护的方法:
protected void Finalize()
base.Finalize();
释放外部资源
垃圾回收器使用名为“终止队列”的内部结构跟踪具有
方法的对象。每次您的应用程序创建具有Finalize 方法的对象时,垃圾回收器都在终止队列中放置一个指向该对象的项。
托管堆中所有需要在垃圾回收器回收其内存之前调用它们的终止代码的对象都在终止队列中含有项。(实现Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用Finalize 方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。
这时,它不能回收具有终结器的不可访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用Finalize 方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。)
Dispose方法
在不使用终结器时,可以考虑使用Dispose方法,你可以使用这个方法来释放所保存包括的在托管对象引用在内的任何资源。但使用它时需用
GC.SuppressFinalize
来告知运行时这些对
象不需要终结。如下所示
public void Dispose()
object.Dispose();
dbConnection.Dispose();
GC.SuppressFinalize(this); //
申明不需要终结
创建并使用了Dispose方法的对象,就需要使用完该对象之后调用这些方法,最好是在Finally中调用。
以下代码演示来自
// Design pattern for the base class.
// By implementing IDisposable, you are announcing that instances
// of this type allocate scarce resources.
public class BaseResource : IDisposable
// Pointer to an external unmanaged resource.
private IntP
// Other managed resource this class uses.
private Component C
// Track whether Dispose has been called.
private bool disposed =
// Constructor for the BaseResource object.
public BaseResource()
// Insert appropriate constructor code here.
// Implement IDisposable.
// Do not make this method virtual.
// A derived class should not be able to override this method.
public void Dispose()
Dispose(true);
// Take yourself off the Finalization queue
// to prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly
// or indirectly by a user's code. Managed and unmanaged resources
// can be disposed.
// If disposing equals false, the method has been called by the
// runtime from inside the finalizer and you should not reference
// other objects. Only unmanaged resources can be disposed.
protected virtual void Dispose(bool disposing)
// Check to see if Dispose has already been called.
if (!this.disposed)
// If disposing equals true, dispose all managed
// and unmanaged resources.
if (disposing)
// Dispose managed resources.
Components.Dispose();
// Release unmanaged resources. If disposing is false,
// only the following code is executed.
CloseHandle(handle);
handle = IntPtr.Z
// Note that this is not thread safe.
// Another thread could start disposing the object
// after the managed resources are disposed,
// but before the disposed flag is set to true.
// If thread safety is necessary, it must be
// implemented by the client.
disposed =
// Use C# destructor syntax for finalization code.
// This destructor will run only if the Dispose method
// does not get called.
// It gives your base class the opportunity to finalize.
// Do not provide destructors in types derived from this class.
~BaseResource()
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of
// readability and maintainability.
Dispose(false);
// Allow your Dispose method to be called multiple times,
// but throw an exception if the object has been disposed.
// Whenever you do something with this class,
// check to see if it has been disposed.
public void DoSomething()
if (this.disposed)
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!C#垃圾回收和托管堆及堆栈
值类型和引用类型的内存分配:
值类型变量与引用类型变量的内存分配模型不一样。
为了理解清楚这个问题,读者首先必须区分两种不同类型的内存区域:线程堆栈(Thread Stack)和托管堆(Managed Heap)。
每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多线程(thread),每个线程都拥有块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉。
所有值类型的变量都是在线程堆栈中分配的。另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)”。用new 关键字创建的类的对象时,分配给对象的内存单元就位于托管堆中。
在程序中我们可以随意地使用new 关键字创建多个对象,因此,托管堆中的内存资源是可以动态申请并使用的,当然用完了必须归还。打个比方更易理解:托管堆相当于一个旅馆,其中的房间相当于托管堆中所拥有的内存单元。
当程序员用new 方法创建对象时,相当于游客向旅馆预订房间,旅馆管理员会先看一下有没有合适的空房间,有的话,就可以将此房间提供给游客住宿。当游客旅途结束,要办理退房手续,房间又可以为其他旅客提供服务了。
引用类型共有四种:类类型、接口类型、数组类型和委托类型。
所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。
严格地说,我们常说的“对象变量”其实是类类型的引用变量。但在实际中人们经常将
引用类型的变量简称为“对象变量”,用它来指代所有四种类型的引用变量。
堆栈和托管堆:
内存格局通常分为四个区
全局数据区:存放全局变量,静态数据,常量
代码区:存放所有的程序代码
栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等,
堆区:即自由存储区
堆栈和堆(托管堆)都在进程的虚拟内存中。(在32位处理器上每个进程的虚拟内存为4GB)
堆栈中存储值类型。
堆栈实际上是向下填充,即由高内存地址指向低内存地址填充。
堆栈的工作方式是先分配内存的变量后释放(先进后出原则)。
堆栈中的变量是从下向上释放,这样就保证了堆栈中先进后出的规则不与变量的生命周期起冲突!
堆栈的性能非常高,但是对于所有的变量来说还不太灵活,而且变量的生命周期必须嵌套。
通常我们希望使用一种方法分配内存来存储数据,并且方法退出后很长一段时间内数据仍然可以使用。此时就要用到堆(托管堆)!
堆(托管堆)heap
堆(托管堆)存储引用类型。
此堆非彼堆,.NET中的堆由垃圾收集器自动管理。
与堆栈不同,堆是从下往上分配,所以自由的空间都在已用空间的上面。
比如创建一个对象:
&span style="font-family:Arial B"&S
stu = new Student();&/span&
申明一个Student的引用cus,在堆栈上给这个引用分配存储空间。这仅仅只是一个引用,不是实际的Student对象!
stu占4个字节的空间,包含了存储Student的引用地址。接着分配堆上的内存以存储Student对象的实例,假定Student对象的实例是32字节,为了在堆上找到一个存储Student对象的存储位置。
.NET运行库在堆中搜索第一个从未使用的,32字节的连续块存储Student对象的实例!
然后把分配给Student对象实例的地址赋给stu变量!
从这个例子中可以看出,建立对象引用的过程比建立值变量的过程复杂,且不能避免性能的降低!
实际上就是.NET运行库保存对的状态信息,在堆中添加新数据时,堆栈中的引用变量也要更新。性能上损失很多!
有种机制在分配变量内存的时候,不会受到堆栈的限制:把一个引用变量的值赋给一个相同类型的变量,那么这两个变量就引用同一个堆中的对象。
当一个应用变量出作用域时,它会从堆栈中删除。但引用对象的数据仍然保留在堆中,一直到程序结束 或者 该数据不被任何变量应用时,垃圾收集器会删除它。
栈是内存中完全用于存储局部变量或成员字段(值类型数据)的高效的区域,但其大小有限制。
托管堆所占内存比栈大得多,当访问速度较慢。托管堆只用于分配内存,一般由CLR(Common Language Runtime)来处理内存释放问题。
当创建值类型数据时,在栈上分配内存;
当创建引用型数据时,在托管堆上分配内存并返回对象的引用。注意这个对象的引用,像其他局部变量一样也是保存在栈中的。该引用指向的值则位于托管堆中。
如果创建了一个包含值类型的引用类型,比如数组,其元素的值也是存放在托管堆中而非栈中的。当从数组中检索数据时,获得本地使用的元素值的副本,而该副本这时候就是存放在栈中的了。所以,不能笼统的说“值类型保存在栈中,引用类型保存在托管堆中”。
值类型和引用类型的区别:引用类型存储在托管堆的唯一位置中,其存在于托管堆中某个地方,由使用该实体的变量引用;而值类型存储在使用它们的地方,有几处在使用,就有几个副本存在。
对于引用类型,如果在声明变量的时候没有使用new运算符,运行时就不会给它分配托管堆上的内存空间,而是在栈上给它分配一个包含null值的引用。对于值类型,运行时会给它分配栈上的空间,并调用默认的构造函数,来初始化对象的状态。
托管堆优化:
  看上去似乎很简单,但是垃圾收集器实际采用的步骤和堆管理系统的其他部分并非微不足道,其中常常涉及为提高性能而作的优化设计。举例来说,垃圾收集遍历整个内存池具有很高的开销。然而,研究表明大部分在托管堆上分配的对象只有很短的生存期,因此堆被分成三个段,称作generations。新分配的对象被放在generation 0中。这个generation是最先被回收的——在这个generation中最有可能找到不再使用的内存,由于它的尺寸很小(小到足以放进处理器的L2
cache中),因此在它里面的回收将是最快和最高效的。
  托管堆的另外一种优化操作与locality of reference规则有关。该规则表明,一起分配的对象经常被一起使用。如果对象们在堆中位置很紧凑的话,高速缓存的性能将会得到提高。由于托管堆的天性,对象们总是被分配在连续的地址上,托管堆总是保持紧凑,结果使得对象们始终彼此靠近,永远不会分得很远。这一点与标准堆提供的非托管代码形成了鲜明的对比,在标准堆中,堆很容易变成碎片,而且一起分配的对象经常分得很远。
  还有一种优化是与大对象有关的。通常,大对象具有很长的生存期。当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,这部分堆永远不会被整理。因为移动大对象所带来的开销超过了整理这部分堆所能提高的性能。
  关于外部资源(External Resources)的问题
  垃圾收集器能够有效地管理从托管堆中释放的资源,但是资源回收操作只有在内存紧张而触发一个回收动作时才执行。那么,类是怎样来管理像数据库连接或者窗口句柄这样有限的资源的呢?等待,直到垃圾回收被触发之后再清理数据库连接或者文件句柄并不是一个好方法,这会严重降低系统的性能。
  所有拥有外部资源的类,在这些资源已经不再用到的时候,都应当执行Close或者Dispose方法。从Beta2(译注:本文中所有的Beta2均是指.NET Framework Beta2,不再特别注明)开始,Dispose模式通过IDisposable接口来实现。这将在本文的后续部分讨论。
  需要清理外部资源的类还应当实现一个终止操作(finalizer)。在C#中,创建终止操作的首选方式是在析构函数中实现,而在Framework层,终止操作的实现则是通过重载System.Object.Finalize 方法。以下两种实现终止操作的方法是等效的:
&span style="font-family:Arial B"&~OverdueBookLocator()
   Dispose(false);
  public void Finalize()
   base.Finalize();
   Dispose(false);
  }&/span&
  在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)中,此队列为这个对象建立一个引用,来避免这个对象被回收。后台线程为队列中的每个对象执行它们各自的终止操作,并且将已经执行过终止操作的对象从终止队列中删除。只有那些已经执行过终止操作的对象才会在下一次垃圾回收过程中被从内存中删除。这样做的一个后果是,等待被终止的对象有可能在它被清除之前,被移入更高一级的generation中,从而增加它被清除的延迟时间。
  需要执行终止操作的对象应当实现IDisposable接口,以便客户程序通过此接口快速执行终止动作。IDisposable接口包含一个方法——Dispose。这个被Beta2引入的接口,采用一种在Beta2之前就已经被广泛使用的模式实现。从本质上讲,一个需要终止操作的对象暴露出Dispose方法。这个方法被用来释放外部资源并抑制终止操作,就象下面这个程序片断所演示的那样:
 &span style="font-family:Arial B"& public class OverdueBookLocator: IDisposable
   ~OverdueBookLocator()
   InternalDispose(false);
   public void Dispose()
   InternalDispose(true);
   protected void InternalDispose(bool disposing)
   if(disposing)
   GC.SuppressFinalize(this);
   // Dispose of managed objects if disposing.
   // free external resources here
  } &/span&
这些都是.NET中CLR的概念,和C#没多大关系。
使用基于CLR的语言编译器开发的代码称为托管代码。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
认真看MSDN Library,就会搞清楚这些概念。
以下代码说明的很形象:
&span style="font-family:Arial B"&&span style="font-family:Arial B"&//引用类型('class' 类类型)
class SomeRef { public int32}
//值类型('struct')
struct SomeVal(pulic Int32}
static void ValueTypeDemo()
SomeRef r1=new SomeRef();//分配在托管堆
SomeVal v1=new SomeVal();//堆栈上
r1.x=5;//解析指针
v1.x=5;//在堆栈上修改
SomeRef r2=r1;//仅拷贝引用(指针)
SomeVal v2=v1;//先在堆栈上分配,然后拷贝成员
r1.x=8;//改变了r1,r2的值
v1.x=9;//改变了v1,没有改变v2
} &/span&&/span&垃圾回收机制 :
首先声明一点所谓垃圾回收,回收的是分配在托管堆上的内存,对于托管堆外的内存,它无能为力。
讨论垃圾回收机制就不得不提内存的分配,在C运行时堆(C-runtime heap)中,堆是不连续的,我们new一个新的对象时,系统会检查内存,找一块足够大的内存然后初始化对象,对象被销毁后,这块空间会用于初始化新的对象。这样做有什么弊端?随着程序运行一直有对象生成释放,内存会变得碎片化,这样有新的大的对象要生成时就必须扩展堆的长度,碎片内存无法得到充分利用,还有一个弊端是每次创建一个对象时都要检查堆内存,效率不高。而C#托管堆采取连续内存存储,新创建对象时只要考虑剩下的堆内存是否足够大就成,但一直生成对象而不析构会使托管堆无限增大,怎么维护这样一块连续内存呢?这也就引出了垃圾回收机制。托管堆的大小是特定的,垃圾收集器GC负责当内存不够的时候释放掉垃圾对象,copy仍在使用的对象成一块连续内存。而这就带来了性能问题,当对象很大的时候,频繁的copy移动对象会降低性能,所以C#的垃圾收集引入了世代和大对象堆小对象堆的概念。
所谓大对象堆小对象堆从字面意义就能看出其作用,大对象堆主要负责分配大的对象,小对象堆分配小的。但对象大小怎么确定呢?在.NET Framework中规定,如果对象大于或等于 85,000 字节,将被视为大型对象。当对象分配请求传入后,如果符合该大小阈值,便会将此对象分配给大型对象堆。这个85000字节是根据性能优化的结果确定。值得注意的是垃圾回收对大对象堆和小对象堆都起作用。那么分大对象和小对象堆作用是什么呢?还是性能,对于大对象和小对象区别对待采取不同灵活的垃圾回收策略必定比一棍子打死死板的采用同一种策略要好。下面我们讨论一下在SOH(大型对象堆)和LOH(小型对象堆)不同的垃圾收集策略:
先说一下世代,之所以分世代,是因为在第0代就能清除大部分对象。请注意,世代是个逻辑上的概念,物理上并没有世代这个数据结构。以小对象堆垃圾回收为例:当一个对象被创建的时候,它被定义为第0代对象,而经历一次垃圾收集后还存余的对象就被归入了第1代对象,同理经过两次或者两次以上仍然存在的对象就可以看成第2代对象。虽然世代只是逻辑概念,但它却是有大小的,对于SOH对象来说,由于每次垃圾回收都会压缩移动对象,所以世代数越大越在堆底。经历一次垃圾回收,对象都会被移入下一个世代的内存空间中(下图以小对象堆上垃圾回收为例。对象W至少经过两次垃圾回收而不死,所以放入世代2,X经历了一次垃圾回收)。而每次一个世代内存达到其阙值,都会引发垃圾收集器回收一次。这么做的好处就是每次垃圾回收器只是回收一个世代的内存,从整体上来看,减少了对象复制移动的次数。
以上讨论都是针对于SOH,对于SOH对象来说复制移动较之LOH成本性能要小一点,那么对于LOH呢?复制一个LOH的对象并且清除原来内存位置的字节,成本相当大,怎么确保它的垃圾回收呢?首先从物理上来说,LOH在托管堆的堆底,SOH在其上,逻辑上讲LOH对象都分配在第二世代,也就是说前边第0代和第1代的垃圾收集回收的都是第OH对象,这也就解释了为什么前两个世代垃圾回收中允许复制移动对象。但对于第二世代垃圾回收呢?第二代垃圾回收之后的对象仍是第二世代,其回收时并不移动仍在使用的对象,压缩空间,而只是清除垃圾对象。当一个新LOH对象创建时,它会从堆底遍历寻找LOH中能够满足要求的内存,如果没有接着向堆顶创建(这个过程和C运行时工作原理一样,所以也存在相同的弊端,LOH堆内存有可能存在碎片)。此时如果堆顶已经超出阙值,引发垃圾回收器回收内存空间。
从上边讨论中我们可以总结一下:我们new出一对象时它要么小对象会被放入第0代,大对象会被分在LOH中,只有垃圾回收器才能够在第1代和第2代中“分配”对象,这里所说分配对象是指移动复制对象。
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!浅析JAVA的垃圾回收机制(GC) - 简书
浅析JAVA的垃圾回收机制(GC)
字数 12755
1.什么是垃圾回收?
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,可以理解为一种文字游戏。分析:
引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(引用都有哪些?对垃圾回收又有什么影响?)
垃圾:无任何对象引用的对象(怎么通过算法找到这些对象呢?)。
回收:清理“垃圾”占用的内存空间而非对象本身(怎么通过算法实现回收呢?)。
发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中(堆内存为了配合垃圾回收有什么不同区域划分,各区域有什么不同?)。
发生时间:程序空闲时间不定时回收(回收的执行机制是什么?是否可以通过显示调用函数的方式来确定的进行回收过程?)
带着这些问题我们开始进一步的分析。2.Java中的对象引用
(1)强引用(Strong Reference):如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
(2)软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。
(3)弱引用(Weak Reference):它也是用来描述非须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
(4)虚引用(Phantom Reference):最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。
区分Java对象和对象引用请参考。3.判断对象是否是垃圾的算法。
Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)找到所有存活对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。3.1引用计数算法(Reference Counting Collector)
堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利(OC的内存管理使用该算法)。
缺点: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。
早期的JVM使用引用计数,现在大多数JVM采用对象引用遍历(根搜索算法)。3.2根搜索算法(Tracing Collector)首先了解一个概念:根集(Root Set)
所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
这种算法的基本思路: (1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。 (2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。 (3)重复(2)。 (4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
Java和C#中都是采用根搜索算法来判定对象是否存活的。标记可达对象:
JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图。下图3.0中所展示的JVM中的内存布局可以用来很好地阐释这一概念:
图 3.0 标记(marking)对象
首先,垃圾回收器将某些特殊的对象定义为GC根对象。所谓的GC根对象包括:(1)虚拟机栈中引用的对象(栈帧中的本地变量表);(2)方法区中的常量引用的对象;(3)方法区中的类静态属性引用的对象;(4)本地方法栈中JNI(Native方法)的引用对象。(5)活跃线程。
接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。
存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。关于标记阶段有几个关键点是值得注意的:
(1)开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便JVM可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。
(2)暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
(3)在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:
1.如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
2.如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
(4)实际上GC判断对象是否可达看的是强引用。
当标记阶段完成后,GC开始进入下一阶段,删除不可达对象。4.回收垃圾对象内存的算法4.1 Tracing算法(Tracing Collector) 或 标记—清除算法
标记—清除算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。
优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:(1)标记和清除过程的效率都不高。(这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量。如图4.1所示。)。(2)标记清除后会产生大量不连续的内存碎片。虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败(在Java中就是一次OutOfMemoryError)不得不触发另一次垃圾收集动作。如图4.2所示。算法示意图:
标记—清除算法
图4.1 标记—清除算法
4.2 Compacting算法(Compacting Collector) 或 标记—整理算法
该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
优点:(1)经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。(2)使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。算法示意图:
图4.2 标记—整理算法
图4.3 标记—整理算法
4.3 Copying算法(Copying Collector)
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。一种典型的基于Coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。
优点:(1)标记阶段和复制阶段可以同时进行。(2)每次只对一块内存进行回收,运行高效。(3)只需移动栈顶指针,按顺序分配内存即可,实现简单。(4)内存回收时不用考虑内存碎片的出现(得活动对象所占的内存空间之间没有空闲间隔)。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。算法示意图:
图4.4 Copying算法
图4.4 Copying算法
Adaptive算法(Adaptive Collector)
在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。5
Java的堆内存(Java Heap Memory)
Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法(4.1-4.3)进行垃圾回收(GC),以便提高回收效率。堆内存分区示意图:
图5.0 Java Heap Memory
Java Heap Memory
Java的内存空间除了堆内存还有其他部分:1)栈
每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。2)本地方法栈
用于支持native方法的执行,存储了每个native方法调用的状态。4)方法区
存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(PermanetGeneration)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。详细可以参考:。5.1堆内存分配区域:1.年轻代(Young Generation)
几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。大部分对象在Eden区中生成。当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。2.年老代(Old Generation)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。比如:
byte[] data = new byte[4*]
这种一般会直接在老年代分配存储空间。
当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。3.持久代(Permanent Generation)
用于存放静态文件(class类、方法)和常量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
永久代空间在Java SE8特性中已经被移除。取而代之的是元空间(MetaSpace)。因此不会再出现“java.lang.OutOfMemoryError: PermGen error”错误。5.2 堆内存分配策略明确以下三点:(1)对象优先在Eden分配。(2)大对象直接进入老年代。(3)长期存活的对象将进入老年代。5.3 对垃圾回收机制说明以下三点:
新生代GC(Minor GC/Scavenge GC):发生在新生代的垃圾收集动作。因为Java对象大多都具有朝生夕灭的特性,因此Minor GC非常频繁(不一定等Eden区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
老年代GC(Major GC/Full GC):发生在老年代的垃圾回收动作。Major GC,经常会伴随至少一次Minor GC。由于老年代中的对象生命周期比较长,因此Major GC并不频繁,一般都是等待老年代满了后才进行Full GC,而且其速度一般会比Minor GC慢10倍以上。另外,如果分配了Direct Memory,在老年代中进行Full GC时,会顺便清理掉Direct Memory中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
用Java VisualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装载,当老年代也满了后,就会报outofmemory的异常,如下图所示:
图5.2 垃圾回收分析
如何使用Java VisualVM 进行垃圾回收的监视和分析请参考:。6
垃圾回收器(GC)6.1 按执行机制划分Java有四种类型的垃圾回收器:(1)串行垃圾回收器(Serial Garbage Collector)(2)并行垃圾回收器(Parallel Garbage Collector)(3)并发标记扫描垃圾回收器(CMS Garbage Collector)(4)G1垃圾回收器(G1 Garbage Collector)
每种类型都有自己的优势与劣势,在很大程度上有 所不同并且可以为我们提供完全不同的应用程序性能。重要的是,我们编程的时候可以通过向JVM传递参数选择垃圾回收器类型。每种类型理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序(单CPU、新生代空间较小及对暂停时间要求不是非常高的应用)。是client级别默认的GC方式。通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,当执行垃圾回收的时候它也会冻结所有的应用程序线程。
适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式。可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。3、并发标记扫描垃圾回收器
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。(1)当标记的引用对象在Tenured区域;(2)在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。以上各种GC机制是需要组合使用的,指定方式由下表所示:
不同垃圾回收器的组合方式
6.2 垃圾回收的JVM配置运行的垃圾回收器类型:
GC的优化配置:
GC优化配置
使用JVM GC 参数的例子:java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar6.3 HotSpot(JDK 7)虚拟机提供的几种垃圾收集器
垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。用户可以根据自己的需求组合出各个年代使用的收集器。1.Serial(SerialMSC)(Copying算法)
Serial收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法。2.Serial Old (标记—整理算法)
Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。2.ParNew (Copying算法)
ParNew收集器是新生代收集器,Serial收集器的多线程版本。使用多个线程进行垃圾收集,在多核CPU环境下有着比Serial更好的表现。3.Parallel Scavenge (Copying算法)
Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。追求高吞吐量,高效利用CPU。吞吐量一般为99%。 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。4.Parallel Old(ParallelMSC)(标记—整理算法)
Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。吞吐量优先。5.CMS
(标记—整理算法)
CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。高并发、低停顿,追求最短GC回收停顿时间,CPU占用比较高。响应时间快,停顿时间短,多核CPU 追求高响应时间的选择。6.G1
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域。通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器。Java 8 的新特性:
在使用G1垃圾回收器的时候,通过 JVM参数 -XX:+UseStringDeduplication 。 我们可以通过删除重复的字符串,只保留一个char[]来优化堆内存。这个选择在Java 8 u 20被引入。
我们给出了全部的几种Java垃圾回收器,需要根据应用场景,硬件性能和吞吐量需求来决定使用哪一种。
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge。
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS。
表 6.1 HotSpot 1.6 JVM 垃圾回收器
垃圾回收执行时间和注意事项
GC分为Scavenge GC和Full GC。
Scavenge GC :发生在Eden区的垃圾回收。
Full GC :对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
有如下原因可能导致Full GC:
1.年老代(Tenured)被写满;
2.持久代(Perm)被写满;
3.System.gc()被显示调用;
4.上一次GC之后Heap的各域分配策略动态变化.7.1
与垃圾回收时间有关的两个函数 1.
System.gc()方法
命令行参数监视垃圾收集器的运行:
使用System.gc()可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc可以查看Java使用的堆内存的情况,它的格式如下:
java -verbosegc classfile
需要注意的是,调用System.gc()也仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。2.
finalize()方法
概述:在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源。但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象以释放资源,这个方法就是finalize()。它的原型为:protected void finalize() throws Throwable在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。
意义:之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个finalize()方法。
特殊的区域例如:1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。
换言之,finalize()的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。因为在Java中并没有提够像“析构”函数或者类似概念的函数,要做一些类似清理工作的时候,必须自己动手创建一个执行清理工作的普通方法,也就是override Object这个类中的finalize()方法。比如:销毁通知。
一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。
JAVA里的对象并非总会被垃圾回收器回收。1 对象可能不被垃圾回收,2 垃圾回收并不等于“析构”,3 垃圾回收只与内存有关。也就是说,并不是如果一个对象不再被使用,是不是要在finalize()中释放这个对象中含有的其它对象呢?不是的。因为无论对象是如何创建的,垃圾回收器都会负责释放那些对象占有的内存。
当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。7.2
触发主GC的条件
1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
3)在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。
由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。7.3
减少GC开销的措施
根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。具体措施包括以下几个方面:(1)不要显式调用System.gc()
此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。(2)尽量减少临时对象的使用
临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。(3)对象不用时最好显式置为Null
一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。(4)尽量使用StringBuffer,而不用String来累加字符串
由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。(5)能用基本类型如Int,Long,就不用Integer,Long对象
基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。(6)尽量少用静态对象变量
静态变量属于全局变量,不会被GC回收,它们会一直占用内存。(7)分散对象创建或删除的时间
集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。7.4
关于垃圾回收的几点补充经过上述的说明,可以发现垃圾回收有以下的几个特点:
(1)垃圾收集发生的不可预知性:由于实现了不同的垃圾回收算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。
(2)垃圾收集的精确性:主要包括2 个方面:(a)垃圾收集器能够精确标记活着的对象;(b)垃圾收集器能够精确地定位对象之间的引用关系。前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。而后者则是实现归并和复制等算法的必要条件。所有不可达对象都能够可靠地得到回收,所有对象都能够重新分配,允许对象的复制和对象内存的缩并,这样就有效地防止内存的支离破碎。
(3)现在有许多种不同的垃圾收集器,每种有其算法且其表现各异,既有当垃圾收集开始时就停止应用程序的运行,又有当垃圾收集开始时也允许应用程序的线程运行,还有在同一时间垃圾收集多线程运行。
(4)垃圾收集的实现和具体的JVM 以及JVM的内存模型有非常紧密的关系。不同的JVM 可能采用不同的垃圾收集,而JVM 的内存模型决定着该JVM可以采用哪些类型垃圾收集。现在,HotSpot 系列JVM中的内存系统都采用先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的垃圾收集。
(5)随着技术的发展,现代垃圾收集技术提供许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。针对以上特点,我们在使用的时候要注意:
(1)不要试图去假定垃圾收集发生的时间,这一切都是未知的。比如,方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放。
(2)Java中提供了一些和垃圾收集打交道的类,而且提供了一种强行执行垃圾收集的方法--调用System.gc(),但这同样是个不确定的方法。Java 中并不保证每次调用该方法就一定能够启动垃圾收集,它只不过会向JVM发出这样一个申请,到底是否真正执行垃圾收集,一切都是个未知数。
(3)挑选适合自己的垃圾收集器。一般来说,如果系统没有特殊和苛刻的性能要求,可以采用JVM的缺省选项。否则可以考虑使用有针对性的垃圾收集器,比如增量收集器就比较适合实时性要求较高的系统之中。系统具有较高的配置,有比较多的闲置资源,可以考虑使用并行标记/清除收集器。
(4)关键的也是难把握的问题是内存泄漏。良好的编程习惯和严谨的编程态度永远是最重要的,不要让自己的一个小错误导致内存出现大漏洞。(5)尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null,暗示垃圾收集器来收集该对象,还必须注意该引用的对象是否被监听,如果有,则要去掉监听器,然后再赋空值。8
Java内存泄露
(1)静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。Static Vector v = new Vector();for (int i = 1; i&100; i++){Object o = new Object();v.add(o);o =}
在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
(2)各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
(3)监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。8.2
GC性能调优
Java虚拟机的内存管理与垃圾收集是虚拟机结构体系中最重要的组成部分,对程序(尤其服务器端)的性能和稳定性有着非常重要的影响。性能调优需要具体情况具体分析,而且实际分析时可能需要考虑的方面很多,这里仅就一些简单常用的情况作简要介绍。
我们可以通过给Java虚拟机分配超大堆(前提是物理机的内存足够大)来提升服务器的响应速度,但分配超大堆的前提是有把握把应用程序的Full GC频率控制得足够低,因为一次Full GC的时间造成比较长时间的停顿。控制Full GC频率的关键是保证应用中绝大多数对象的生存周期不应太长,尤其不能产生批量的、生命周期长的大对象,这样才能保证老年代的稳定。
Direct Memory在堆内存外分配,而且二者均受限于物理机内存,且成负相关关系。因此分配超大堆时,如果用到了NIO机制分配使用了很多的Direct Memory,则有可能导致Direct Memory的OutOfMemoryError异常,这时可以通过-XX:MaxDirectMemorySize参数调整Direct Memory的大小。
除了Java堆和永久代以及直接内存外,还要注意下面这些区域也会占用较多的内存,这些内存的总和会受到操作系统进程最大内存的限制:1、线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError(横向无法分配,即无法建立新的线程)。
Socket缓冲区:每个Socket连接都有Receive和Send两个缓冲区,分别占用大约37KB和25KB的内存。如果无法分配,可能会抛出IOException:Too many open files异常。关于Socket缓冲区的详细介绍参见我的Java网络编程系列中深入剖析Socket的几篇文章。
JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中。
虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。9
代码分析垃圾回收过程public class SlotGc{
public static void main(String[] args){
byte[] holder = new byte[32*];
System.gc();
代码很简单,就是向内存中填充了32MB的数据,然后通过虚拟机进行垃圾收集。在Javac编译后,在终端执行如下指令:java -verbose:gc SlotGc来查看垃圾收集的结果,得到如下输出信息:[GC 208K-&134K(5056K), 0.0017306 secs][Full GC 134K-&134K(5056K), 0.0121194 secs][Full GC 32902K-&3K), 0.0094149 sec]
注意第三行,“-&”之前的数据表示垃圾回收前堆中存活对象所占用的内存大小,“-&”之后的数据表示垃圾回收堆中存活对象所占用的内存大小,括号中的数据表示堆内存的总容量,0.0094149 sec 表示垃圾回收所用的时间。
从结果中可以看出,System.gc(()运行后并没有回收掉这32MB的内存,这应该是意料之中的结果,因为变量holder还处在作用域内,虚拟机自然不会回收掉holder引用的对象所占用的内存。修改代码如下:public class SlotGc{
public static void main(String[] args){
byte[] holder = new byte[32*];
System.gc();
加入花括号后,holder的作用域被限制在了花括号之内,因此,在执行System.gc()时,holder引用已经不能再被访问,逻辑上来讲,这次应该会回收掉holder引用的对象所占的内存。但查看垃圾回收情况时,输出信息如下:[GC 208K-&134K(5056K), 0.0017100 secs][Full GC 134K-&134K(5056K), 0.0125887 secs][Full GC 32902K-&3K), 0.0089226 secs]
很明显,这32MB的数据并没有被回收。下面我们再做如下修改:public class SlotGc{
public static void main(String[] args){
byte[] holder = new byte[32*];
System.gc();
}}这次得到的垃圾回收信息如下:[GC 208K-&134K(5056K), 0.0017194 secs][Full GC 134K-&134K(5056K), 0.0124656 secs][Full GC 32902K-&134K(37828K), 0.0091637 secs]说明这次holder引用的对象所占的内存被回收了。
首先明确一点:holder能否被回收的根本原因是局部变量表中的Slot是否还存有关于holder数组对象的引用。
在第一次修改中,虽然在holder作用域之外进行回收,但是在此之后,没有对局部变量表的读写操作,holder所占用的Slot还没有被其他变量所复用。所以作为GC Roots一部分的局部变量表仍保持者对它的关联。这种关联没有被及时打断,因此GC收集器不会将holder引用的对象内存回收掉。 在第二次修改中,在GC收集器工作前,手动将holder设置为null值,就把holder所占用的局部变量表中的Slot清空了,因此,这次GC收集器工作时将holder之前引用的对象内存回收掉了。
当然,我们也可以用其他方法来将holder引用的对象内存回收掉,只要复用holder所占用的slot即可,比如在holder作用域之外执行一次读写操作。
为对象赋null值并不是控制变量回收的最好方法,以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。另外,赋null值的操作在经过虚拟机JIT编译器优化后会被消除掉,经过JIT编译后,System.gc()执行时就可以正确地回收掉内存,而无需赋null值。参考文章:;;;;;。
作者:一字马胡 转载标志 【】 更新日志 日期更新内容备注 新建文章初版 导入 作为Java语言的使用者,不像C++那样需要自己负责内存的申请和释放,因为Java语言有垃圾收集器(garbage collector GC),GC会负责...
Android梦想特工队作者:
Aaron主页:
http://www.wxtlife.com/原文连接: http://www.wxtlife.com//java-jvm-gc/ 如果想加入Android技术交流群,请长按识别二维码关注...
Java 基础思维导图,让 Java 不再难懂 - 工具资源 - 掘金思维导图的好处 最近看了一些文章的思维导图,发现思维导图真是个强大的工具。了解了思维导图的作用之后,觉得把它运用到java上应该是个不错的想法,这样回顾知识点的时候一目了然,快速知道自己的短板。 思维导图...
垃圾回收算法具体实现 翻译原文 =& plumbr Java GC handbook 前文参见: Java垃圾回收手册(一):初识垃圾回收Java垃圾回收手册(二):Java中的垃圾回收Java垃圾回收手册(三):垃圾回收算法基础 在熟悉GC算法背后的核心概念之后,我们来看...
1. 垃圾回收的意义在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是&无用信...
微信小程序适合多种行业,特别是对于建立在微信上卖货的微商来说,更加绝好的机会。关于微信小程序的推广方法,赢在移动小程序商总结的有一下几点。 小程序与好友/群 好友分享和群分享,这背后是对分享传播引导、活动运营、社群运营三个运营点的考验,这也是线上运营最值得去深耕的地方。小程...
青表姐一家又回娘家了。一家人包括青表姐,表姐夫,一儿一女。青表姐回家,大姨是很开心的。好吃好喝伺候了一家大小半个月以后,青表姐直言回来的目的了。儿子读的小学不好,希望大姨能够想办法把儿子转到一个好一点的学校。女儿也快到读书的年龄了,顺便也一起办了吧。反正都是同样的事,两次并...
70年代我出生在四川的一个小村庄,那时的天很蓝,水很清,汽车几乎没有,每天窄窄的土路上只有自行车叮叮铛铛响着清脆的铃声来来往往。 每年的夏天是娃娃们最喜欢的时节。一到暑假,村里孩子可欢了,男孩子们背着大人去小溪里洗澡,怕回家挨打受罚,每次洗澡后就躺在太阳底下把身上的水晒干才...
这就是真实。
孙翔x周泽楷 性转 国标舞paro 预警,文笔拙劣,复健时期,灵感来自动漫《舞动青春》。 对现实舞蹈了解还很少,若有bug希望能指出来。 小周是非职业赛场中的实力碾压型选手,职业赛场中在国内已经可以算优秀选手了,因为风格实力项目等等原因暂时没有舞伴。 翔翔是非职业选手中优秀...}

我要回帖

更多关于 可回收垃圾处理方式 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信