JAVA中堆内存空间管理问题探讨

摘要:Java堆是一个运行时的数据区,对象从中分配空间,但是空间的容量是有限的。在堆内存空间中有“有用信息”,也有“无用信息”, “无用信息”占据着内存空间,降低了内存的使用效率。因此,我们必须使用某种算法将无用的信息从内存中清除,并把有用的信息重组,放在内存的一端,那么另一端则变成连续的空闲区,可以用来存放有用的信息。内存空间回收的作用是重大的。Java中堆内存的管理便达到了以上的要求。充分理解Java堆内存的特点,可以更有效的让我们利用资源。

关键词:堆内存,垃圾收集,算法,特点

 

 

第一章            堆内存管理的作用

 

程序的指令和数据只有存放在处理机能直接访问的内存中,这部分程序才能被执行,而计算机的内存容量总是有限的。面对着规模越来越大的计算机软件,内存容量不足的矛盾越来越尖锐。增加内存容量,可以在一定程度上解决这个问题但要从根本上解决这个问题,我们不应当从硬件着手,硬件“只能治标而不能治本”。处理机在执行某段程序时,那么处理机以前执行的程序便是“无用信息”,但这些“无用信息”仍占据着内存,以至于这些内存成了垃圾,无法存放其他的“有用信息”,那么对内存管理的任务是重大的,将堆内存中“无用信息”丢弃,把它的内存回收,用来存放其他的有用的信息。于是堆内存中的垃圾收集器便发挥了重大的作用,也正是因为垃圾收集完成了对堆内存的管理。我们所说的堆内存对空间的管理,也就是说堆内存中垃圾收集这一机制对堆内存空间的管理,实现内存空间的动态平衡。所谓“垃圾收集”,即是收集那些被“无用信息”占据的内存,以此来提高内存的使用效率。

在Java中,当没有对象引用指向原先分配给某个对象的内存时,JVM的一个系统线程便会自动释放该内存块,内存回收它所占用的空间,以便给后来新的对象使用。这就意味着程序不再需要的对象是“无用信息”,就应当被丢弃。在C++中,对象所占的内存在程序结束运行之前一直被占用,不管是“有用信息”还是“无用信息”,在没有明确释放之前是不可以分配给其他对象的,这样就给内存增加了负担。关于C++对象释放和内存回收问题将会在后文介绍。

容易理解,对象释放后,内存回收并不是连续的,必然会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。垃圾收集机制则可以清除内存记录碎片,它将所占用的堆内存存移到堆的一端,于是在堆的另一端便是连续的内存空间,用来分配给新的对象。

内存空间的释放从根本上解决了内存不足的问题,这样就提高了内存的使用效率和编程效率,保护了程序的完整性。

每一个先进的机制都有自己的缺点。堆内存管理中垃圾收集机制的一个潜在的缺点是它的开销影响程序的性能。Java虚拟机必须追踪运行程序中的有用对象,释放无用对象,这些都要经过处理机的处理,都要花费处理机的时间,而且垃圾收集机制也不可能100%收集到所有的废弃内存,这也正是它的不完备性。但是随着现代技术的发展,垃圾收集算法的改进,这些问题都将会得到解决。

 

 

第二章            堆内存管理中垃圾收集算法的种类

 

   在介绍垃圾收集算法前,先介绍一个概念:根集,所谓根集就是正在执行的Java程序可以访问的引用变量的集合,这个集合中包含了局部变量、参数以及类变量等等。垃圾收集算法要确定从根开始哪些是可达的,哪些是不可达的。从根开始可达的(包括间接可达)是活动对象,它们拒绝被释放,不能作为垃圾被回收;从根开始通过任意路径皆不可达的对象将被释放,作为垃圾而被回收。下面介绍集中常见的算法。

2.1 引用计数法(Reference Counting Collector)

引用计数法是唯一一个没有使用根集的垃圾收集算法,它使用引用计数器的计数来区分存活对象和不再使用的对象。每一次创建一个对象并赋给一个变量时,引用计数器为1,当对象赋给任意变量时,引用计数器加1,当对象出了作用域,引用计数器减1,一旦为0,对象就要被释放,它所占的内存将要被回收。引用计数器在加1和减1时,增加了程序执行的开销。

2.2 tracing算法(Tracing Collector)

tracing算法是为了解决引用计数器法的问题而提出的,它使用了根集的概念。它从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式对可达的对象进行标记。例如对每一个可达的对象设置一个或多个位。在扫描识别的过程中,tracing算法的垃圾收集也称为标记和清除(mak-and-sweep)垃圾收集器。

2.3 compacting算法(Compacting Collector)

内存回收的过程中必然会出现碎片,compacting算法正是为了解决这一问题而提出的。它在清除的过程中,将所有的对象移到堆的一端,堆的另一端就变成了连续的空闲区,收集器会对它移动的所有对象的所有引用进行刷新,使得这些引用在新的位置上能识别原来的对象,保证原有程序的完整性,为了compacting算法的收集器的实现,一般增加句柄和句柄表,这也增加了程序的开销。

2.4 coping算法(Coping Collector)

为了解决compacting算法中增加句柄和句柄表给程序带来更大的开销问题,于是提出了coping算法,不仅如此coping算法还解决了堆碎片的垃圾回收问题。它把堆分成一个对象面和多个空闲面,为对象分配空间。当对象满了,coping算法的垃圾收集从根集中扫描可达对象,并将每个可达对象复制到空闲面,这样使得对象所占内存之间没有空闲洞。空闲面也就变成了对象面,原来的对象面也就变成了空闲面,程序会在新的对象面中分配内存。

Stop-and-copy算法是一种典型的coping算法,它将堆分成对象面和空闲区域,在对象面与空闲区域面的切换过程中,程序会暂停执行。

2.5 generation算法(Generational Collector)

Stop-and-copy垃圾收集器必须复制所有的可达对象,这也无形的增加了程序的等待时间,coping算法的低效性也正是如此。程序设计中有这样的规律:多数对象存在的时间比较短,少数的对象存在时间比较长。Generation算法也正是根据这一规律,它将堆分成两个或多个,每个子堆作为对象的一代。由于多数对象存在的时间比较短,垃圾收集器将从做年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行没有被释放的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。

2.6 adaptive算法(Adaptive Collector)

特定的垃圾收集算法在特定的环境中会发挥更好的效果。Adaptive算法的垃圾收集器就是监视当前堆的使用情况,并选择合适的垃圾收集器。

由以上的种种算法可知,任何一种垃圾收集算法并没有被定性为解决某种内存回收问题,但是它们都是在解决这样的事情;

(1)发现无用的信息对象;

(2)回收被无用对象占用的内存空间,使该空间可以被其它程序再次使用。

所有的垃圾收集算法都是为了让不可达的对象被释放,让它们堆内存变得“干净”而更有效。

 

第三章            透视Java垃圾收集的运行

 

3.1命令行参数透视垃圾收集器的运行

命令行参数-verbosegc 可以查看Java的堆内存使用情况,堆内存也因此在用户面前透明化了,它的格式如下

java -verbosegc classfile

垃圾收集的算法很多,但使用System.gc()可以不管JVM使用的某种算法,将Java的无用对象释放,垃圾回收。通过下面的例子说明这一点:

class TestGC { public static void main(String[] args) { new TestGC(); System.gc(); System.runFinalization(); } }

可以看出,此例的目的是建立一个新的对象,它的classfile为TestGC,并且回收垃圾。程序编译后执行java -verbosegc TestGC ,查看堆内存使用情况。假设结果为:[Full GC 168K->97K(1984K), 0.0253873 secs]

则说明堆内存容量为1984K,168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有168K—97K=71K的对象的内存被回收了,收集所需要的时间为0.0253873秒。

3.2 finalize方法透视垃圾收集器的运行

JVM在提供很多垃圾收集算法的同时还提供了缺省机制来释放资源,这个方法就是finalize(),原型为:

protected void finalize() throws Throwable

在finalize()方法返回之后,对象被释放,垃圾收集开始执行。原型中的throws Throwable表示可以抛开任何异常类型,强制的回收。

finalize()是从Java里调用非Java方法的一种方式,并且通过它分配内存做一些具有C风格的事情,这里主要通过“固有方法”来进行。目前C和C++是唯一获得固有方法支持的语言,它们能够调用通过其他编程语言编写的子程序,故finalize()可以有效的调用任何东西。但是其他语言编写的程序所占的空间不能自动被释放,比如说调用C中的malloc()函数分配存储空间,在程序运行完毕后,就必须用free()函数释放该空间,从而造成内存“漏洞”的出现,所以说我们不能过多的使用finalize()。

一般地清除过程中,要清除一个对象,必须在清除地点调用一个清除方法,这与C++中的“破坏器”相对应,但是在C++中所有的对象都回被破坏。若在C++中创建一个本地对象,比如在堆栈中创建(在Java中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的创建这个对象的作用域的末尾进行;若对象是用new创建的(类似于Java),那么程序员就必须通过调用C++中的delete命令(Java中没有此命令)进行清除,同时也就调用了相应的破坏器。如果程序员没有调用delete命令,那么永远不会调用破坏器,对象的其他部分将不可能被清除,而且还会出现内存“漏洞”。

Java中所有的对象必须用new进行创建,删除对象则不必使用delete命令,因为垃圾收集机制会自动完成这项工作。将Java与C++在删除对象时作简单的比较,我们可以看出Java中的垃圾收集机制代替了C++中的破坏器,但是随着学习的深入,垃圾收集机制并不能完全的消除对破坏器(或以破坏器为代表的那种机制)的需要。

 

第四章            垃圾收集的特点和注意事项

 

4.1 垃圾收集的特点

通过以上分析,我们可以知道垃圾收集对堆内存的管理也正是堆内存对空间的管理,它们是同一的概念。垃圾收集有以下几个特点:

(1)垃圾收集的不可预知性:JVM中有很多垃圾收集算法,提供了不同的垃圾收集机制。它们有可能定时发生,有可能在CPU空闲时发生,也有可能在内存出现极限时发生,这些都与垃圾收集器的选择和设置有关。

(2)垃圾收集的精确性:垃圾收集器可以精确的标记出可达的对象,这是完全回收所有废弃对象的前提,这样很容易造成内存泄露;不仅如此,垃圾收集器也能够精确的定位对象之间的引用关系,这就为归并和复制算法提供了前提条件,这样所有的不可达对象都能够可靠的回收,所有可达对象被复制,重新分配,放在连续的内存空间,这样就能有效德尔防止内存地址的不连续。

(3) 垃圾收集器的种类各异,他们的算法也不一样,有只允许垃圾收集器运行的,也有允许垃圾收集器和应用程序同时运行的,还有通一时间垃圾收集多线程运行的。

(4)不同的JVM可能采用不同的垃圾收集,因此垃圾收集与具体的JVM以及JVM的内存模型有非常密切的关系。

(5)随着技术的发展,现代垃圾收集技术提供了许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。

4.2 垃圾收集的注意事项

根据以上特点,我们在使用垃圾收集时应注意以下事项:

(1)不可以假定垃圾收集发生的时间,因为垃圾收集随时可能发生,是一个未知数。

(2)Java垃圾收集的方法也是一个不确定的数,因为所有的垃圾收集方法只不过是向JVM发出这样一个申请,会不会去执行,我们并不知道,包括强行执行的垃圾收集方法——调用system.gc()。

(3)挑选适合自己的垃圾收集器,一般说来,如果系统没有特殊和苛刻的性能要求,可以采用JVM缺省选项。

(4)要有良好的编程习惯和严谨的编程态度,不要在编程时发生错误而导致内存泄露。

(5)尽早释放无用对象的引用,以免占据内存,提高内存的使用效率。

总之,不管使用这样的垃圾收集算法和怎样的垃圾收集机制,我们的目的都是唯一的,让有用的信息进入内存,让无用的信息离开内存,让内存保持在动态中,而且让内存有一个连续的空闲区,这样就提高了内存的使用效率。JVM中堆内存的分配和垃圾的处理,可以让我们更有效的利用资源,我们有必要理解Java的这一特性。

 

上一篇:Java堆外内存之突破JVM枷锁


下一篇:Spring与Struts2整合VS Spring与Spring MVC整合