5.5 清理:终结处理和垃圾回收
导航
- Java里的对象并非总是被垃圾回收
- 对象可能不被垃圾回收
- 垃圾回收并不等于”析构”
- 垃圾回收只和内存有关
5.5.1 finalize()用途何在
- 使用垃圾回收器的唯一原因是为了回收程序不再使用的内存
- 无论对象是如何创建的,垃圾回收器都会负责释放对象所占用的所有内存
- 之所以要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法
- 不要过多使用finalize()
终结函数无法预料,常常是危险的,总之是多余的
5.5.2 你必须实施清理
- 如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法.
- 如果JVM并未面临内存耗尽的情况,它是不会浪费时间去执行垃圾回收以恢复内存的.
5.5.3 终结条件
- 当对某个对象不再感兴趣,也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放
- 只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷,finalize()可以用来最终发现这种情况————尽管它并不总是会被调用
//: initialization/TerminationCondition.java
// Using finalize() to detect an object that
// hasn't been properly clean up
class Book{
boolean checkOut=false;
Book(boolean checkOut){ this.checkOut=checkOut;}
void checkIn(){ this.checkOut=false;}
protected void finalize(){
if(checkOut) System.out.println("Error:Check Out");
// Normally,you'll also do this:
// super.finalize(); //Call the base-class version
}
}
public class TerminationCondition{
public static void main(String[] args) {
Book novel=new Book(true);
//Proper cleanup
novel.checkIn();
//Drop the reference, forget to clean up
new Book(true);
//Fource garbage coolecion & finalization
System.gc();
}
}/*Output:
Error:Check Out
*///:~
5.5.4 垃圾回收器如何工作
1. 引用计数方法
- 实现方式: 每个对象都含有一个引用计数器.当有引用接至对象时,引用计数加1.当引用离开 作用域或被置为null,引用计数减1.
- 问题: 如果对象之间存在循环引用,可能会出现 对象应该被回收,但引用计数却不为零 的情况.对于垃圾回收器而言,定位这样的交互自引用的对象组所需的工作量极大.
- 使用场景: 常用来说明垃圾收集的工作方式,但并未应用于任何一种JVM实现中.
- 特点: 简单,但速度很慢
2. 更快速的垃圾回收技术
- 思想: 对任何”活的”对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用.这个引用链条可能会穿过数个对象层次.
- 实现:
- step 1: 从堆栈和静态存储区开始,遍历所有引用,就能找到所有”活”的对象.
- step 2: 对于发现的每个引用,必须追踪它所引用的对象,然后是这个对象所包含的所有引用,如此反复进行,直到根源于堆栈和静态存储区的引用所形成的网络全部被访问为止.访问过的对象必须都是”活”的
- 注意: 这解决了交互自引用的对象组的问题
3. 停止-复制(stop-and-copy)
- 一种 自适应 的垃圾回收技术
- 思想:
- 先暂停程序运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的都是垃圾
- 当对象被复制到新堆是,他们是一个挨着一个的,所以新堆保持紧凑排列,然后可以前述方法简单直接地分配内存空间了
- 注意点:
- 当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正
- 位于堆或静态存储区的引用可以直接被修正,但是可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到
- 这种所谓的复制式回收器效率会降低
- 首先,维护比实际需要多一倍的空间.某些JVM对此问题的处理方式是:按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间
- 第二个问题在于复制.程序稳定后,可能只产生很少甚至没有垃圾,尽管如此,复制式回收器仍然会将所有内存自一处副知道另一处,这很浪费.
4. 标记-清扫(mark-and-swap)模式
- JVM进行检查,如果没有新垃圾产生,就会转换到另一种工作模式
- 思路:从堆栈和静态存储区出发,遍历所有引用,进而找出所有存活对象.
- 实现:
- 每当找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象.只有在全部标记工作完成的时候,清理动作才会开始.
- 在清理过程中,没有标记的对象将被释放,不会发生任何复制动作.
- 剩下的空间是不连续的
5. 停止-复制
- 这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会被暂停
- 要求在释放旧有对象之前,必须先把所有存活对象从旧堆复制到新堆,这就导致大量内存复制行为
6. 块
- JVM中内存分配以较大的“块”为单位
- 如果对象较大,就会占用单独的块
- 有了块,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了
- 每个块都用对应的代数(generation count)来记录是否还存活.通常
- 通常,如果块在某处被引用,其代数会增加;垃圾回收器将会对上次回收动作之后新分配的块进行整理.这对处理大量短命临时对象很有帮助
- 垃圾回收器会定期进行完整的清理动作–大型对象仍然不会被复制,内含小型对象的那些块则会被复制并整理
7. 自适应技术:
- JVM会进行监视.如果所有对象很稳定,垃圾回收器的效率降低的话,就切换到标记-清扫模式
- JVM会跟踪“标记-清扫”模式的效果,要是堆空间多处很多碎片,就会切换回停止-复制模式
- 即时(Just-In-Time,JIT)
- 这种技术可以吧程序全部或部分翻译成本地机器码,程序运行速度因此得到提升
- 当需要装载某个类时,编译器会先找到其.class文件,然后将该类的字节码装入内存
- 让即使编译器编译所有代码
- 缺陷一: 执行时间增长 这种加载动作散落在整个程序生命周期内,累加起来要花更多的时间
- 缺陷二: 执行速度降低 增加可执行代码的长度,浙江导致页面调度,从而导致降低程序速度
- 惰性评估(lazy evaluation): 即时编译器只在必要的时候才编译代码
- 让即使编译器编译所有代码