探究Java垃圾回收机制

java垃圾回收机制使得java程序员不用手动去释放通过new关键字在heap上申请的空间。但是,任何事情都是有利有弊。它也许并不像我们想象中那样强大!让我们来一探究竟。本blog链接:blog.csdn.com/whuslei。
   阅读本文前,请务必参看《think in java》第四版的第五章"初始化和清理"。其他网上的资料就没必要看了,没有太大价值。 
   有几个问题应该考虑下:
1、什么是垃圾?它是如何形成的?
2、java的垃圾收集器是什么?它是什么时候执行的?执行过程是怎样的?
3、sun为什么不推荐使用finalize()函数来释放一个对象?这个函数究竟做了些什么?
4、java中是否也存在内存泄漏?是否和C++中的一样?如果存在,应当如何避免?

   带着问题来思考java内部是怎么做的。

一、对象在什么情况下变为垃圾?
   java中称那些不可达的对象为垃圾。那什么叫不可达?其实就是没办法再引用到该对象了。主要有以下几种情况使对象变为垃圾:

1、对非线程对象来说,所有的活动线程都不能访问该对象,那么该对象就变为垃圾。
2、对线程对象来说,满足上面的条件,且线程未启动或已停止。

例如:
(1)改变对象的引用,如置为null或者指向其他对象。
   Object x=new Object();//object1
   Object y=new Object();//object2
   x=y;//object1 变为垃圾
   x=y=null;//object2 变为垃圾

(2)超出作用域
   if(i==0){
      Object x=new Object();//object1
   }//括号结束后object1将无法被引用,变为垃圾

(3)类嵌套导致未完全释放
   class A{
      A a;
   }
   A x= new A();//分配一个空间
   x.a= new A();//又分配了一个空间
   x=null;//将会产生两个垃圾

(4)线程中的垃圾
   class A implements Runnable{ 
     void run(){
       //....
     }
   }
   //main
   A x=new A();//object1
   x.start();
   x=null;//等线程执行完后object1才被认定为垃圾

   这样看,确实在代码执行过程中会产生很多垃圾,不过不用担心,java可以有效地处理他们。

二、垃圾收集机制
   首先,垃圾收集机制是由垃圾收集器Garbage Collection(以下简称gc)来实现的。而gc其实就是一个后台守护进程(其实这种说法不是很确切,因为当可用内存不足时,gc会先暂停应用程序)。它的特别之处在于它是一个低优先级的进程,但是可以根据内存的使用情况动态地调整它的优先级。因此,通常在系统内存低到一定限度时才会自动运行,从而实现对内存的回收。这也就是我们通常说垃圾回收的时间是不确定的原因。
   为什么要这样设计呢?我们知道,gc也是进程,也要消耗cpu等资源,如果gc执行过于频繁会对java程序的执行产生较大影响(java解释器本来就不快)。因此JVM的设计者们选择了不定期的gc。

   在垃圾回收器回收内存前,还需要做一些清理工作。why?

   其实,gc只能回收通过new关键字申请的内存(在堆上),但是堆上的内存却不完全是通过new申请分配的!java中通过JNI可以访问本地方法,一般是调用C,而c是通过malloc和free来进行申请内存的。这部分"特殊"内存如果不手动释放,会引起内存泄漏!gc是无法回收这部分内存的!举例:System.gc()可以显示调用垃圾收集器。从java api中对这个函数的解释可以看出,这个方法只是一种尽力而为的回收,并不能保证将所有的垃圾都回收,为什么不能保证回收全部垃圾?原因如上。
   所以,java允许在类中定义一个名为finalize的方法(Object类中的一个方法)。它可以用来做一些清理工作。它的工作原理"假定"是这样的:一旦JVM的垃圾回收器准备好释放对象所占用的内存空间,将首先调用其finalize方法(每个类都有,他们都继承自Object类),并且在下一次垃圾回收动作时才会真正回收对象所占用的内存空间。

   对上面的原理,很容易产生疑惑,如下:
   1、gc是负责回收内存的,那finalize也是用来回收内存的,他们一样吗?
   答:通过创建对象产生的内存,gc可以回收(至于如何回收,后面会讲到)。但是有些特殊内存gc不能回收,因此可以在finalize中用本地方法(native method)如free操作等!

   2、一个对象的finalize方法有什么要注意的?
   答:一个对象的finalize方法只能被调用一次!它并没有减少垃圾回收器的工作量!执行finalize后,gc依然需要判断这块内存空间是否可以回收!

   3、为什么sun并不推荐用finalize方法来完成清理工作?
   答:finalize方法只在垃圾回收之前才被调用。但是我们知道,gc本来就是不确定的。因此,当一个对象变为垃圾后,我们并不知道gc何时运行,也就不知道finalize何时被调用了。有可能它永远都不被调用!(程序退出时,未回收的内存将直接交还给操作系统)。由此可知,依靠finalize方法来完成清理工作是不靠谱的。正确的方法是,在放弃这个对象前调用相应的方法来进行内存的释放!(显式地调用)。

   4、有人可能会问,显式调用System.gc()强制运行gc不也可以调用到finalize完成清理吗?
   答:我认为,不可取。因为每次想执行清理都需要调用gc()来强制启动垃圾处理器,这样垃圾回收率低,反而很影响程序效率!为何不将清理工作放到某个方法中,准备放弃该对象的内存空间之前就调用该方法!

   重点来了,如何回收内存?
   方法一:引用计数法。简单但速度很慢。缺陷是:不能处理循环引用的情况。用法如下:每个对象都含有一个引用计数器,当被连接到对象时计数器加1,当离开作用域或者被置为null时,引用计数器减1。垃圾回收时,释放计数器为0的内存空间。如果出现循环引用,则会出现"对象应该被回收,但计数器不为0"。例如: a引用了b, b又反过来引用了a, 这时a,b就在堆上形成了一个闭环,  如果除b外只有c引用了a, 且只有a引用了b, 则c死亡时a,b也应该死掉, 但此时b还在引用着a, 于是a死不掉, a死不掉则b也死不掉,这就是所谓的循环引用问题。
   方法二:停止-复制(stop and copy)。效率低,需要的空间大。用法如下:先暂停程序的运行,将所有存活的对象从一个堆复制到另一个堆。没有被复制的都是垃圾,被复制部分将是连续空间没有碎片。
   方法三:标记-清扫(mark and sweep)。速度较快,占用空间少,但是释放后空间不来连续。用法如下:先暂停程序的运行,遍历所有引用,每找到一个存活对象就给它一个标记,此过程中不进行回收。当标记结束后才开始清理。清理后,剩下的部分是不连续的。如果希望等到连续的空间则需要进行复制。

   JAVA虚拟机中是如何做的?
   java的做法很聪明,我们称之为"自适应"的垃圾回收器,或者是"自适应的、分代的、停止-复制、标记-清扫"式垃圾回收器。它会根据不同的环境和需要选择不同的处理方式。

三、java中的内存泄漏
   从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏;JVM 内存中 native memory 的内存泄漏。
  1、java heap的内存泄漏。
   由于java heap的空间是在jvm过程中动态变化的。如果Java对象越来越多,占据Java Heap的空间也越来越大,JVM会在运行时扩充Java Heap的容量。如果Java Heap容量扩充到上限,并且在GC后仍然没有足够空间分配新的Java对象,便会抛出out of memory异常,导致JVM进程崩溃。Java Heap 中 out of memory 异常的出现有两种原因——①程序过于庞大,致使过多Java对象的同时存在;②程序编写的错误导致Java Heap内存泄漏。多种原因可能导致Java Heap内存泄漏。JNI编程错误也可能导致Java Heap的内存泄漏。

   2、JVM 中 native memory 的内存泄漏
   从操作系统角度看,JVM 在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。JVM进程空间中,Java Heap以外的内存空间称为JVM的native memory。进程的很多资源都是存储在JVM的native memory中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM的静态数据、全局数据等等。也包括JNI程序中native code分配到的资源。在JVM运行中,多数进程资源从native memory中动态分配。当越来越多的资源在 native memory中分配,占据越来越多native memory空间并且达到native memory上限时,JVM会抛出异常,使JVM进程异常退出。而此时Java Heap往往还没有达到上限。多种原因可能导致JVM的native memory内存泄漏。例如JVM在运行中过多的线程被创建,并且在同时运行。JVM 为线程分配的资源就可能耗尽native memory的容量。

你可能感兴趣的:(java,jvm,编程,jni)