《Java编程思想》笔记之第五章 初始化与清理——5.5 清理:终结处理和垃圾回收

时间:2022-04-14 19:41:55

<Java编程思想之第五章——初始化与清理>   笔记

说明:本文章只作为自己阅读笔记。只选取部分个人认为有价值的内容。

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

        Java允许在类中定义一个名为finalize()方法,但是finalize()不该作为通用的清理方法。

        Java中对象并非总是被垃圾回收,总结三点特性:

               1.对象可能不被垃圾回收。

         2.垃圾回收不等于“析构”。(对于C++而言)

         3.垃圾回收只与内存有关。

    无论对象是如何创建的,垃圾回收都会负责释放对象占据的所以内存。

    filnaleze()存在的意义:由于在分配内存是可能采用了类似C语音中的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法(日后查阅后补充)”的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法也许会调用C的malloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放,从而造成内存泄露。所以需要在finalize()中用本地方法调用它。(malloc()free() 是C和C++中的函数)

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

 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 TermainationCondtion{
	public static void main(String[] args) 
	{
		Book novel = new Book(true);
		novel.checkIn();
		new Book(true);  // 无引用对象,将被回收。回收时会调用 finalize() 方法
		System.Gc();
	}
}

该例子的终结条件是:所以的Book对象在被当做垃圾回收钱都应该被签入(check in)。但是在main()方法中,存在错误操作。要是没有finalize() 方法验证将很难发现这种缺陷。

System.gc()用于强制进行终结动作。通过重复执行程序,即使该对象大量占用存储空间,最终也能清除。

个人总结:finalize() 方法主要是为了清理垃圾回收时无法清理对象。

垃圾回收如何工作

在某些Java虚拟机中,堆分配对象比喻:就像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。Java的“堆指针”只是简单的移动到尚未分配的区域,其效率比得上C++的在堆栈上分配空间的效率。当然,实际过程中在薄记工作方面还是少量额外开销,但比不上查找可用空间开销大。

但Java堆未必完全是这样工作的。否则势必会导致频繁的内存页面调度——将其移进移出硬盘,因此会显得需要拥有比实际需要更多的内存。页面调度会显著的影响性能,创建大量的对象后最终内存资源耗尽。

而这时Java垃圾回收器介入。当他工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送到的开始处,也就尽量避免了页面错误。

通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型

其他垃圾回收机制(提供了解)

1.   引用记数

     引用记数是一种简单但速度很慢的垃圾回收技术。每个对象都含义一个引用计数器,当有引用连接至对象时,引用计数器加1。当引用离开作用域或被置 null 时,引用计数器减 1。引用记数的开销不大,但是这项开销在整个程序生命周期中持续发生。垃圾回收器会在含有全部对象的列表上遍历,释放引用记数为 0 的对象(但是引用记数模式进城绘制记数值变化为 0 时立即释放对象)。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用记数却不为零”的情况。对于垃圾回收而言,定位这样的交互自引用的对象组所需的工作了极大。引用记数常用来说明垃圾收集的工作方式,但似乎从未被英语与任何一种Java虚拟机实现中。

2.  另一种更快的,并非基于引用记数技术的垃圾回收机制

         对任何“活”的对象,一定能最终追溯到其存货在堆栈或者静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所以的引用,就能中单所以“活”的对象。对于发现的每个引用必须追踪它所引用的对象,然后是此对象保护的所以引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的忘了全部被访问为止。你所访问过的对象必须都是“活”的。主要,这就解决了“交互自引用的对象组”的问题——这种现象根本不会被发现,因此也就被自动回收了。

Java垃圾回收机制

Java虚拟机采用的是一种自适应的垃圾回收技术。至于如何处理找到的存货对象,取决于不同的Java虚拟机实现。

停止—复制(stop-and-copy)

有一种做法名为停止—复制(stop-and-copy)。显然这意味着,先暂停程序的运行(所以它不属于后台回收模式),然后将所有存货的对象从当前堆复制到另一堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,他们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。

每次对象复制时,所有指向它的引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还要其他指向这些对象的引用,他们在遍历的过程中才能被找到(可以想象成有个表格,讲旧地址映射至新地址)。

对于这种所谓的“复制式回收器”而言,效率会降低,这有两个原因。首先,得有两个堆,然后得在这两个分离的堆之间来回倒腾,从而得维护比实际需要多一倍的空。某些java虚拟机对此问题的处理方式是,按需从堆找那个分配即可比较大的内存,复制动作发生在这些大块内存之间。

第二个问题在于复制。程序进入文档状态之后,可能只会产生少了的垃圾,甚至没有垃圾。尽管如此,复制式回收器任然会将所有的内存自一处复制到另一侧,这很浪费。为了避免这种情形,一下Java虚拟机会进行检查:要是没有新垃圾产生,就好转换到另一种工作模式(即“自适应”)。这种模式称为标记—清扫

标记—清扫(mark-and-sweep)

“标记—清扫”的速度相当慢,但是这种模式只会处理少量甚至没有垃圾,这样速度就很快了。

“标记—清扫”的思路同意是从堆栈和静态存储区出发,遍历所有引用,进而找出所以存活的对象。为每个存活对象设置标记,当标记工作完成时,才会进行清理动作,释放没有标记的对象。所以剩下的堆空间是不连续的,如果垃圾回收器需要连续空间的话,就需要重新整理剩下的对象。

“停止—复制”和“标记—清扫”都不是后台进行的。都需要暂停程序。

------------------------------------------------------------------------------------------------------------------------------

Java虚拟机内存分配以比较大的“块”为单位。大的对象会占用单独的块。“停止—复制”在复制时会往废弃的块中拷贝对象。每个块都用相应的代数(generation count)来记录它是否还存活。如果块被引用,代数就会增加;垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象任然不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。

Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记—清扫”方式;同样,Java虚拟机会跟着“标记—清扫”的效果,要是堆空间出现很多碎片,就好切换回“停止—复制”方式。这就是“自适应”技术。

-----------------------------------------------------------------------------------------------------------------------------

Java虚拟机中有许多附加技术用以提升速度

“即时”(Just-In-Time,JIT)编译器

这种技术与加载器操作有关。它可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工资),程序运行速度因此得以提升。当需要装载某个类(通常是在为该类创建第一个对象)时。编译器会先找到其 .class 文件,然后将该类的字节码装入内存。此时有两种方案可供选择:一种是就让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序声生命周期内,累加起来要花更多时间;并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。

惰性评估(lazy evaluation)

惰性评估使即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许就压根不会被JIT所编译。新版本JDK中的Java HotSpot技术就采用了类似的方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。