关于本文的说明,在jdk8版本中,heap中的持久带已经被甲骨文抛弃了,类的元信息存储也移动到native heap中,增加了一些新的tag(具体的甲骨文文档有说),本文针对的heap结构还保留持久带,不针对最新jdk,本文主要是内存分配以及GC工作原理的分析,我们学习jvm,一方面在享受java关怀的同时也理解下jvm背着我们开发者干了什么,另一方面也感悟一下这种运行内核设计的思想,甚至可以说艺术。所以,我个人觉得版本的差异对本文分析的影响不大。大家注意下jdk8heap结构的改动即可。
说到GC,首先要说到GC实现一个很重要的角色,那就是jvm(java虚拟机),jvm作为java的一个托管平台在java代码的运行过程中提供了诸多的性能优化,确保代码的高效率执行,至于怎么优化就暂且不关注这个问题,我们先来看看oracle对于jvm的架构说明:
在这个图里面我们可以看到,我们用java写的各种类都是通过一个类加载器交换到jvm,jvm的中间层包括java方法存储区,以及java中堆(heap),在java代码中new分配内存的对象都会被托管在heap中,再经过jvm底层的jit编译器以及执行引擎的优化后变成(mapping)到内存空间,中间层还包括java线程跟一些本地线程等资源的运行空间,最后看到我们的gc是放在jvm的最底层。
说说gc的本质到底是什么吧,其实就是一个watcher进程,开发过Android的可以在debug模式下看到一个gc线程,gc监视的目标就是heap中对象,在jvm中对于heap有一个区域划分模型,我们先来看看(非jdk8版本结构):
刚被new出来的对象都会存在于heap中Eden区,但这个区域是有自己的容量的,满了gc就会开始工作了,gc主要是检查哪些对象没有被引用了(沿着引用链寻找,其实是引用链图的广度优先遍历),或者判断以后也不会被引用,这些废物就会被gc标记,成为marking过程,如下图:
marking之后(在上图绿色的代表还被引用着的对象,粉红色代表没有被引用,以及推断以后也没有引用可能性的对象,变为粉红色代表已经被标记),之后gc开始进入sweep/compact阶段,先deletion在进行内存整合,这里就把两个合起来吧,然后Eden区的对象就变成这样:
在mark-sweep方式下,多出来的内存空间会以引用的形式添加到jvm内部的一个内存链表中,下次new一个对象需要内存的分配时取出一个大小适合的内存空间的引用(链表的形式允许内存空间的不连续分布)。
当然这种方式可能会导致内存碎片。碎片化是比较可怕的,全是1k的离散碎片我分配2k它都没法玩了,所以也就存在另一种compact方式,会进行内存的整合(结果就如上图),但这意味着内存的移位,对于给jvm划分较大的heap情况下这样也够呛,所以两种方式的使用还是看情况的。
在Eden区的垃圾回收成为Minor garbage collections这个名字,这个回收在jvm中被优化得很厉害,基本算上是gc最快速度的垃圾回收了,而且这个举动还特别的伟大,得整个jvm上下都得为她让道,叫做Stop the World Event,观察过android dalvik的log输出的伙伴可能想起来经常会看到gc的垃圾回收提示,free了多少多少k的内存,其实那个时候全世界只有gc在工作,所以她的效率不高那就是一个很欠揍的行为了。
好咯,Eden区一旦满了gc就会被触发,这个时候没有被引用的对象就像上面描述的过程一样被标记然后再被销毁咯,被引用的对象这时候可谓幸存者了,所以它们被移到幸存者区,即s0区,这个时候的Eden区是空的,在s0,对象的生命计数还是在进行的,经过一轮major gc的生死历练就得进入old generation区了.
然而jvm有个有趣的地方就在于s0与s1两个区是交替使用的,在下一轮gc被触发时,引用对象会被挪到s1,s0的对象也都被全部转移到s1,下一轮再转移到s0,就这么来回折腾,反正每次都保证Eden区跟其中一个s区都是空的,另外一个s区存在着不同年龄的对象(在对象的生命周期中,就这么叫吧),当到达一定阙值时,对象就会转移到下一个generation区,长大了嘛,肯定需要更多的发展空间,这个区的gc运行速度会比在Eden慢一点。
当然,这个区的垃圾回收也是一个Stop the World events,没了引用就得被清除掉,这个过程叫做major garbage collection,最终来说,这个区的对象都要被回收的,因此这个过程要缓慢一很多。
至于我们的用的弱引用,软引用,在内存告罄时也是必须为heap作出贡献的,GC就喜欢欺负弱的引用了,其实GC在mark完了之后还会给标记对象一个续关的机会,不过得检查它是否重写了finalize方法,如果在这个方法中它能及时地将自己添加到引用链图,也就是说自己要让自己在临死前突然变得有价值,这样GC才能大发慈悲的赦免它。
另外,survivor区如果我们指定jvm分配的太小,有时候我们对象分配的太疯狂,它也无法容纳太多的幸存者,所以也会在预测计算老生带空间不够时导致full gc进行空间担保,为无处可去的对象敞开温暖的怀抱。
ok,现在就详细分析了一下gc的工作过程以及对象在heap中的一生,本次文章就到这里结束了,关于gc的标记决策,以后会再来一篇文章进行说明,主要是分析如何判断对象有没有被引用,毕竟java又不像c++一样存在智能指针,也比较非主流的不搞引用计数,本文结束。。。