Java GC机制和对象Finalize方法的一点总结
GC是垃圾收集的意思(Garbage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。
有向图垃圾回收机制
.NET的垃圾回收采用引用计数,java的垃圾回收机制采取的是有向图的方式来实现,具体的说,java程序中的每个线程对象就可以看作是一个有向图的起点,有向边从栈中的引用者指向堆中的引用对象。在这个有向图中,如果一个对象和根节点之间是可达的,那么这个对象就是有效的,反之,这个对象就是可以被回收的。采取这样一种机制的优点是可以有效的避免循环引用。
当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通过有向图机制确定哪些对象是"可达的",哪些对象是"不可达的".当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
GC在JVM中通常是由一个或一组线程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU.当GC进程运行时,应用程序停止运行。为了防止finalize函数抛出的异常影响到垃圾回收线程的运作,垃圾回收线程会在调用每一个finalize函数时进行try catch,如果捕获到异常,就直接丢弃,然后接着处理下一个失效对象的finalize函数。所以finalize函数内一般需要自己处理抛出的异常,防止发生未处理异常情况。
当GC运行时间较长时,用户能够感到 Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有很多应该回收的对象没有被回收,仍然占用大量内存。因此一种折中的方案就是每次GC处理一定比例的对象,分成多次进行,这就叫增量式GC。
GC的分代
1) 在Young Generation中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to),它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。
2) 在Old Generation中,主要存放应用程序中生命周期长的内存对象。
3) 在Young Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个SurvivorSpace,当Survivor Space空间满了后,剩下的live对象就被直接拷贝到OldGeneration中去。因此,每次GC后,Eden内存块会被清空。
4) 在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。
5) 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
GC只回收堆区的内存,即处理java new出来的对象,但无法关闭其他资源,也无法处理java调用C或其他语言分配出的内存。
如果调用对象的finalize函数,对象处于不可达状态,并且GC准备回收该对象的内存,即finalize函数的调用发生在回收内存之前。
JVM不保证finalize函数一定会被调用。System.runFinalizersOnExit方法不安全,已经废弃,所有并不能保证程序退出时一定调用finalize函数。另外,规范还保证finalize函数最多运行一次。
System.gc并不保证GC执行,只是向JVM发送建议,并不是命令。
对象不可达,但是调用finalize之后又变得可达的情况存在,在finalize函数中通过this指针让其他句柄执行本身即可,但是再下次回收时不会再调用finalize,因为只能调用一次。
protected void finalize()
{
main.ref=this; // 恢复本对象,让本对象可达
}
垃圾回收器不能对用Java以外的代码编写的Class(比如JNI,C++的new方法分配的内存)进行正确的回收,这时就需要覆盖默认finalize的方法来实现对这部分内存的正确释放和回收(比如C++需要delete)。
finalize不能等同于C++对象的析构函数,C++析构函数在在对象生命周期结束时会确定执行,而finalize函数的调用具有很大的不确定性。
调用时间不确定——有资源浪费的风险
如果把某些稀缺资源放到finalize()中释放,可能会导致该稀缺资源等上很久很久以后才被释放。造成资源的浪费!另外,某些类对象所携带的资源(比如某些JDBC的类)可能本身就很耗费内存,这些资源的延迟释放会造成很大的性能问题。
可能不被调用——有资源泄漏的风险
在某些情况下,finalize()压根儿不被调用。比如在JVM退出的当口,内存中那些对象的finalize函数可能就不会被调用了。
因此一些清理工作如文件的关闭,连接的关闭等不要放到finalize函数中,要在程序中单独进行管理,一般finalize只做C/C++内存的回收。
即使有GC机制,Java还是存在内存泄露的问题
1.静态集合类形成的对象引用
复制代码
Static Vector v = new Vector();
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
复制代码
2.当集合里面的对象属性被修改后,再调用remove()方法时不起作用
3.各种连接,数据库连接,网络连接,IO连接等,显示调用close关闭后才能被GC回收
Java语言中的对象引用分为以下几种:强引用,软引用,弱引用,虚引用
强引用就是平时最常用的引用,当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
虚引用,这种引用不常用,主要用途是结果引用关联对象,实现对对象引用关系追踪。虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
几种引用分别位于java.lang.ref.SoftReference; java.lang.ref.WeakReference; 和 java.lang.ref.PhantomReference;
关于句柄和对象的一点记录
String s = new String("abc") ; 在编译阶段在文字池中创建“abc”对象,运行new时,将pool中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s持有,因此这条语句创建了2个String对象。
String s1 = new String("abc") ; String s2 = new String("abc") ; 共创建三个对象,pool中一个,heap中2个。
句柄可以不依赖于对象而存在,例如String s,创建了句柄s,在栈中保存,但并没有任何对象相关联。句柄可以考虑为遥控器,对象为电视,拥有了遥控器,就可以操控电视,但是没有电视,一样可以有遥控器。
由于字符串对象的大量使用(它是一个对象,一般而言对象总是在heap分配内存),Java中为了节省内存空间和运行时间,在编译阶段就把所有的字符串文字放到一个文字池(pool of literal strings)中,而运行时文字池成为常量池的一部分。文字池的好处,就是该池中所有相同的字符串常量被合并,只占用一个空间。所以上述的 "abc" 只占一份空间。
String s1 = "abc" ;
String s2 = "abc" ;
这里 s1 == s2 成立
String s1 = new String("abc") ;
String s2 = new String("abc") ;
这里 s1 == s2 不成立,s1.equals(s2)成立
垃圾回收算法
Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾收集算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就量正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。
1、 引用计数法(Reference Counting Collector)
引用计数法是唯一没有使用根集的垃圾回收的法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。
2、tracing算法(Tracing Collector)
tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.
3、compacting算法(Compacting Collector)
为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来 的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
4、copying算法(Coping Collector)
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
5、generation算法(Generational Collector)
stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。
6、adaptive算法(Adaptive Collector)
在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。