内蒙古呼伦贝尔职业技术学院 于世萍
Java垃圾回收器是java语言的根基,随着硬件设备和java语言的发展,多个版本的java虚拟机在不同条件的硬件设备下表现的性能有所差异,选择最合适的垃圾回收器可以提高基于java开发的系统的性能。本文分析了几款主流的垃圾回收器,并在学校主流的办公设备为实验环境测试,该方法可应用于OA系统开发时,配置合适时的垃圾回收器。
随着java版本的更新,java的内存回收算法也逐渐更新迭代,如今已经提供了多种的垃圾回收器供开发人员选择。面对不同性能的硬件设备和不同特征的场景,不同种类的垃圾回收器各有优劣之分。对于开发人员而言,如果不去分析其执行原理和算法特点,把自动内存管理当做黑匣子,只依赖最新版本的垃圾回收器,则不会带来好的效果。
为实现在不同性能的设备上选择最合适的垃圾回收器,本章将探究几种垃圾回收器的工作原理和优缺点。
引用计数法为每个分配的内存空间保存一个引用计数器,统计有多少个内存对象引用了这个空间。当引用计数器的值为0时,表示该内存空间不再使用,即可回收。虽然该方法实现简单,判定效率高,但是该方法无法解决循环引用的问题,由于这个致命缺陷,如今的垃圾回收器已经不再使用此类算法。
当有效的内存空间即将耗尽,则会停止程序的执行,转而进行标记和清除两项工作:首先从引用的根开始遍历,标记所有被引用过的对象,然后回收其余的没有被引用的对象。该方法虽然可以有效的解决循环引用的问题,但是该方法的执行效率不高,且执行期间需要暂停整个应用程序的正常执行,导致用户体验不好,且由于回收的空间不连续,产生了内存碎片,需要维护一个空闲列表来解决内存不连续的问题。
为解决标记清除算法会产生内存碎片的问题,提出了复制算法对标记清除算法作了以下改进:将内存空间分为两块,每次只使用一块内存。在执行垃圾回收时,未被回收的内存复制到另一块内存空间,并交换两块内存空间的角色。复制的时候内存空间会连续的排列好,由此可知,该方法解决了内存碎片的问题。不过,复制算法的有效性是建立在内存总空间较小,垃圾较多的情况下。如果需要垃圾回收的内存空间多,每次回收的垃圾少,则会占用大量的时间进行复制操作。
该算法是在标记清除算法上的改进,该方法执行的第一步与标记清除算法一样,标记所有被引用的对象,然后将存活的对象压缩到一部分内存区域,连续的排列,最后将清理所有存活对象之外的空间。这种操作解决了内存碎片的问题,相较于空闲列表法,该方法的开销显然更小。
根据上述的研究可知,垃圾回收器的算法各有优劣,如果可以根据内存空间大小不同和垃圾与存活空间的比例不同,将内存空间合理分配,在每一部分内存空间使用不同的算法,即可达到取长补短的效果。为此,大部分的垃圾回收器会将内存划分为新生代和老年代两块区域,其中新生代占用总内存空间的1/3,老年代占用2/3。首先新生成的垃圾被分配到新生代,新生代空间又分为一个eden区和两个survivor区域。由于全部的内存初始分配都在新生代进行,所以新生代的垃圾产生的较多,回收的频率较高,所以新生代使用复制算法;当新生代的内存空间已满切无法进行垃圾回收时,这些存活对象会被移动到老年代中。老年代的空间大,且很少会执行垃圾回收。老年代占用总内存空间的2/3,使用标记压缩算法,虽然该算法执行的时间较长,但是老年代只负责回收新生代区域无法回收的内存,且老年代的空间较大,很难全部耗尽。所以老年代执行垃圾回收的次数远少于新生代,执行标记压缩算法的次数也很少。
如今的java语言已经更新了十四个大版本,其中也更新了多个种类的垃圾回收器,以满足在硬件环境的性能逐渐进步的条件下,充分利用硬件设备的性能,以达到更快速的执行效率。目前主流的几款垃圾回收期分别包括如下内容。
Serial回收器是java语言第一代垃圾回收器,Serial回收器采用复制算法,应用与新生代;Serial Old回收器采用标记压缩算法,应用于老年代。整体次采用串行回收的方式。Serial是单线程的回收器,这不仅说明该回收器只有一个线程执行垃圾回收,还表示执行垃圾回收时,必须暂停所有工作线程,暂停工作线程期间,会造成应用程序停顿。但是Serial垃圾回收器省去多个线程来回切换的开销,对于限定单核cpu的环境来说,该方法仍然可以达到一定水平的工作效率。
ParNew回收器是在Serial回收器的基础上扩展了并行回收的能力。在底层上,两者共享了大部分的实现代码,ParNew回收器同样采用了复制算法和暂停用户线程的机制,只是在垃圾回收执行时,由多个线程执行任务。在多核cpu的环境下,相较于Serial回收器,ParNew回收器执行垃圾回收的速度要更快一些,用户线程被暂停的时间就会短一些。ParNew回收器负责回收新生代,可搭配Serial Old回收器或者CMS回收器负责老年代的垃圾回收工作。
CMS垃圾回收器可以是实现并发工作,即垃圾回收线程在执行某一部分操作时可以与用户线程同时执行。CMS负责老年代垃圾回收,基于标记清除算法实现。其工作流程可分为初始标记、并发标记、重新标记、并发清除四步骤。初始标记负责定位一些立即回收的内存空间,并发标记查找一些与初始标记过有关的且未被使用的内存。并发标记表示该步骤的垃圾回收线程与用户线程可以同时执行。重新标记负责修正并发标记期间由于垃圾回收线程与用户线程可以同时执行导致的错误标记。并发清除负责回收垃圾,该步骤的垃圾回收线程与用户线程任然可以同时执行。该方法可以实现垃圾回收线程与工作线程在某些时段的同时工作,降低了由于垃圾回收造成工作线程停顿的时间。但是其缺点也很明显:首先CMS采用标记清除算法,必然会造成大量内存碎片产生,无法合理的利用内存空间。其次,CMS在并发标记阶段会对垃圾对象产生误判,即该阶段被判断为不是垃圾的对象,用于工作线程也在执行的原因,这些对象有可能在标记结束后成为垃圾。总之CMS回收器在一次垃圾回收操作中无法准确的回收所有的垃圾,当垃圾增长过快时,该方法的性能明显下降。
Parallel scavenge回收器同样采用了复制算法,并行回收和暂停用户线程机制。与parnew回收器区别在于该收集器以吞吐量优先,并且会根据当前程序的运行情况,动态的调整内存分配情况,以达到吞吐量和延迟的可控制可协调。
G1收集器采用了与以上完全不同的分区方法,在垃圾回收期间,允许有多个线程同时工作,且拥有与应用程序并发执行的能力,不会完全阻塞工作线程。G1回收器将内存空间划分为多个区,每个区之间采用复制算法,整体采用标记压缩算法。由于引入分区机制,G1可以自由控制垃圾回收线程的暂停时长,缩小回收范围,优先回收价值最高的区,保证了在有限的时间内获取最高的回收效率。
为验证在学校经常使用中等配置的硬件设备与工作中主要使用的教务管理系统的场景下,以上介绍的几种垃圾回收器的优劣,本文设计的实验如下:实验运行两个线程,第一个线程负责每秒打印一次时间,第二个线程通过循环生成字节数组占用内存,当达到一定的占用量后回收的操作,来模拟用户使用教务管理系统。当第二个线程回收内存时,分别设置不同的垃圾回收器,来比较程序总共的运行时间,总运行时间=第一个线程的运行时间+垃圾回收占用的时间。总时间越短,则表示垃圾回收执行的时间越短,垃圾回收期的效果越好。表1为该实验的执行结果,由实验结果可知,Parallel scavenge回收器的总时间最短,性能最优。
表1 实验结果对比
总结:不同的垃圾回收器在不同的工作场景下各有优劣性,一些新推出的垃圾回收器适用于互联网B/S业务,对处理高并发的业务请求有更好的效果,然而对于配置不高的单核cpu和简单的教务管理系统,反而使用早期的垃圾回收器的效果更好。