清理:终结处理和垃圾回收

GC的原理

通常我们在使用一个对象后就会对其“弃之不顾”,但是这样并非是安全的。当然,java有GC负责回收无用对象占据的内存资源。但也有特殊情况:假如你的对象活的了一块“特殊”的内存区域,由于GC只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了解决这样的情况,java允许在类中定义一个名为finalize()的方法。

finalize的原理

它的工作原理“假定”是这样的:一旦GC准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。但是可能程序中不会发生“垃圾回收”的情况,如果是这样,finalize()方法是一直不会被调用的。

finalize的用途

到这,你们可能会明白不能将finalize()作为通用的清理方法。那么其真正的用途是什么呢?
首先我们要记住:
1.对象可能不被垃圾回收
2.垃圾回收并不等于“析构”(C++术语)
3.垃圾回收只与内存有关
也就是说,使用垃圾回收器的唯一原因是为了回收程序不在使用的内存。所以对于与垃圾回收有关的任何行为来说,它们也必须同内存以及回收有关。
但这是否意味着要是对象中含有其他对象,finalize()就应该明确释放那些对象呢?
并不是,无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对finalize()的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间,也就是分配空间的方式不是我们通常使用的new。不过在java中一切皆对象,那么如何会出现这种情况呢?
这种情况一般出现在:在java中调用非java代码,比如调用C的malloc()函数来分配空间,而且除非调用free()函数否则存储空间将得不到释放,从而造成内存泄漏。因为free()是C和C++中的函数,所以需要在finalize()中调用它。

finalize总结

这不是一个在java中开发会经常使用的方法。通常情况,我们可以把该方法看作为多于方法,毕竟终结函数是无法预料的。

在java中如何实时清理

通常要清理一个对象,我们只需要在清理的时刻调用清理的方法即可。因为java不允许局部对象的存在,则必须使用new来创建对象。但是我们要存在忧患意识,无论“垃圾回收”还是“终结”都不一定会发生。如果JVM并没有面临内存耗尽的情况,它是不会浪费时间去执行垃圾回收以恢复内存的。

终结条件

通常,不能指望finalize(),必须创建其他的“清理”方法,并且明确的调用它们。不过finalize()用一个有趣的用法,它并不依赖于每次都要对finalize()进行调用。
public class Book {
    boolean checkedOut = false;
    public Book(boolean checkOut){
        checkedOut = checkOut;
    }
    void checkIn(){
        checkedOut = false;
    }
    protected void finalize(){
        if(checkedOut){
            System.out.println("Error: checked out");
        }
    }
}
public class TerminationCondition{
    public static void main(String[] args) {
        Book novel = new Book(true);
        novel.checkIn();
        new Book(true);
        System.gc();
    }
}

上述例子的终结条件是:所有Book对象在被当作垃圾回收前都应该被签入check in。但是在main方法中,由于错误,有一本书未被签入。要是没有finalize来验证终结条件,将很难发现这种缺陷。
gc是用于强制进行终结动作,该例子中如果不这样做,通过重复的执行程序,最终也会调用finalize()方法。

GC是如何工作的

栈:保存局部变量的值,包括:1.用来保存基本数据类型的值;2.保存类的实例,即堆区对象的引用。也可以用来保存加载方法时的帧。

堆:用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。

常量池:JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中。

代码段:用来存放从硬盘上读取的源程序代码。

数据段:用来存放static定义的静态成员。

可以看出来在java中,对象是存在于堆中,而栈是用来保存局部变量的值和基本数据类型的值和堆的引用。在其他语言中,在堆上分配对象的代价很高,而java不会,这是因为垃圾回收器对对象的创建有明显的影响。
在其他语言中,堆就像一块地,每个对象管理自己的区域,如果一段时间以后,某个区域的对象被销毁,那么该区域会被重用。在某些jvm中,堆的实现更像传送带,每分配一个对象,就往前移动一格,这就意味着对象的分配速度非常快。而在创建对象的过程中,GC也会介入。当它工作时,将一面回收空间,一面使堆中的对象紧凑排列。

GC的几种机制

1:引用计数器
引用计数器就是指,当有引用连接对象时,计数器加1,让引用离开作用域或被设置为null时,计数器减1。当发现某个对象的引用计数为0时,就释放其空间。该计数器机制有几个弊端,如果有兴趣可以留言,我会单独回复。
2:活对象追溯(我自己的叫法)
首先,我们能肯定,一个活的对象,一定能找到其引用。该引用可能会穿过数个对象层次。由此,如果从堆栈和静态存储区去开始遍历所有的引用,就能找到所有活的对象。对于发现每个引用,必须追溯到它所引用的对象,然后再根据追溯到的对象去找所有的引用,如此反复直到堆栈和静态存储区的引用所行程的网络全部被访问。
3:根据2这种方式,将会衍生两种处理机制
    a:停止-复制
    也就是说要先暂停程序的运行,然后将所有活的对象从当前堆中复制到另一个堆,没有被复制的都是垃圾。被复制过来的是一个挨着一个的,我们就可以直接快速的分配空间地址了。
    把对象从一处搬到另外一处,所有指向它的引用都必须修正。这种方式也有两个弊端,欢迎留言。
    b:标记-清扫
    就是在对活对象追溯时,其中活对象会有标记,期间不会做任何回收,在标记结束后统一清扫。清扫有一个弊端是清扫完成后,对象并不连续,想要连续的话,必须要重新整理剩下的对象。
上述的对象追溯情况“停止-复制”,“标记-清扫”其实会并肩运行,并且还会不同策略来降低他们的消耗。具体的情况可以私信我或者谷歌了解。

下一章会将到成员的初始化,指定初始化,构造器初始化,初始化顺序和静态数据的初始化。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值