天天看点

Java编程思想之清理与初始化

Java数据存储

对象存在何处,C++认为效率控制是最重要的议题,所以给程序员提供了选择的权利。为了追求最大的执行速度,对象的存储空间与生命周期可以在编写程序时确定,可以将对象置于堆栈,限域变量或者静态存储区。

第二种方式是在被称为堆的内存池中动态创建对象,在这种方式中,知道运行时才知道需要多少对象。

对象生命周期。对于允许在堆栈中创建对象的语言,编译器可以确定对象存活的时间,并自动销毁它。而对于堆上创建的对象,编译器则对它的生命周期一无所知。

Java对象存储位置

  • 寄存器。最快的存储区,不受控制
  • 堆栈。位于通用RAM中,通过堆栈指针可以从处理器哪里直接获得支持。堆栈支持向下移动,则分配新的内存;向上移动,则释放那些内存。这是一种快速有效的分配存储方式,仅次于寄存器。创建程序时Java必须知道堆栈内所有项的生命周期,对象引用存于其中,Java对象并不存储其中。
  • 堆。一种通用的内存池,也位于RAM,用于存放所有的Java对象。相比栈,编译器不需要知道堆中数据的存活时间,具有很大的灵活性。但这种灵活性也有很大代价,用堆进行存储分配和清理比栈需要更长的时间。
  • 常量存储。常量值通常直接存放在程序代码内部,因为常量永远不会改变。
  • 非RAM存储。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。两个基本粒子是流对象和持久化对象。
  • 特例:基本类型。对于一些基本类型,Java采用和C++相同方法,不用new创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
  • 特例:Java中的数组。当创建一个数组对象,实际创建了一个引用数组,并且每个引用都会自动被初始化为一个特定值null。当然,还可以创建存放基本数据类型的数组,编译器也能确保这种数组的初始化。

finalize方法

Java垃圾回收器只知道如何释放那些由new分配的内存,对于非使用new创建的特殊内存区域(比如Java本地方法,调用C/C++),垃圾回收器不能有效的进行释放。

在这种情况下,Java提供了finalize方法。

这里与C++的析构函数进行对比。在C++中,析构函数使得对象一定会被销毁,而Java中调用finalize并非总能被垃圾回收。

  • 对象可能不被垃圾回收
  • 垃圾回收并不等于析构
  • 垃圾回收只与内存有关

使用垃圾回收器的唯一原因就是为了回收程序不再使用的内容,所以对于垃圾回收有关的任何行为(尤其finalize方法),它们也必须与内存及其回收有关。

无论对象如何创建,垃圾回收期都会负责释放对象占据的所有内存。所以一般情况下都不需要通过finalize方法释放。除非某些特殊情况,例如本地方法调用C/C++,malloc的内存需要在finalize方法中调用free释放。

所以,不要过多的使用finalize方法。

finalize方法可以用来验证对象终结条件。

比如对于一批书籍,new Book生成的对象释放的条件是这本书需要录入系统,但如果存在某些书籍在没有录入系统的情况下被回收了可以使用finalize方法查出异常。

protected void finalize() {
    if(checkedOut)
        System.out.println("Error: check out");
}
           

初始化

静态数据的初始化

静态数据初始化只有在必要时刻才会进行,此后,静态对象不会再次被初始化。

初始化的顺序是先静态对象,而后是非静态对象。比如创建了一个Dog的类:

  • 即使没有显式使用static,构造器实际也是静态方法。因此首次创建Dog对象时,或者Dog类的静态方法/域被访问时,Java解释器必须查找类路径,定位Dog.class文件
  • 载入Dog.class,有关静态初始化的所有动作都会执行,并且静态初始化仅执行一次
  • 当用new Dog()创建对象时,在堆上为Dog对象分配足够的存储空间
  • 这块存储空间清零(基本类型赋默认值,引用赋null)
  • 执行所有出现于字段定义处的初始化动作
  • 执行构造器

数组初始化

所有数组都有一个固定成员,通过它可以获得数组内包含了多少个元素,但不能进行修改。length

垃圾回收机制

一般程序语言在堆上分配对象的代价十分高昂,然后对于Java,垃圾回收器对于提高对象的创建速度具有明显效果。Java从堆分配空间的速度,可以和其他语言从栈中分配空间的速度相媲美。

打个比方,C++的堆可以想象一个院子,里面每个对象都负责管理自己的地盘,一段时间后,对象可能被销毁,但地盘必须加以重用;而对于Java,堆更像一个传送带,每分配一个新对象,它就往前移动一格,这意味着对象存储空间分配速度非常快,效率可以比得上C++栈上分配空间。

Java的堆并不完全像传送带那样工作,不然会导致频繁的内存页面调度,严重影响性能。其中的秘密在于垃圾回收器的介入,当它工作时,将一面回收空间,一面使堆中的对象紧凑,这样堆指针很容易移动到更靠近传送带的开始处。

现在理解其他语言的垃圾回收机制,引用计数器是一种简单但速度很慢的方法。每个对象都有一个引用计数器,当有引用连接对象时,引用计数器+1,当引用离开作用域或置null时,-1。虽然引用计数开销不大,但这项开销在整个程序生命周期持续发生,垃圾回收器在含有全部对象的列表遍历,某个对象引用为0是释放空间。这种方法有个缺陷,如果对象之间存在循环引用,可能出现“对象应该被回收,但引用计数不为0”。所以引用计数这种方式常常用来说明垃圾回收机制,并未被真正使用。

在一些更快的模式中,垃圾回收器并非基于引用计数技术,它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链可能穿过数个对象层次。因此,从堆栈和静态存储区开始,遍历所有的引用,就能找到“活”的对象。

在这种方式下,Java虚拟机采用了一种自适应的垃圾回收技术。至于如何处理存活的对象,取决于不同的Java虚拟机实现。有一种做法“停止-复制(stop-and-copy)”先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾。

对于这种复制式回收器而言,效率很低。

  • 两个堆倒腾,需要双倍空间。某些虚拟机解决方法是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
  • 复制本身。程序稳定后可能只产生了少量垃圾,甚至没有垃圾。复制必然会造成浪费。解决方法是如果没有新垃圾产生,会切换到另一种模式(自适应)“标记-清扫(mark-and-sweep)”

“标记-清扫”思路同样是从堆栈和静态存储区触发,遍历所有引用,找到存活对象进行标记。标记完成后对没有标记的对象进行清理。这样剩下的堆空间可能不连续,垃圾回收器希望得到连续空间的话需要重新整理剩下的对象。

“停止-复制”,“标记-清扫”都必须在程序暂停的情况下进行。

Java虚拟机中,内存分配以较大的“块”为单位,如果对象较大,会占用单独的“块”。“停止-复制”要求释放对象前把所有存活对象从旧堆复制到新堆,这会造成大量内存复制行为。有了块以后,垃圾回收器在回收时可以往废弃的块拷贝对象。每个块都有相应的代数来记录是否存活,某个块被引用,代数增加。垃圾回收器对上次回收动作之后新分配块进行整理,大型对象不会被复制,小型对象的那些块则被复制并整理。如果所有对象都很稳定,垃圾回收器的效率低的话,就切换到“标记-清扫”,如果堆中出现很多碎片,则切换回“停止-复制”方式,这就是“自适应”技术。

Java虚拟机有很多附加技术用以提升速度,尤其与加载器操作相关的,被称为“即时”(Just-In-Time,JIT)编译器的技术。这种技术把程序全部或部分翻译成本地机器码,程序运行速度得以提升。当需要装载某个类,编译器首先找到其class文件,然后将该类的字节码载入内存。此时,有两种方法选择

  • 即时编译器编译所有代码。缺陷很明显:加载动作散落在整个程序生命周期,累计起来耗时更大;增加可执行代码的长度,导致页面调度,降低程序速度
  • 惰性评估,即时编译器只需要在必要的时候编译代码,这样不执行的代码不会被JIT编译。

新版Jdk中的Java Hotspot技术采用类似方法,代码每次执行多会做一些优化,执行次数越多,速度就越快。