现代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(分别叫from和to,命名为A和B),默认比例为8:1。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时(Minor GC,因为是对新生代进行垃圾回收,所以又称Young GC),扫描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和B Suvivor Space)。这么做主要是为了减少内存碎片的产生。年轻代的垃圾回收算法是复制算法,复制算法不会产生内存碎片。
我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收(Major GC,由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC)也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。
3)持久代(Perm Gen):全称是Permanent Generation space,持久代主要存放类定义、字节码和常量等很少会变更的信息。实际上,方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这与持久代存放的内容很相似,很多人更愿意把方法区称为“永久代”,本质上两者并不等价,仅仅是因为HotSpot虚拟机设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。所以虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫做Non-Heap(非堆),便于与Java堆区分开。这个内存区域用于存放Class和Meta的信息,Class在被 Load的时候被放入这个区域。因为Perm里存储的东西永远不会被JVM垃圾回收的,所以如果你的应用程序LOAD很多CLASS的话,就很可能出现PermGen space错误。默认大小为物理内存的1/64。
在JDK1.8之前,叫做持久代,JDK1.8及之后,叫做元空间(MetaSpace)。元空间其实是一个逻辑概念,逻辑上存在,物理上不存在。

这里写图片描述

不过总的说来,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调优的入门教程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值