现代JVM内存管理方法及GC的实现和主要思路
谨以此文纪念已经辞世的C语言之父,Dennis Ritchie。无论世事如何变迁,无论日月如何更替,您的光辉成就都照耀着现代计算机技术发展之路。
提到现代JVM内存管理,就不能不提到一个意义深远的东西,C语言。C语言最为人诟病,但是也是C语言最让人神往的,就是它的内存管理机制。在C语言中,程序员可以自由的控制内存,自己决定内存里写0还是写1.所谓的数据类型转换,在C语言看来,不过就是内存里的几次复制以及排列位置的不同,仅此而已。
然而随着应用规模的不断增大,无论是盘根错节的对象耦合关系,还是巨大的内存使用量,都让开发人员麻爪。动辄几个GB的内存总量,动辄成千上万的内存对象数量,都不再是一个人乃至十个人可以控制的范围了。况且,百密一疏,只要有一点点内存泄露,随着时间的推移,都有可能变成无比的灾难。OOM之类的问题,在程序员眼里,早已经是家常便饭,谁还没溢出过内存呢,是吧。
的确是有高手可以控制好内存,但是不是所有人。那么,大规模团队化开发的时候,如何保证内存使用不出现问题呢?代码走查?人工校验?反复测试?这些能不能行的通先不谈,就算可行,巨大的工作量也可以让所有合同超期到下个世纪。于是有人提出了一个想法。可以不可以让一部分高手写出完善的内存管理模块,再加上一堆各式各样的类库和标准,最后构成一个庞大的运行时?
这一想法被无数语言团队采用。第一个实现的,就是James Gosling领导的Java团队。Java的目标是Write Once,Run Anywhere.估计他们在咖啡馆喝咖啡的时候一时写错了,应该是Debug Anywhere,这才符合现在的实际,呵呵。扯远了,我们回头看内存管理。
JVM提供了很多类库,封装了很多数据类型和常用工具类,作为自己的基本库来使用,比如java.lang包。举一个最简单的例子,来一句最简单的代码。int i = 5;
在C语言里,这句话申请了几个字节的内存,然后放了个5进去,Java也是这么搞的。只不过,C语言里申请了以后要自己管理,而Java你不用自己烦恼这个事情,虚拟机会帮你处理。它会判断何时需要,何时不需要。由此推开去,更加复杂的业务,比如连接数据库,读取文件,我们要做的只是调用类库而已,内存申请和释放都由虚拟机全盘接管,我们不用动一根手指头。
我们是爽了,虚拟机就头疼了。这么多对象,什么时候该销毁,什么时候该保持,什么时候要检查这些关系呢?在JVM里,这个事情有一个模块来做,也就是我们这片文章的主角,GC,Garbage Collection,垃圾回收。
假设我们是实现GC的程序员,那么我们要做什么呢?首先,负责分配内存,负责控制对象的持有计数,负责销毁内存对象,还得负责内存整理什么的。在Sun制定的JVM规范里,详细描述了GC部分要做的事情,这里就不赘述了,想看的话,请自行Google。
现有的JVM,主流的,分别是HotSpot和JRockit,主要研究对象也是这两个。这篇文章里,我们只研究HotSpot,也就是所谓的Sun JVM。目前阶段,Sun的GC方式主要有CMS和G1两种。考虑到效果和实际应用,这里只介绍CMS。
CMS,全称Concurrent Low Pause Collector,是JDK1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求较高的应用,并且预期这部分应用能够承受垃圾回收线程和应用线程共享处理器资源,且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。
JVM在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象,小部分是长周期的对象,对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频率垃圾回收以确保无谓地垃圾扫描检测。为解决这种矛盾,Sun JVM的内存管理采用分代的策略。
1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制到Old Gen。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和BSuvivor Space。这么做主要是为了减少内存碎片的产生。
我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
3)持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。
本图片来自 timyang的博客
不过总的说来,Java的GC算法感觉是业界最成熟的,目前很多其他语言或者框架也都支持GC了,但大多数都是只达到Java Serial gc这种层面,甚至分generation都未考虑。JDK7里面针对CMS进行了一种改进,会采用一种G1(Garbage-First Garbage Collection)的算法。实际上Garbage-First paper(PDF) 2004年已经出现。
另外,Sun Tech Days上Joey Shen讲的Improving Java Performance(PDF)也是一个很好的Java GC调优的入门教程。