回忆一遥远 发表于 2017-8-17 08:28:10

0基础学习Java ——第五段 ---初始化与清理(中)

本帖最后由 回忆一遥远 于 2017-8-17 15:53 编辑

初始化与清理

“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
初始化和清理(cleanup)正是涉及安全方面的两个问题。


清理——终结处理和垃圾回收
Java 垃圾回收器负责回收无用对象的内存空间
但当你的对象不使用 new ,获得了一块“特殊”的内存区域时,由于垃圾回收器只知道释放那些由 new 分配的内存,因此垃圾回收器无法释放这块“特殊”内存。

为了能够顺利完成内存的回收工作,Java 允许在类中定义 finalize() 的方法。
它的使用条件是:当垃圾回收器准备好释放对象占用的存储空间时,会首先调用 finalize() 方法,并在下次垃圾回收动作发生时回收对象占用的内存

finalize() 和 C++ 中的析构函数的区别
        ▢ Java 中,对象可能不被垃圾回收
        ▢ 垃圾回收和虚构不同
finalize() 用途?
        ▢ 垃圾回收只与内存有关
使用垃圾回收器的唯一原因是为了回收不再使用内存。因此 finalize() 方法也必须同内存及其回收有关。

使用 finalize() 主要时在使用“本地方法”的情况下
本地方法是一种在 Java 中调用非 Java 代码的方式。
(本地方法目前只支持 C 和 C++,但它们可以调用其它语言的代码,因此实际上可以调用任何代码。)
在非 Java 代码中,如果用 C 的 malloc 或 C++ 的 new 开辟空间,为了能释放这些内存,需要在 finalize() 中分别调用 free() ,delete() 函数。

无论是“垃圾回收”还是“终结”,都不保证一定会发生。
如果 Java 虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。


虽然 Java 没有析构函数的原因是存在垃圾回收器,然而垃圾回收器的存在并不能完全替代析构函数。

终结条件
通常不能指望 finalize() 进行清理工作,必须创建其他的“清理”办法,并明确调用它们。
finalize() 可以对对象终结条件验证

当对某个对象不再感兴趣——也就是它可以被清理了,这个对象应该处干某种状态,使它占用的内存可以被安全地释放。
例如,要是对象代表了一个打开的文件,在对象被回收前程序员应该关闭这个文件。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷。
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");
                //super.finalize();
        }
       
}

public class Termin {

        public static void main(String[] args) {
                Book novel = new Book(true);
                novel.checkIn();
                new Book(true);
                System.gc();
                // 提示垃圾回收器进行内存的回收
        }
}

输出:
Error: checked out
上面例子的终结条件:所有 Book 对象在当作垃圾回收前都应该被 check in ,但是实际上有一本书未被 check in。
如果没有 finalize() 验证终结条件,将很难发现这种缺陷。
System.gc() 用于强制进行终结动作。

垃圾回收器的工作方式
Java 从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。
打个比方,你可以把 C++ 里的堆想像成一个院子,里面每个对象都负责管理自己的地盘。 一段时间以后,对象可能被销毁,但地盘必须加以重用。在某些Java虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。
Java 的“堆指针”只是简单地移动到尚未分配的区域,效率比得上 C++ 在堆栈上分配空间的效率。(虽然实际上还会有少量额外的开销,但比不上查找可用空间开销大。)

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


一些垃圾回收机制
引用计数
一种简单但速度很慢的垃圾回收技术
每个对象都有一个引用计数器,当有引用连接至对象时,引用计数加1,当引用离开作用域或被置 null 时,引用计数减1。
它会在整个程序生命周期中持续发生。
垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用次数为0时,就释放占用的空间。
这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的惜况。对垃圾回收器而言,定位这样的交互自引用的对象组所需的工作量极大。

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


停止-复制(stop-and-copy)
先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨若一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。
当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。

对于这种所谓的“复制式回收器”而言,效率会降低,这有两个原因。
首先,得有两个堆,然后得在这两个分离的堆之间来回捣腾,从而得维护比实际需要多一倍的空间。某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
第二个问题在于复制。程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。
尽管如此,复制式回收器仍然会将所有内存自一处复制到另一处,这很浪费。为了避免这种情 形,一些Java虚拟机会进行检査:要是没有新垃圾产生,就会转换到另一种工作模式(即“自 适应”)。这种模式称为标记-清扫(mark-and-sweep),Sun公司早期版本的Java虚拟机使用了这 种技术。对一般用途而言,“标记-清扫”方式速度相当慢,但是当你知道只会产生少量垃圾甚 至不会产生垃圾时,它的速度就很快了。


标记-清扫(mark-and-sweep)
从堆栈和静态存储区出发,遍历所有的引用并找出所有存活的对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,淸理动作才会开始。
在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要得到连续空间的话,就得重新整理剩下的对象。
它必须也要在程序暂停的时候才能运行。


Java 虚拟机中,内存分配以较大的“块”为单位。如果对象较大,它会占用单独的块。
每个块都用相应的代数(generation count)来记录它是否还存活。
“自适应”技术:
Java 虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式;
同样,Java虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式。



Java 虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时” (Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类(通常是在为该 类创建第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。
此时,有两种方案可供选择。一种是就让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间,并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。另一种做法称为惰性评估(lazy evaluation),意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许就压根不会被 JIT 所编译。新版 JDK中的Java HotSpot 技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。






页: [1]
查看完整版本: 0基础学习Java ——第五段 ---初始化与清理(中)