最近在读Thinking In Java,秉着有些地方还能知道自己不会的精神,都去好好查阅了一些资料,在内存分配这一章,看到finalize()这个方法,刚开始很不理解,查阅了一些资料,顺带看了一下final、finally,现在分享一下。
一、final的介绍
final可用在4个地方,分别是变量(static 或者 !static),形式参数,方法和类,每种情况都有不同的含义,下面分别介绍之:
final修饰变量: 对基本类型表示变量被赋值后是一个常量,即不可改变的;对引用类型,表示引用指向的地址是不可改变的,但地址对应的内容是可变的,即final只保证引用不可变,而不是对象本身。
final修饰形式参数:作用同上。
以上这两种情况其实说明final修饰的变量只可以被赋值一次,但不是jvm对变量赋予的默认值(基本类型对应基本默认值,引用对应null)。
下面来看对final修饰变量的初始化,先看一个示例:
public classFinalTest {//在定义时初始化
public final int A = 10;//在初始化块中初始化
public final intB;
{
B= 20;
}//非静态final变量不能在静态初始化块中初始化//!public final int C;//!static {//!C = 30;//!}//静态常量,在定义时初始化
public static final int STATIC_D = 40;//静态常量,在静态初始化块中初始化
public static final intSTATIC_E;static{
STATIC_E= 50;
}//静态变量不能在初始化块中初始化//public static final int STATIC_F;//{//STATIC_F = 60;//}
public final intG;//静态final变量不可以在构造器中初始化//public static final int STATIC_H;//在构造器中初始化
publicFinalTest() {
G= 70;//静态final变量不可以在构造器中初始化//STATIC_H = 80;//给final的变量第二次赋值时,编译会报错//A = 99;//STATIC_D = 99;
}//final变量未被初始化,编译时就会报错//public final int I;//静态final变量未被初始化,编译时就会报错//public static final int STATIC_J;
}
由以上代码可以看出 被final修饰的变量必须被初始化。初始化的方式有以下几种:
在定义的时候初始化。
final变量可以在初始化块中初始化,不可以在静态初始化块中初始化。
静态final变量可以在静态初始化块中初始化,不可以在初始化块中初始化。
final变量还可以在类的构造器中初始化,但是静态final变量不可以。
final修饰方法:final修饰的方法表示它不可以被子类重写(override),但子类会继承之,private的方法也不会被重写
final修饰类:final修饰类表示该类不可以被继承,这整好与abstract类相反,abstract表示必须被继承,所以final与abstract不能同时修饰一个类,而接口与抽象类相似,所以final也不能用来修饰接口
下面看两个实例:
实例1意在说明final方法是不可重写的,但是子类会继承父类的final方法。
Java代码public classParentClass {public final voidTestFinal() {
System.out.println("父类--这是一个final方法");
}
}public classSubClass extends ParentClass {/**
* 子类无法重写(override)父类的final方法,否则编译时会报错*/
//public void TestFinal() {//System.out.println("子类--重写final方法");//}
public static voidmain(String[] args) {
SubClass sc= newSubClass();
sc.TestFinal();
}
}
实例2意在说明final类的非final域是可变的。
public final classFinalTest {int i = 10;
finalint j = 50;public static voidmain(String[] args) {
FinalTest ft= newFinalTest();
ft.i= 99; //final类FinalTest的属性值 i是可以改变的,因为属性值i前面没有final修//
//!ft.j = 49;//报错....因为 j 属性是final 的不可以改变。
System.out.println(ft.i);
}
}
下边继续讨论final的效率问题,欲了解final的效率,就要了解前期绑定与后期绑定,后期绑定即多态的实现的基石。先看如下代码:
Parent p = newChildren();
p.walk();//其中Clildren类是继承自Parent的,怎样知道walk()调用的是父类walk与子类walk()呢,这引入了绑定的概念
以上代码在执行过程中首先向上转型,即Childred对象“窄化”转型为Parent对象,然后是绑定,下面先介绍绑定,绑定分为前期绑定与后期绑定,或者叫做静态绑定或者动态绑定,即将一个方法与该方法所属的具体类型关联起来
静态绑定:在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。例如:C。针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法有final,static,private和构造方法是前期绑定。
动态绑定:在运行时根据具体对象的类型进行绑定。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法
调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
动态绑定的过程:虚拟机提取对象的实际类型的方法表;虚拟机搜索方法签名;调用方法。
关于绑定相关的说明:在java中,几乎所有的方法都是后期绑定的,在运行时动态绑定方法属于子类还是基类。但是也有特殊,针对static方法和final方法由于不能被继承,因此在编译时就可以确定他们的值,他们是属于前期绑定
的。特别说明的一点是,private声明的方法和成员变量不能被子类继承,所有的private方法都被隐式的指定为final的(由此我们也可以知道:将方法声明为final类型的一是为了防止方法被覆盖,二是为了有效的关闭java中的动态绑
定)。java中的后期绑定是有JVM来实现的,我们不用去显式的声明它,而C++则不同,必须明确的声明某个方法具备后期绑定。
java当中的向上转型或者说多态是借助于动态绑定实现的,所以理解了动态绑定,也就搞定了向上转型和多态。
前面已经说了对于java当中的方法而言,除了final,static,private和构造方法是前期绑定外,其他的方法全部为动态绑定。而动态绑定的典型发生在父类和子类的转换声明之下:
比如:Parent p = new Children();
编译器检查对象的声明类型和方法名。假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C类中所有的名称为f的方法和从C类的超类继承过来的f方法
接下来编译器检查方法调用中提供的参数类型。如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析”
当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类
推,下面看一个示例:
public classFather {public voidmethod() {
System.out.println("父类方法,对象类型:" + this.getClass());
}
}public classSon extends Father {public static voidmain(String[] args) {
Father sample= new Son();//向上转型
sample.method();
}
}//声明的是父类的引用,但是执行的过程中调用的是子类的对象,程序首先寻找子类对象的method方法,但是没有找到,于是向上转型去父类寻找
public classSon extends Father {public voidmethod() {
System.out.println("子类方法,对象类型:" + this.getClass());
}public static voidmain(String[] args) {
Father sample= new Son();//向上转型
sample.method();
}
}//由于子类重写了父类的method方法,根据上面的理论知道会去调用子类的method方法去执行,因为子类对象有method方法而没有向上转型去寻找//前面的理论当中已经提到了java的绑定规则,由此可知,在处理java类中的成员变量时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性还是父类的属性。
public classFather {protected String name="父亲属性";public voidmethod() {
System.out.println("父类方法,对象类型:" + this.getClass());
}
}public classSon extends Father {protected String name="儿子属性";public voidmethod() {
System.out.println("子类方法,对象类型:" + this.getClass());
}public static voidmain(String[] args) {
Father sample= new Son();//向上转型
System.out.println("调用的成员:"+sample.name);
}
}//结论,调用的成员为父亲的属性。//这个结果表明,子类的对象(由父类的引用handle)调用到的是父类的成员变量。所以必须明确,运行时(动态)绑定针对的范畴只是对象的方法。//现在试图调用子类的成员变量name,该怎么做?最简单的办法是将该成员变量封装成方法getter形式。
public classFather {protected String name = "父亲属性";publicString getName() {returnname;
}public voidmethod() {
System.out.println("父类方法,对象类型:" + this.getClass());
}
}public classSon extends Father {protected String name="儿子属性";publicString getName() {returnname;
}public voidmethod() {
System.out.println("子类方法,对象类型:" + this.getClass());
}public static voidmain(String[] args) {
Father sample= new Son();//向上转型
System.out.println("调用的成员:"+sample.getName());
}
}//结果:调用的是儿子的属性
ok!经过以上介绍,似乎已经对动态绑定有了一定的了解,not enough!下面介绍动态绑定的更底层机制,如果难以理解可以先跳过,可以直接跳过进入下一节!
JAVA虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法(static、private、构造方法、final)。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选
择所调用的方法,这就是动态绑定。
JAVA对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类数据(存储于方法区的类型信息),
因此在对象中通常会有一个指向方法区的指针。当程序在运行时需要转换某个对象引用为另外一种类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者它的超类型。当程序在执行instanceof
操作时,虚拟机也进行了同样的检查。所以虚拟机都需要查看被引用的对象的类数据。
不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表因为方法表加快了调用实例方法时的效率。但是JAVA虚拟机规范并未要求必须使用方法表,所以并不是所有实现中都会使用它。
下面是一种JAVA对象的内存表示:
方法数据存放在类的方法区中,包含一个方法的具体实现的字节码二进制。方法指针直接指向这个方法在内存中的起始位置,通过方法指针就可以找到这个方法。
动态绑定内部机制
方法表是一个指向方法区中的方法指针的数组。方法表中不包含static、private等静态绑定的方法,仅仅包含那些需要动态绑定的实例方法。
在方法表中,来自超类的方法出现在来自子类的方法之前,并且排列方法指针的顺序和方法在class文件中出现的顺序相同,这种排列顺序的例外情况是,被子类的方法覆盖的方法出现在超类中该方法第一次出现的地方。
例如有超类Base和子类Derive:
public classBase
{publicBase()
{
}public voidtest()
{
System.out.println( "int Base");
}public voidprint()
{
}
}public classDerive extends Base
{publicDerive()
{
}public voidtest()
{
System.out.println( "int Derive");
}public voidsayHello()
{
}public static voidmain( String[] args )
{
Basebase = newDerive();base.test();
}
}
上例中的Base和Derive的方法表如下:
在这个例子里,test()方法在Base和Derive的方法表中都是同一个位置-位置1。在Base方法表中,test()指针是Base的test()方法内存地址;而在Derive方法表中,方法表的位置1放置的是Derive的test()方法内存地址。
当JAVA虚拟机执行base.test()时,通过base引用可以找到base所指向的实际对象的内存位置,现在虚拟机不知道base引用的实际对象是Base还是Derive。但是根据上面的对象内存模型,虚拟机从对象内存中的第一个指针“特殊结构指针”开始,可以找到实际对象的类型数据和Class实例,这样虚拟机就可以知道base引用的实际对象是Derive对。为了执行test(),虚拟机需要找到test()的字节码,方法的字节码存放在方法区中。虚拟机从对象内存中的第一个指针“特殊结构指针”开始,搜寻方法表的位置1,位置1指向的test()方法是Derive类的test()方法,这就是JAVA虚拟机将要执行的test()的字节码。现在,虚拟机知道了调用的实际对象是Derive对象,调用的实际test()方法是Derive类的test()方法,所以JAVA虚拟机能够正确执行-调用base引用的实际对象的方法而不是base引用本身的方法。
这是动态绑定的一种实现方式,根据不同的JAVA虚拟机平台和不同的实际约束,动态绑定可以有不同的内部实现机制。这便是动态绑定的更深一层的机制。之所以这么大费周章的介绍动态绑定,是为了说明final的另一个作用,效率:首先final会关闭动态绑定,这样便不会由以上复杂的模型,方法即为类方法,调用起来会节省开销,编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。调用一个函数除了函数本身的执行时间之外,还需要时间去寻找这个函数(类内部有一个函数签名和函数地址的映射表)。故减少函数调用次数就等于降低了性能消耗。final修饰的函数会被编译器优化,优化的结果是减少了函数调用的次数。
public classTest3
{
finalvoidfunction()
{
System.out.println("xy");
}public static voidmain(String[] args)
{
Test3 t= newTest3();for(int i = 0;i< 1000;i++)
{
t.function();
}
}
}//经过编译器优化之后,这个类变成了相当于这样写:
public classTest3
{
finalvoidfunction()
{
System.out.println("xy");
}public static voidmain(String[] args)
{
Test3 t= newTest3();for(int i = 0;i< 1000;i++)
{
System.out.println("xy");
}
}
}
优点:编译器直接将function的函数体内嵌到了调用函数的地方,这样的结果是节省了1000次函数调用,当然编译器处理成字节码,只是我们可以想象成这样,看个明白。
缺点:当函数体若太长时用final会适得其反,因为经过编译器内嵌之后代码长度大大增加,于是就增加了jvm解释字节码的时间。而且如果final修饰的方法体过大的话,编译器可能会放弃内联。
用final修饰的变量(常量)可能比非final的变量(普通变量)拥有更高的效率,因此我们在实际编程中要考虑的用final来修饰普通变量,这也是一个很好的编程习惯。
二、finally的介绍
接下来我们一起回顾一下finally的用法。这个就比较简单了,它只能用在try/catch语句中,标志
public final classFinallyTest {public static voidmain(String[] args) {try{throw newNullPointerException();
}catch(NullPointerException e) {
System.out.println("程序抛出了异常");
}finally{//这里总会被执行,不受break,return影响另如数据库连接的close()一般写在这里,可以降低程序的出错几率
System.out.println("执行了finally语句块");
}
}//输出: 程序抛出了异常//执行了finally语句块}
那么,有没有一种情况使finally语句块得不到执行呢?大家可能想到了 return、continue、break这三个可以打乱代码顺序执行语句的规律。那我们就来试试看,这三个语句是否能影响finally语句块的执行:
Java代码public final classFinallyTest {//测试return语句publicReturnClass testReturn() {try{return newReturnClass();
}catch(Exception e) {
e.printStackTrace();
}finally{
System.out.println("执行了finally语句");
}return null;
}//测试continue语句
public voidtestContinue() {for (int i = 0; i < 3; i++) {try{
System.out.println(i);if (i == 1) {continue;
}
}catch(Exception e) {
e.printStackTrace();
}finally{
System.out.println("执行了finally语句");
}
}
}//测试break语句
public voidtestBreak() {for (int i = 0; i < 3; i++) {try{
System.out.println(i);if (i == 1) {break;
}
}catch(Exception e) {
e.printStackTrace();
}finally{
System.out.println("执行了finally语句");
}
}
}public static voidmain(String[] args) {
FinallyTest ft= newFinallyTest();//测试return语句
ft.testReturn();
System.out.println();//测试continue语句
ft.testContinue();
System.out.println();//测试break语句
ft.testBreak();
}
}classReturnClass {publicReturnClass() {
System.out.println("执行了return语句");
}
}
上面这段代码的运行结果如下:1. 执行了return语句2. 执行了finally语句3.4. 0
5. 执行了finally语句6. 1
7. 执行了finally语句8. 2
9. 执行了finally语句10.11. 0
12. 执行了finally语句13. 1
14. 执行了finally语句
结果显示:编译器在编译return new ReturnClass();时,将它分成了两个步骤,new ReturnClass()和return,前一个创建对象的语句是在finally语句块之前被执行的,而后一个return语句是在finally语句块之后执行的,也就是
说finally语句块是在程序退出方法之前被执行的,return、continue和break都没能阻止finally语句块的执行。从输出的结果来看,return语句似乎在 finally语句块之前执行了,事实真的如此吗?我们来想想看,return语句的作用
是什么呢?是退出当前的方法,并将值或对象返回。如果 finally语句块是在return语句之后执行的,那么return语句被执行后就已经退出当前方法了,finally语句块又如何能被执行呢?因此,正确的执行顺序应该是这样的:编译器
在编译return new ReturnClass();时,将它分成了两个步骤,new ReturnClass()和return,前一个创建对象的语句是在finally语句块之前被执行的,而后一个return语句是在finally语句块之后执行的,也就是说finally语句块是
在程序退出方法之前被执行的。同样,finally语句块是在循环被跳过(continue)和中断(break)之前被执行的。
三、finalize的介绍
当撤消一个对象时,需要完成一些操作。例如,如果一个对象正在处理的是非Java 资源,如文件句柄或window 字符字体,这时你要确认在一个对象被撤消以前要保证这些资源被释放。为处理这样的状况,Java 提供了被称为收尾
(finalization )的机制。使用该机制你可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行。
要给一个类增加收尾(finalizer ),你只要定义finalize ( ) 方法即可。Java 回收该类的一个对象时,就会调用这个方法。在finalize ( )方法中,你要指定在一个对象被撤消前必须执行的操作。垃圾回收周期性地运行,检查对象不
再被运行状态引用或间接地通过其他对象引用。就在对象被释放之前,Java 运行系统调用该对象的finalize( ) 方法。
finalize()方法的通用格式如下:
protected voidfinalize( )
{//finalization code here
}
其中,关键字protected是防止在该类之外定义的代码访问finalize()标识符。该标识符和其他标识符将在第7章中解释。
理解finalize( ) 正好在垃圾回收以前被调用非常重要。例如当一个对象超出了它的作用域时,finalize( ) 并不被调用。这意味着你不可能知道何时——甚至是否——finalize( ) 被调用。因此,你的程序应该提供其他的方法来释放由
对象使用的系统资源,而不能依靠finalize( ) 来完成程序的正常操作。
注意:如果你熟悉C ,那你知道C 允许你为一个类定义一个撤消函数(destructor ),它在对象正好出作用域之前被调用。Java不支持这个想法也不提供撤消函数。finalize() 方法只和撤消函数的功能接近。当你对Java 有丰富经
验时,你将看到因为Java使用垃圾回收子系统,几乎没有必要使用撤消函数。
在 C/C++、Pascal和其他几种多种用途的编程语言中,开发者有责任在内存管理上发挥积极的作用。例如,如果你为一个对象或数据结构分配了内存,那么当你不再使用它时必须释放掉该内存。
在 Java 中,当你创建一个对象时,Java 虚拟机(JVM)为该对象分配内存、调用构造函数并开始跟踪你使用的对象。当你停止使用一个对象(就是说,当没有对该对象有效的引用时),JVM 通过垃圾回收器将该对象标记为释放状态。
当垃圾回收器将要释放一个对象的内存时,它调用该对象的finalize() 方法(如果该对象定义了此方法)。垃圾回收器以独立的低优先级的方式运行,只有当其他线程挂起等待该内存释放的情况出现时,它才开始运行释放对象的内
存。(事实上,你可以调用System.gc() 方法强制垃圾回收器来释放这些对象的内存。)
在以上的描述中,有一些重要的事情需要注意。首先,只有当垃圾回收器释放该对象的内存时,才会执行finalize()。如果在 Applet 或应用程序退出之前垃圾回收器没有释放内存,垃圾回收器将不会调用finalize()。
其次,除非垃圾回收器认为你的 Applet 或应用程序需要额外的内存,否则它不会试图释放不再使用的对象的内存。换句话说,这是完全可能的:一个 Applet 给少量的对象分配内存,没有造成严重的内存需求,于是垃圾回收器没
有释放这些对象的内存就退出了。
显然,如果你为某个对象定义了finalize() 方法,JVM 可能不会调用它,因为垃圾回收器不曾释放过那些对象的内存。调用System.gc() 也不会起作用,因为它仅仅是给 JVM 一个建议而不是命令。
下面是关于finalize的一些常识:
1 java的GC只负责内存相关的清理,所有其它资源的清理必须由程序员手工完成。要不然会引起资源泄露,有可能导致程序崩溃。
2 调用GC并不保证GC实际执行。
3 finalize抛出的未捕获异常只会导致该对象的finalize执行退出。
4 用户可以自己调用对象的finalize方法,但是这种调用是正常的方法调用,和对象的销毁过程无关。
5 JVM保证在一个对象所占用的内存被回收之前,如果它实现了finalize方法,则该方法一定会被调用。Object的默认finalize什么都不做,为了效率,GC可以认为一个什么都不做的finalize不存在。
6 对象的finalize调用链和clone调用链一样,必须手工构造。
在对象的销毁过程中,按照对象的finalize的执行情况,可以分为以下几种,系统会记录对象的对应状态:
unfinalized 没有执行finalize,系统也不准备执行。
finalizable 可以执行finalize了,系统会在随后的某个时间执行finalize。
finalized 该对象的finalize已经被执行了。
GC怎么来保持对finalizable的对象的追踪呢。GC有一个Queue,叫做F-Queue,所有对象在变为finalizable的时候会加入到该Queue,然后等待GC执行它的finalize方法。
这时我们引入了对对象的另外一种记录分类,系统可以检查到一个对象属于哪一种。
reachable 从活动的对象引用链可以到达的对象。包括所有线程当前栈的局部变量,所有的静态变量等等。
finalizer-reachable 除了reachable外,从F-Queue可以通过引用到达的对象。
unreachable 其它的对象。
来看看对象的状态转换图
1 首先,所有的对象都是从Reachable+Unfinalized走向死亡之路的。
2 当从当前活动集到对象不可达时,对象可以从Reachable状态变到F-Reachable或者Unreachable状态。
3 当对象为非Reachable+Unfinalized时,GC会把它移入F-Queue,状态变为F-Reachable+Finalizable。
4 好了,关键的来了,任何时候,GC都可以从F-Queue中拿到一个Finalizable的对象,标记它为Finalized,然后执行它的finalize方法,由于该对象在这个线程中又可达了,于是该对象变成Reachable了(并且Finalized)。而
finalize方法执行时,又有可能把其它的F-Reachable的对象变为一个Reachable的,这个叫做对象再生。
5 当一个对象在Unreachable+Unfinalized时,如果该对象使用的是默认的Object的finalize,或者虽然重写了,但是新的实现什么也不干。为了性能,GC可以把该对象之间变到Reclaimed状态直接销毁,而不用加入到F-Queue
等待GC做进一步处理。
6 从状态图看出,不管怎么折腾,任意一个对象的finalize只至多执行一次,一旦对象变为Finalized,就怎么也不会在回到F-Queue去了。当然没有机会再执行finalize了。
7 当对象处于Unreachable+Finalized时,该对象离真正的死亡不远了。GC可以安全的回收该对象的内存了。进入Reclaimed。
finalize总结:
方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继
承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。
Java中所有类都从Object类中继承finalize()方法。
当垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法。值得C++程序员注意的是,finalize()方法并不能等同与析构函数。Java中是没有析构函数的。C++的析构函数是在对象消亡时运行的。由于
C++没有垃圾回收,对象空间手动回收,所以一旦对象用不到时,程序员就应当把它delete()掉。所以析构函数中经常做一些文件保存之类的收尾工作。但是在Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进
行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。
那么finalize()究竟是做什么的呢?它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),
finalize()的工作就是回收这部分的内存。
参考文献:
3.还有一些出处丢失 望见谅^ ^