Java的垃圾回收机制使得java程序员不用手动去释放“通过new 关键字在heap上申请的空间”,但是任何事情都是有利有弊的。
一、对象在什么情况下会变成垃圾?
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(Java Native Interface)可以访问本地方法,一般是调用C,而C是通过malloc(分配存储空间)和free(释放存储空间,存储空间得不到释放,就会造成内存泄露)进行申请内存的。这部分“特殊的内存”如果不手动释放,就会导致内存泄露,gc是无法回收这部分内存的。例如:System.gc()可以显式调用垃圾收集器,从java API中对这个函数解释可以看出,这种方法是一种尽力而为的回收,并不能保证所有的垃圾都回收,上面所解释的就是。
所以java类中定义一个名为finalize的方法。它可以用来做一些清理工作。它的工作原理“假定”是这样的:一旦JVM的垃圾回收期准备好释放对象所暂用的内存空间,将首先调用其finalize方法(每个类都有,都继承object类),并且在下一次垃圾回收动作时才会真正回收对象所占用的内存空间。
对上面的原理,很容易产生疑惑,如下:
1、gc是负责回收内存的,那finalize也是用来回收内存的,他们一样吗?
答:通过创建对象产生的内存,gc可以回收(至于如何回收,后面会讲到)。但是有些特殊内存gc不能回收,因此可以在finalize中用本地方法(native method)如free操作等!
2、一个对象的finalize方法有什么要注意的?
答:一个对象的finalize方法只能被调用一次!它并没有减少垃圾回收器的工作量!执行finalize后,gc依然需要判断这块内存空间是否可以回收!
3、为什么sun不推荐用finalize方法来完成清理工作呢?
答:finalize方法只是在垃圾回收之前才会被调用。但是我们知道gc本来就是不确定的,因此当一个对象变为垃圾之后,我们并不知道垃圾回收器在何时运行,也就不知道finalize方法何时被调用了。有可能永远都不被调用。(程序退出的时候,未回收的内存将直接交给操作系统)。由此可知,依靠finalize来完成清理工作是不靠谱的。正确的方法是在放弃这个对象之前调用相应的方法来进行内存的释放!(显式地调用)。
4、有人可能会问,显式调用System.gc()强制运行gc不也可以调用到finalize完成清理吗?
答:我认为,不可取。因为每次想执行清理都需要调用gc()来强制启动垃圾处理器,这样垃圾回收率低,反而很影响程序效率!为何不将清理工作放到某个方法中,准备放弃该对象的内存空间之前就调用该方法!
如何回收内存?下面介绍几种垃圾回收技术:
5、方法一:引用计数法。简单但速度很慢。缺陷是:不能处理循环引用的情况。用法如下:每个对象都含有一个引用计数器,当连接到对象的时候计数器就加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中的内存泄漏
由于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的容量。