和对象打交道时有一个至关重要的问题,那就是它们的创建和销毁方式。每个对象的创建都要消耗一些资源,尤其是内存资源。当我们不再需要一个对象时,就要及时清理它,这样它占用的资源才能被释放并重复使用。在一些环境简单的场景下,清理对象似乎不是一个难题:你创建了一个对象,根据自己的需要使用,不再使用的时候就将其销毁。然而不幸的是,我们经常会遇到更为复杂的情况。
假设你需要为某个机场设计一个航空管制系统(你也可以用同样的方式管理仓库中的箱子、录像带租赁系统,甚至还有装宠物的笼子)。刚开始的时候,一切都是如此简单:新建一个用于保存飞机对象的集合,然后每当有飞机需要进入航空管制区域的时候,就新建一个飞机对象并将其放入集合中。而每当有飞机离开航空管制区域时,就清理对应的飞机对象。
再做一个假设:还有其他系统也会记录飞机的数据,而这些数据不需要像主控制程序那样及时更新,比如只会对离开机场的小型飞机做记录。于是,你需要创建一个新的集合用于保存小飞机对象,并且每当新建的飞机对象是小型飞机时,需要将其放入这个新的集合中。当有后台进程处于空闲状态时,就会操作这些对象。
现在问题变得更加棘手了,你怎么判断什么时候需要清理对象?当你不再需要一个对象时,系统的其他部分也许还在使用该对象。更糟糕的是,在许多其他情况下也会遇到同样的问题。而对于诸如C++这样需要显式删除对象的编程语言来说,这绝对是一个相当让人头疼的问题。
对象的数据保存在哪里,系统又是如何控制对象的生命周期的呢?C++语言的宗旨是效率优先,所以它交给程序员来选择。如果要最大化运行时效率,可以通过栈区(也叫作“自动变量”或“局部变量”)保存对象,或者将对象保存在静态存储区里,这样在编写程序时就可以明确地知道对象的内存分配和生命周期。这种做法会优先考虑分配和释放内存的速度,在有些情况下是极为有利的。但是,代价就是牺牲了灵活性,因为你必须在编写代码时就明确对象的数量、生命周期以及类型。如果你希望解决一个更为普遍的问题,比如计算机辅助设计、仓库管理或者航空管制等,这种做法的限制性就太大了。
还有一种方案是在内存池里动态创建对象,这个内存池叫作“堆”(heap)。如果使用这个方案,直到运行时你才能知道需要多少对象,以及它们的生命周期和确切的类型是什么。也就是说,这些信息要等到程序运行时才能确定。如果你需要创建一个新对象,可以直接通过堆来创建。因为堆是在运行时动态管理内存的,所以堆分配内存所花费的时间通常会比栈多一些(不过也不一定)。栈通常利用汇编指令向下或向上移动栈指针(stack pointer)来管理内存,而堆何时分配内存则取决于内存机制的实现方式。
动态创建对象的方案基于一个普遍接受的逻辑假设,即对象往往是复杂的。所以在创建对象时,查找和释放内存空间所带来的额外开销不会造成严重的影响。此外,更大的灵活性才是解决常规编程问题的关键。
Java只允许动态分配内存 。每当你创建一个对象时,都需要使用new操作符创建一个对象的动态实例。
然而还有另一个问题——对象的生命周期。对于那些允许在栈上创建对象的编程语言,编译器会判断对象将会存在多久以及负责自动销毁该对象。但是如果你是在堆上创建对象,编译器就无从得知对象的生命周期了。对于像C++这样的语言来说,你必须在编码时就明确何时销毁对象,否则万一你的代码出了差错,就会造成内存泄漏。而Java语言的底层支持 垃圾收集器 (garbage collector)机制,它会自动找到无用的对象并将其销毁。垃圾收集器带来了很大的便利性,因为它显著减少了你必须关注的问题数量以及需要编写的代码。因此,垃圾收集器提供了一种更高级的保障以防止潜在的内存泄漏,而正是内存泄漏导致了许多C++项目的失败。
Java设计垃圾收集器的意图就是处理内存释放的相关问题(虽然不包括清理对象所涉及的其他内容)。垃圾收集器“知道”一个对象何时不再有用,并且会自动释放该对象占用的内存。再加上所有对象都继承自顶层基类Object,以及只能在堆上创建对象等特点,使得Java编程比C++简单了不少。一言以蔽之,需要你介入的决策和阻碍都大大减少了。