一, 垃圾回收器和 finalize()
java 垃圾回收器只负责回收无用对象占据的内存资源. 但是如果你的对象不是通过 new 创建的 (所有的 new 对象都往堆中开辟资源, 在一个地方, 方便清理 / 管理资源), 它会不知道该如果释放该对象的这块特殊内存. 为了应对这个情况, Object 自带一个 finalize() 方法.
finalize()这方法的原理是: 一旦垃圾回收器准备释放该对象占用的存储空间, 将会先调用其继承 / 重写的 fialize(), 并且调用方法后不是立即执行回收, 而是在下一次 (JVM 觉得需要更大内存的时候) 回收动作发生时, 才会真正回收对象占用的内存. 所以一般自己重写 fialize()方法, 是在回收的最后时刻做一些重要的清理工作.
java 垃圾回收几个特点:
1, 对象可能不被垃圾回收
你创建的对象做了某个功能, 比如显示在电脑的屏幕上. 那么除非你特别处理从屏幕上擦除, 它永远不可能得到清理. 所以如果在 finalize()方法中做擦除屏幕的处理, 当垃圾回收时, finalize()被调用, 屏幕图像清除. 请注意: 垃圾回收器只有在 JVM 觉得需要更大内存的时候才会运行(虽然开销小, 但是一直运行还是有开销的), 所以大部分回收动作是发生在濒临存储空间用完的那一刻, 逼得 JVM 去运行垃圾回收器. 如果程序执行结束 (或者中断运行), 那些资源也会全部还给操作系统.
2, 垃圾回收并不等于析构
这个是 C 的概念, 因为 java 和 C 的牵扯太深, 所以经常拿来对比. 简单说 C 有一个东西叫析构函数, 在销毁对象前必须执行这个析构函数. 这里的垃圾回收并不代表析构. finalize()就是类似功能但是不等于.
3, 垃圾回收只与内存有关
这里就要讲到 finalize()的真正用途. 该方法内部执行的操作也应该和内存及其回收有关, 所以 fialize()方法不是通用的方法. 你可能会想到, 当对象包含成员对象属性的时候, finalize()是否应该明确要清除那些对象呢? 不正确. 应该这样理解: 无论对象如果创建, 垃圾回收器都会负责释放对象占用的所有内存. 所以 finalize()一般是来处理通过创建对象以外的方式为对象分配存储空间. 说起来有些绕口, 但是举个例子就知道了.
java 跟踪源码的时候经常遇到关键字 native 修饰的方法, 这些方法也叫 "本地方法". 在使用这些本地方法的时候, 内部调用的是非 java 代码的方式 (不是 C 就是 C++).. 这些非代码中, 也许会用到 C 的 malloc() 函数系列来分配存储空间. 这样除非调用 C 的 free()方法, 否则存储空间将不会释放. 所以可以在 finalize()使用 native 方式调用 free.
以上, 就是建议尽量少重写 finalize()的道理.
二, 垃圾回收条件
既然 fialize()使用场景这么生僻, 那就不要指望频繁使用 fialize(). 你必须创建其他的清理方法, 来自己根据业务清理. 但是 fialize()有个特点是: 程序调用它, 是该对象 "终结条件" 的验证. 也就是被标记了, 该对象已死, 可以回收了. 例如: 某个对象代表打开的一个文件, 在对象被回收前程序员应该关闭文件. 只要对象存在没有被适当清理的部分, 程序就存在隐晦的缺陷. fialize()可以用来最终发现这种情况.
- // Using finalize() to detect an object that
- // hasn't been properly cleaned up.
- 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");
- // 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);
- // Force garbage collection & finalization:
- System.gc();
- }
- }/* Output:
- Error: checked out
- *///:~
这个例子的终结条件是: 所有 Book 对象在被当做垃圾回收前都应该 checkIn. 但是在 main 里面, 第二本书没有 checkIn, 这个时候通过 finalize(), 就能明确知道, 有的对象没有在销毁前处理干净了. 另外, 代码中还使用了 System.gc(); 这是强制唤起垃圾回收机器, 来触发 BOOK 的 finalize(); 当然, 如果不这样强制唤起也行, 当程序运行到被分配了大量内存的时候(可以大量反复创建 BOOK), 逼得垃圾回收器会自动触发. 如果 BOOK 有继承某个父类, 要触发该父类的 finalize(), 可以使用 super.finalize(); 调用.
三, 垃圾回收器如何工作
一般印象里面, 在堆内分配新资源会比较慢, 毕竟比不了堆栈快. 但是其实 JVM 在这方面是做了大量的优化, 其中垃圾回收器对于提高对象的创建速度, 具有明显效果. 即使用垃圾回收器释放存储空间有利于未使用存储空间的分配. 通俗点就是说, 垃圾回收器回收的内存越多, 创建对象理论上会更快 (还是有临界的, 一般认为媲美其他语言在堆栈中创建对象, 比如 C).C 的堆就好像一个院子, 里面的每个对象各管各的存储空间. 一段时间以后某个对象被销毁了. 它的空间必须被重新使用. 在某些 JVM 中, 堆就像一个传送带, 分配一个新的对象, 它就往前移动一格. 这个意味着空间分配会非常快(寻址快).java 的寻址指针只需要简单移动到尚未分配的区域就行, 这样效率比得上 C 在堆栈上分配的速度. 其中, 记录对象空间地址 "下标" 方面, 还是有部分开销的, 但是比 C 需要查找堆的开销, 小得多. 其实, java 中的堆未必完全是像传送带, 因为这会造成频繁的内存页面调度(内存是分页的, 翻页时是要移出硬盘, 放在虚拟内存上). 页面调度会显著影响性能, 最终, 在创建足够的对象后, 内存(大量虚拟内存充斥) 资源耗尽.
这里就轮到垃圾回收器登场了, 当垃圾回收器工作的时候, 一边回收空间, 一边使堆中的对象排列紧凑. 这样 "堆指针" 就可以很容易移动到更靠近传送带的开始处 (java 堆分配空间是先进), 也就尽量避免了页面错误. 垃圾回收期会对对象重新排列, 实现高速的, 有无限空间(?) 可以分配的堆模型.
下面是垃圾回收 (不止 java) 常用的三种设计方式:
1, 引用计数
每个对象含有一个应用计数. 当有引用连接到对象的时候 + 1, 引用离开作用域或者赋值 null 的时候 - 1. 好了, 那么当发现某个对象的引用是 0 的时候, 就释放它占用的空间(这里会出现一变为 0 就释放空间). 这里就存在缺陷, 如果对象循环引用, 即 A 引用 B,B 引用 A, 就出现 "对象可以被回收, 但是引用计数不是 0" 的情况. 对于垃圾回收器来说, 定位这种互相引用的对象组开销极大. 另外, 管理引用计数的开销不大, 但是这个会在整个程序生命周期内持续发生. 引用计数的方式一般用来表述垃圾收集, 但没有应用于任何一种 JVM 中.
2,stop-and-copy
这种方式是先暂停程序 (不是后台运行, 而是停止程序, 执行垃圾回收), 然后将所以存活的对象从当前的堆中复制到另外一个堆, 剩下的都是垃圾. 当对象被复制到新的家(堆) 时, 会把这些对象一个挨着一个, 所以新堆保持紧凑排列. 这个就是前面说的 JVM 虚拟机的垃圾回收期为什么能做到使对象紧凑排列了. 复制过程会产生新的开销, 以及所有指向就旧对象的引用都要指向新的地址. 这里可能出现来自非堆的引用(不是 new 出来的对象), 这些会在遍历旧堆的引用的时候被找出来, 重新指向新堆.
这种回收方式, 效率低. 首先, 是因为要有两个堆, 然后对象要在两个堆中复制转移, 所以实际上维护的空间比理论上大一倍. 例如, 某些 JVM 的做法是, 在堆里面分配几个大的内存块, 复制的操作在这里个内存块中进行. 其次, 当程序运行趋于稳定以后, 产生的垃圾比较少, 甚至可能没有垃圾. 这个时候来回复制就很浪费了. 为了避免浪费, JVM 会进行检查, 要是没有新垃圾产生, 就自动转换成下一种模式.
3,mark-and-sweep
Sun 公司早期版本的虚拟机使用的就是这个. 这种方式的思路是从堆和静态存储区出发, 遍历所有的引用, 然后就能找到所有存活对象. 每当找到一个存活对象, 就给该对象一个标记, 记上一笔. 所有标记都做完以后, 清理开始. 在清理的时候, 没有标记的对象将被释放, 不会发生复制动作. 这个时候堆中就有点像 C 的样子, 所以如果要让剩下的对象内存连续, 就需要重新整理剩下的对象.
小结:
Sun 的文献把垃圾回收看做是低优先级的后台进程, 指的是因为 stop-and-copy: 毕竟要暂停程序. 但是在早期版本中, JVM 使用的是 mark-and-sweep. 现在这两种回收方式通过 JVM 进行监视, 如果有所对象都很稳定, 垃圾回收器效率降低的话, 就切换到 mark-and-sweep. 同样的, 如果 mark-and-sweep 的效果不好, 堆中出现了很多垃圾碎片 (无引用对象), 就会切换到 stop-and-copy. 在 stop-and-copy 使用的时候, 因为内存分配以较大的 "块" 为单位, 如果对象较大, 它会占用整个块. 在 stop-and-copy 运行的到停止程序运行的操作前, 会把所有存活的对象复制到新的块(堆) 中, 这个时候旧的块就会被废弃, 垃圾回收器就可以向废弃的块复制新对象进去, 灵活利用资源. 每个块都有相应的代数 (count) 来记录它是否还存活. 如果块在某处被引用, count 会增加. 垃圾回收器将对上次回收动作之后新分配的块进行整理. 这个対短命的对象很有帮助. 垃圾回收期会定期进行完整的清理动作 ---- 大型对象仍然不会被复制, 内含小型对象的那些块还是会被复制并且整理.
JVM 有许多的附加技术用以提升速度. 比如 JIT(Just-In-Time)编译器的技术. 这个会把程序全部或部分翻译成本地机器码, 程序运行速度因此快上很多. 当需要装载某个类的时候 (创建该类的第一个对象, 后续创建不会再次装载), 编译器先找到对应的 class 文件, 然后把字节码内容装入内存中. 这个时候, 有两种方式可以选. 其一是让 JIT 编译所有代码转换成机器码. 但是因为装载的发生不可控制, 是零散在整个程序的生命周期内的, 累加起来就需要花费很多时间, 并且会增加可执行代码的长度(字节码要比 JIT 编译后展开的机器码小很多), 这个可能导致内存页面调度, 从而降低程序的速度. 其二是惰性评估(lazy evaluatio), 意思是 JIT 只有在必要的时候才编译代码, 这样从不会被执行的代码(import 进来, 但是没有使用) 就压根不会被 JIT 编译. JDK 中的 Java HotSpot 技术就是采用了类似方法, 代码每次执行都会做一些优化, 所以执行次数越多, 它的速度就越快.
来源: https://www.cnblogs.com/lb-alex/p/11112483.html