一、Java的内存管理
对象是功能强大的软件构造模块,在 Java中它们有着极其广泛的应用。实际上,由于对象的应用是如此广泛,开发者有时忘记了创建对象所要付出的代价,结果就导致程序进入了“对象搅拌器”(Object Churn)状态。在这种状态下,处理器的大部分时间消耗在周而复始的创建对象和回收被废弃对象的操作中。
对于熟悉C/C++的开发者来说,内存管理方面的简化是Java重要的特点。与C/C++要求程序显式地分配和释放内存相反,Java允许开发者根据需要为对象分配空间,并确保当程序不再需要对象时,对象占用的空间会被JVM回收。这些工作都在后台的垃圾回收进程中进行。
在编程语言中,用垃圾收集机制来管理内存可以回溯到计算机刚刚诞生的二十世纪六十年代。无论具体情形如何,垃圾收集的基本原理都是一样的:先标识出那些程序不再使用的对象,然后回收这些对象占用的内存。
一般地,JVM利用一种“可到达性”(Reachability)算法标识正在使用的对象,然后回收所有其余的对象。这个过程从一组程序直接使用的变量开始,包括每一个活动线程的局部变量、方法调用堆栈上的参数变量以及已装入类的静态变量所引用的对象。所有上述几类变量所引用的对象都被加入到“可到达”对象集合。接着,这些对象的成员变量所引用的每一个对象也被加入到可到达对象集合。这个过程重复进行直至结束;结束时,可到达对象集合中的任意对象所引用的每一个对象都已经在可到达集合中。所有不在可到达对象集合中的对象都被认为已经废弃不用,也就是可以安全地回收。
通常,Java的垃圾收集过程无需开发者干预。JVM周期性地运行垃圾收集过程,或者是当程序的线程因为等待外部事件,所以允许垃圾收集过程运行;或者是当程序创建新对象时内存不足,因而必须运行垃圾收集过程。尽管垃圾收集是自动进行的,但了解这一过程是很重要的,因为垃圾收集将占用很大一部分Java程序的开销。
除了垃圾收集的时间开销之外,Java中的对象还会产生大量的空间开销。对于每一个已分配的对象,JVM将加上必要的内部信息以便垃圾收集过程顺利进行。另外,JVM还要加入一些Java语言规范所要求的信息,这些信息对于某些功能来说是必不可少的,比如在任意对象上同步的功能。如果把JVM内部为每一个对象分配的空间计入对象的占用空间,则小型Java对象要比C/C++中等价的对象大得多。下面的表格显示了几种不同JVM下一些简单对象的内存占用情况,其中“内容”列显示用户可访问内容的大小,其余各列显示不同JVM下对象实际占用的内存大小,从该表可以看出JVM额外增加的内存开销。
上面的空间开销是单个对象的值,因此,对于大对象来说空间开销的百分比将下降。如果程序使用了大量的小型对象,可能使性能变得很糟糕。
二、尽量使用基本数据类型
在Java程序中,减少对象创建操作最简单的策略或许就是尽可能地使用基本数据类型。然而,这个策略可适用的场合并不是很多,许多时候对象有充足的理由成为首选的数据格式,简单地用基本数据类型来替换对象不能满足设计要求。但是,在确实适用该策略的场合,它可以减少大量的开销。
Java的基本数据类型包括boolean、byte、char、double、float、int、long和short。使用基本数据类型的变量不会产生创建对象的开销,用完之后也不需要进行垃圾收集。对于局部方法变量,JVM将直接在堆栈分配变量空间;对于成员变量,JVM在对象使用的空间为变量分配空间。
Java为每一种基本数据类型定义了相应的封装类。封装类代表的是和基本数据类型值对应但不可变的值,使得基本数据类型的值可以作为对象处理;使用java.util.Vector、java.util.Stack、java.util.Hashtable等工具类时,这种对象非常有用。但除了这些特殊情况之外,应当避免使用封装类,尽量使用基本数据类型,避免创建对象所需要的内存和时间开销。
除了标准的封装类值外,Java类库中还有一些类是在基本类型的基础上加上一层新的语义和行为,java.util.Date和java.awt.Point都属于这类例子。在大量应用这类值的场合,存储和传递对应的基本类型值(只在必要的时候才把它们转换成相应的对象)能够减少不必要的对象创建操作。例如,对于Point类,我们可以直接访问Point内部的int值,或者把它们组合成一个long值使得方法调用只需返回一个基本类型的值。下面是一个计算中点的例子:
...
// 用long值表示Point的例子。
// 每一个long值的高位包含x坐标,
// 低位包含y坐标
public long midpoint(long a, long b) {
// 计算每一个坐标上的中值
int x = (int) (((a >> 32) + (b >> 32)) / 2);
int y = ((int) a + (int) b) / 2;
// 返回中点
return (x << 32) + y;
}
...
三、专用对象重用
减少对象创建操作的另一途径是重用对象。被重用的对象可能是专门用于某一特定用途,也可能在不同的时刻用于不同的目的,因此,重用对象主要包括两种变化形式:专用对象重用,具有简单方便的特点;自由缓冲池重用,具有最好的对象重用效果。
最简单的对象重用情况是,一个频繁执行的任务需要一个或者多个起辅助作用的对象。许多应用经常进行日期格式化操作,下面我们就以此为例讨论专用对象重用。要从一个指定的日期值(按照前面尽量使用基本数据类型的要求,这个值是long类型)生成默认的字符串表示形式,我们可以:
...
// 生成时间的默认字符串表示形式
long time = ...;
String display = DateFormat.getDateInstance().format(new Date(time));
...
这个语句看起来很简单,实际上却进行了大量复杂的对象创建操作。DateFormat.getDateInstance()调用创建了一个新的SimpleDateFormat实例,后者又要创建一系列相关的对象;然后,format调用又要创建新的StringBuffer和FieldPosition对象。在JRE 1.2.2和Windows 98下,这个简单的语句实际分配的内存多达2400字节。如果这个语句需要频繁地执行,临时生成和丢弃的对象数量是相当可观的。
3.1 私有的对象
改进的方法是预先(一次性地)创建进行格式化所需要的对象,这组对象由使用它们的代码所拥有(专用),然后在需要时重用这些对象。例如,如果我们通过实例变量实现该方案,使得容器类的每一个实例拥有这些对象的唯一一份拷贝,修改后的代码如下:...
// 以成员变量的形式分配专用的日期格式化对象
private final Date convertDate = new Date();
private final DateFormat convertFormat = DateFormat.getDateInstance();
private final StringBuffer convertBuffer = new StringBuffer();
private final FieldPosition convertField = new FieldPosition(0);
...
// 生成指定日期的默认字符串表示形式
long time = ...;
convertDate.setTime(time);
convertBuffer.setLength(0);
StringBuffer output =
dateFormatter.format(convertDate, convertBuffer, convertField);
String display = output.toString();
...
这个代码片断显然要比原先的代码长,但在每次执行时只需创建一个输出的字符串对象,因而速度要快得多。简单的测试表明,改进后的代码100000次迭代只需8秒,而原来代码的相应时间则为50秒。由于用来格式化的对象并未在一次使用后马上释放,所以改进后的代码占用了更多的内存(或占用时间更长),但如果代码执行非常频繁,这个代价仍是非常合算的。
值得指出的是,如果在一个执行大量迭代的方法之内有一个内部循环,上面讨论的技术同样有用。我们不一定要把循环内用到的对象转移到包含该方法的类之内,而是可以把这些对象的创建操作移到循环之外,使得创建操作只执行一次。按照这种思想,代码可以为:
// 分配循环内要用到的对象
Date date = new Date();
DateFormat formatter = DateFormat.getDateInstance();
StringBuffer buffer= new StringBuffer();
FieldPosition field = new FieldPosition(0);
// 执行循环
for (...) {
// 生成指定时间的字符串表示形式
long time = ...;
date.setTime(time);
buffer.setLength(0);
StringBuffer output = formatter.format(date, buffer, field);
String display = output.toString();
}
结合前面介绍的用基本类型值替代相应对象类型值技术,这种专用对象重用技术的效果更好。专用对象可以从基本类型值实例化然后传递给Java标准类库中要求对象作为参数类型的方法。上面的专用Date对象就是一个很好的例子。
3.2 多个线程公用的私有对象
假设我们有一组私有的对象,有多个线程并发地执行使用这些对象的代码,因此必须避免不同的线程操作这些对象可能引起的冲突。实现这个目标最简单的方法是指定其中一个对象作为整组对象的锁,把使用这些对象的代码封装在一个在锁对象上同步的块中。尽管每次使用私有对象时都会增加加锁操作的开销,但和创建对象的时间相比,加锁操作的开销较小。
假定以convertDate对象作为锁,则使用这些对象的代码应该改为: // 获得私有对象的使用