7.2 内存管理

运行的程序总要与内存进行交互。内存作为操作系统中的重要资源,对它的分配和释放进行管理是一项非常重要的工作。对于某些编程语言,内存管理的工作由开发人员来处理,C和C++语言是这类编程语言的典型代表。以C++为例,当程序通过new操作符创建新的对象之后,就会分配相应的内存资源。当程序不再需要这些对象的时候,需要在代码中将其显式释放,一般通过delete操作符来完成。内存分配和释放操作的正确性,由开发人员来保证,这在很大程度上取决于开发人员的经验和技能。实际上,在使用这些编程语言开发的程序中,与内存分配和释放相关的问题是比较多的。

第一个常见的问题是产生悬挂引用(dangling reference)。悬挂引用指的是对某个对象的引用实际上指向一个错误的内存地址。比如程序中某部分代码引用了由另外一部分代码创建的对象,在代码的运行过程中,这个被引用的对象被删除,它所占用的内存也被释放。随后这部分内存被重新分配给另外的对象使用,而之前的代码可能仍然保存着对原始对象的引用。当代码仍然通过这个旧的引用尝试访问对象的时候,就会出现错误,因为这个旧的引用所对应的内存地址中的内容已经发生了变化。如果这个新的内存地址被分配给另外的进程,这次访问请求就可能造成程序退出。

第二个常见的问题是产生内存泄露。造成这个问题的原因是某些对象所占用的内存没有被释放,又没有对象引用指向这些内存。这样就会导致这部分内存对程序来说既不可用,又无法被释放,因为在缺乏对象引用的情况下,程序无法对这部分内存进行任何操作。

对于需要进行显式内存管理的编程语言来说,开发人员通常需要遵循某些最佳实践或利用相关工具来进行正确的内存管理。比如C++语言中的最佳实践一般在构造方法中分配内存,在析构方法中释放内存。利用C++中的智能指针也可以进行自动内存释放。不管采用何种方式,都对开发人员提出了比较高的要求,也会造成程序中存在很多与内存管理相关的潜在问题。

因为显式内存管理比较容易出现错误,所以不少编程语言采用了自动内存管理机制。自动内存管理的含义是运行平台会提供专门的组件来进行内存的管理工作。这个组件通常被称为垃圾回收器(garbage collector, GC)。垃圾回收器不仅负责内存的回收,还负责内存的分配。这些编程语言通常也不提供直接的API来进行内存的分配和释放,内存的分配是隐式进行的,在创建新对象时自动分配所需的内存,而对象所占用的内存不需要在程序中显式释放,由垃圾回收器自动进行处理。Java语言是使用自动内存管理机制的典型代表。在Java中有用来创建对象的new操作符,但是没有对应的delete操作符。

Java平台上的内存管理由垃圾回收器负责完成。垃圾回收器属于Java虚拟机的一部分。当Java程序在虚拟机中运行时,垃圾回收器也在运行。垃圾回收器负责管理虚拟机中的内存。它的职责很简单,即负责内存的分配和释放。在程序运行过程中创建新对象时,垃圾回收器要在虚拟机可用的内存中找到一块合适的内存区域供此对象来使用。当不再需要某一个对象时,垃圾回收器会负责回收该对象占用的内存空间,以供之后的内存分配所用。虽然从描述上来说,垃圾回收器的功能并不复杂,但是要实现一个实用的垃圾回收器并非易事,最重要的是处理好垃圾回收器与运行的程序之间的关系。

当Java虚拟机启动并运行某个程序之后,它所能使用的内存总量的上限通常是固定的。在程序刚开始运行的时候,虚拟机中的大部分内存都处于空闲可用的状态。随着程序的运行,不断有空闲的内存区域被分配给程序运行所需的对象来使用。经过一段时间之后,虚拟机的内存大概就可以分成三类:第一类是当前仍然处于空闲状态的可用内存;第二类是正在被程序使用的内存;第三类是程序已经不再使用的内存,已经不再需要其中包含的数据内容。第二类和第三类内存的区别在于其所属的对象是否处于活动状态。一个对象处于活动状态的含义是指在当前运行的程序中还存在指向该对象的引用。如果没有引用指向一个对象,那么说明该对象无法被运行的程序所使用,它所占用的内存会被当成垃圾来回收。

随着程序的不断运行,虚拟机的内存中可用的空闲空间越来越少,垃圾越来越多。这时就需要运行垃圾回收器来回收内存中的垃圾区域,将其转换成可用的区域,以供下次内存分配时使用。垃圾回收器运行一次之后,程序中的部分垃圾区域会被正确回收。Java虚拟机中的垃圾回收器是运行在一个独立的线程中的,它会根据当前虚拟机中的内存状态,决定在什么时候进行垃圾回收工作。每次垃圾回收时所处理的内存区域的范围也是不同的。垃圾回收器的具体运行时间和频率无法事先预计,取决于垃圾回收器的实现算法。不同的虚拟机实现中的垃圾回收算法也有所不同。

由于垃圾回收线程和当前应用程序同时在Java虚拟机中运行,因此当前运行程序会受到垃圾回收器的影响。不同的垃圾回收器实现算法对程序的影响也不相同。应该根据程序的特性来选择与之相匹配的垃圾回收器实现算法。

在垃圾回收器的实现方式中,通常有很多因素需要考虑和权衡,其中与当前运行程序相关的是垃圾回收器的运行方式。一般来说有并发运行和暂停执行两种。并发运行的含义是指垃圾回收器与程序同时运行,垃圾回收器在绝大部分时候不会影响程序的运行。而暂停执行的方式是垃圾回收器在运行时,程序的运行被暂停。并发运行的方式对程序影响较小,但是对垃圾回收器的实现要求较高,实现起来也更复杂。在回收的过程中,由于程序仍然在运行,因此内存的状态也在不断发生变化。垃圾回收器需要花费更多的运行时间和内存空间来处理这种情况。暂停执行的做法实现起来比较简单,因为在回收的过程中,内存的状态是固定不变的。但是暂停会对程序产生比较大的影响,用户也可能会感觉到程序运行时的停顿。

虽然大多数时候垃圾回收器的运行时间和频率是无法预计的,但是程序仍然可以在特定的时机建议垃圾回收器进行回收工作,通过System.gc方法就可以建议垃圾回收器立即运行。不过在这种情况下,垃圾回收器也可能选择不运行。