Thinking in Java 垃圾回收机制

终结条件

通常,不能指望finalize(),必须创建其他的“清理”方法,并且明确地调用它们。看来,finalize()只能存在于很难用到的一些晦涩用法里。
当对某个对象不再感兴趣——也就是它可以被清理的,这个对象应处于某种状态,使它占用的内存可以被安全的释放。例如,要是对象代表一个打开的文件,在对象回收前程序员应该关闭这个文件。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷。finalize()可以用来最终发现这种情况。

class Book{
    boolean checkedOut=false;
    Book(boolean checkOut){
        checkedOut=checkOut;
    }
    void checkIn(){
        checkedOut=false;
    }
    protected void finalize(){
        if(checkedOut){
            System.out.println("Error:checked out");
        }
    }

    
}
public class TerCondition {

    public static void main(String[] args) {
        Book novel=new Book(true);
        novel.checkIn();
        //程序员忘了关闭
        new Book(true);
        System.gc();
    }
}

结果:Error:checked out
本例子的终结条件是:所有的Book对象都被当作垃圾回收前都应该被签入(check in)。但在main()方法中,由于程序员的错误,有一本书未被签入,如果没有没有finalize()来验证终结条件,将很难发现这种缺陷。

垃圾回收机制如何工作

在以前所用过的程序语言中,在堆上分配对象的代价十分高昂,因此会自然的认为java中所有对象(基本类型除外)都在堆上分配的方式也非常高昂。然而,垃圾回收器对于提高对象的创建速度,却有明显的效果。听起来很奇怪——存储空间的释放竟然会影响到存储空间的分配,但这确实是某些Java虚拟机的工作方式。这也意味着,Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。

打个比方,你可以把C++里的堆想象成一个院子,里面每个对象都负责管理自己的地盘,一段时间后,对象可能被销毁,但地盘必须加以重用。在某些Java虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。Java的堆指针,只是简单的移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。

Java中的堆未必完全像传送带那样工作。要真是那样的话,势必会导致频繁的内存页面调度——将其移进移出硬盘,因此会显得需要拥有比实际需要更多的内存。页面调度会显著的影响性能,最终,在创建了足够多的对象之后,内存资源将耗尽。其中的密码在于垃圾回收器的介入。当它工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可分配的堆模型。

在一些更快的模式中,垃圾回收器并非基于引用计数技术。它们依据的思想是:对任何“活”的对象,一定能最终追溯到到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈或静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。你所访问过的对象必须是活的。

在这种方式下,Java虚拟机将采用自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种做法名为 停止—赋值。显然意味着,先暂停程序的运行(所有它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们时一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单直接地分配空间了。
这种所谓的“复制式回收器”,效率会降低,首先得有两个堆,然后得在这两个分离的堆之间来回倒腾,第二个问题在于复制。程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾,尽管如此,复制式回收器仍然会将所有内存自一处复制到另一处,为了避免这样的浪费,一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式(即:自适应)。这种模式称为标记—清扫,这种方式的速度慢,但是当你知道只会产生少量垃圾甚至不会产生垃圾时,它的速度就快了。

标记—清扫所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当找到一个存活对象,就会给对象设一个标记,这个过程不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的空间时不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。

停止—复制的意思时这种垃圾回收动作不是在后台进行的,相反,垃圾回收动作发生的同时,程序将会被暂停。
如前文所述,在这里讨论的Java虚拟机中,内存分配以较大的“快”为单位。如果对象较大,它会占用单独的块。严格来说,停止—复制要求在释放旧有对象之前,必须先把所以存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的代数来记录它是否还存活。通常,如果块在某处被引用,其代数会增加;垃圾回收器将上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到标记—清扫方式;同样,Java虚拟机会跟踪标记—清扫的效果,要是堆空间出现很多碎片,就会切换回停止—复制方式。这就是“自适应”技术,可以给它个啰嗦的称呼:“自适应的,分代的,停止—复制,标记—清扫”式的垃圾回收器。

Java虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关,被称为“即时”编译器的技术,这种技术可以把程序全部或部分翻译成本地机器码,程序运行速度因此得以提升。当需要装载某个类时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可供选择。一种就是让即使编译码编译所有代码。但这种做法有两种缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多的时间;并且会增加可执行代码的长度,这将导致页面调度,降低程序速度。另一种做法称为惰性评估,意思时即使编译器只在必要的时候才编译代码。

初始化顺序

在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。

class Window{
    Window(int marker){
        System.out.println("window( "+marker +" )");
    }
}
class House{
    Window w1=new Window(1);//构造器之前
    House(){
        //展示我们在构造器中
        System.out.println("House()");
        w3=new Window(33);
    }
    Window w2=new Window(2);
    void f(){
        System.out.println("f()");
    }
    Window w3=new Window(3);
}
/**
 * Orderinitialization
 */
public class Orderinitialization {

    public static void main(String[] args) {
        House h=new House();
        h.f();
    }
}

在House类中,故意把几个Window对象的定义散布到各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3在构造器内再次被初始化
由输出可见,w3这个引用会被初始化两次:一次再调用构造器前,一次再调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值