本文来自《Thinking in java 3th》
在虚拟机JVM准备清除一个对象所占用的内存之前,总是先调用finalize()方法,在下次虚拟机进行内存清理的时候,才真正的清除对象所占用的内存,而finalize()并不像c++里的析构函数,对于java的“所有事物都是对象”的思想,一般都可以通过虚拟机自行进行内存释放,而finilize()方法,往往用于不是通过非创建对象的方法形成的对象(比如通过c++里的“本地方法”,malloc(),往往需要在finalize里执行free进行释放),所以说finalize()不是进行普通的清除工作的合适场所。当你对一些对象不感兴趣的时候,这些对象所占用的内存往往可以被释放,finalize当然不合适,你需要添加其他删除对象的方法,但是finalize却有一个其他用处:检验一些对象的是否没完全处于被清除状态。例如:
class Book{
boolean CheckedOut=false;
Book(boolean checkedOut){
CheckedOut=checkedOut;
}
public void IsChecked(){
CheckedOut=false;
}
public void finalize(){
if(CheckedOut)
System.out.println("Error:check out");
}
}
public class TestGC{
public static void main(String[] args){
Book book=new Book(true);
book.IsChecked();
//be clean normally
new Book(true);
//book is not check out
System.gc();
}
}
Garbage Collection(gc)工作方式有很多:
一、引用计数(reference counting)。每个对象都含有一个引用计数器,当引用连接到对象时,计数加一,当引用离开作用域置空时,计数减一。当计数器为0时释放所占的空间。计数器的开销非常小,但需要在整个生命周期中连续开销。reference counting有一个缺陷,对象间存在循环引用时,对象应该被回收但是计数器却不为0,通常用引用计数来说明垃圾回收的机制,几乎没有JVM采用这种方式。
二、自适应方式。从堆栈跟静态区域开始,遍历所有的引用,就能找到所有活的引用,在追踪它所引用的对象,然后找到此对象包含的所有引用,反复进行,直到覆盖的网络全被访问为止。如何找到活的引用,根据不同的JVM有不同的方法:(stop and copy)方法,暂停程序的运行,然后从一个堆里遍历所有对象,活的复制到另一个新的堆,没复制到的即为垃圾。当对象被复制到另外一个堆的时候是紧凑的,所以就形成了新的分配。把对象搬到新地方,它的所有引用必须被修改,在堆跟静态区域的引用可直接修改,但是有些引用必须在遍历过程中才能找到加以修改。但是这种方法会降低执行效率,其一,对象在两个堆来回捣腾,维护需要的工作是平时的两倍。其二,在于复制过程。程序进入稳定状态的时候,一个堆产生的垃圾非常少,就相当与把一个堆里的对象完整的搬到另外一个堆,非常浪费。为避免这种情况,一些JVM会做出自检查,如果产生的垃圾很少,采取(mark and sweep)方法,这种方法效率非常慢,但是由于垃圾很少,所有执行速度还是很快的。(mark and sweep)的工作方式,从堆和静态区域开始,遍历所有引用,进而找出所有存活的对象,加以标记,当遍历完成时,所有未标记的对象为垃圾,被清除,以这种方式处理后的堆空间是相当不连续的,如果要获得连续的堆空间,进行(stop and sweep)处理。这就是JVM的自适应方式。也叫“自适应的,分代的,停止-复制,标记-清除”式垃圾回收。
JIT技术提高程序运行速度:
JIT(Just-in-time),这种技术可以把全部或部分翻译成本地机器码,这本来是JVM的工作,程序运行速度提升,当需要装载一个类时,编译器找到.class类,将该类的字节码存入内存,此时有两种方式。一种编译器编译所有的代码,这种做法有两种缺陷,这种加载动作分散在整个程序生命周期,累加需要很多时间,并且会增加可执行吗的长度(字节码比本地机器码短很多),会导致页面调度,降低了程序的速度。第二种是(lazy evaluation)惰性编译,即有需要的时候编译器才编译代码。这样,不会被执行的代码不会被JIT编译。而新版的Java Hotspot采取类似的方式,代码每次被执行的时候进行一些优化,所以执行次数越多越快。