javaGC与内存分配策略

在我们了解JavaGC之前,尝试思考3个问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

 

如何判定一个对象应该被回收?

  • 引用计数算法

很多教科书判断对象是否存活的算法是这样的,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不再被使用的。

但,

主流的Java虚拟机没有选用引用计数法来管理内存的。

思考一个问题,如果有对象objA和objB,赋值objA.instance==objB.instance,除此之外没有其他引用。

则,

它们的计数器都不为0,于是引用计数算法 无法通知GC回收。

 

  • 可达性分析算法

在主流的商用程序语言的主流视线中,都是通过可达性分析来判定对象是否存活的。

这个算法基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即对象不可达时,证明此对象不可用。

如图中obj567,虽然相互关联,但是它们到GCRoot是不可达的,所以它们会被判定为可回收对象。

javaGC与内存分配策略

  • 在Java语言中,可作为GC Roots的对象包括下面几种:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中JNI(Native方法)引用的对象

 

生存还是死亡

即使在可达性分析算法中不可达的对象,也不是“非死不可”。要真正宣告一个对象死亡,要经历两次标记过程。

当可达性分析发现没有GC Roots引用链,将会第一次标记,并且进行一次“此对象是否有必要执行finalize()”的筛选。当对象没有覆盖finalize()方法,或已经被虚拟机调用过该方法,将被虚拟机视为没有必要执行。

如果对象有必要执行finalize()方法,将会被放置在F-queue队列中,稍后会由一个Finalizer线程去触发执行一次,并不承诺等待运行结束。然后GC将对Fqueue对象进行第二次小规模标记,然后才会被回收。也就是说如果对象需要拯救自己,则在finalize()中引用一个对象建立关联即可。但finalize并不会被执行第二次,所以对象只可能有机会自救一次。

在任何Java相关书籍中,均不推荐使用finalize()方法,因为他是不稳定的,C程序妥协产物。

 

垃圾收集算法

  • 标记-清除算法

最基础的算法,分标记和清除两个阶段。存在两个问题:1.效率问题;2.空间问题。

javaGC与内存分配策略

如图所示:

标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后运行过程需要分配较大对象时,无法找到足够的内存,而不得不提前触发另一次垃圾收集动作。

 

  • 复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉。这样使每次都是对整个半区进行内存回收。分配时也不用考虑内存碎片等复杂情况。实现简单,运行高效。

这种算法的代价是将内存缩小为了原来的一半,代价较高。

javaGC与内存分配策略

现在的商业虚拟机都采用这种收集算法来回收新生代。

新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor。

大多数情况下,对象在新生代的Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC. 

javaGC与内存分配策略

HotSpot默认Eden和survivor比例是8:1,所以新生代内存分配中有90%的内存是被正常使用的。

当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。如果没有足够空间存放存活对象,则这些对象将直接通过分配担保进入老年代。

 

  • 标记-整理算法

根据老年代特点,标记-整理算法标记过程与清除算法一样,然后让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

javaGC与内存分配策略

  • 分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期不同,将内存划分为几块。一般是把Java堆分为新生代和老年代,就可以根据各个年代的特点采用适当的手机算法。

如新生代中,每次垃圾收集都有大批对象死去,少量存活,就选用复制算法。

而老年代中因为对象存活率搞没有额外空间对他进行分配担保,就需要使用“标记-清理”或“标记-整理”算法来进行回收。

 

GC中的新生代与老年代

  • 对象优先在Eden分配

大多数情况下,对象在新生代的Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。

  • 大对象直接进入老年代

指需要大量连续内存空间的Java对象,最典型的就是很长的字符串和数组。

  • 长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次MinorGC后仍然存活,并且能被Suevivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁。当增加到一定程度(通过MaxTenuringThreshold来设置,默认为15岁),就会被晋升到老年代中。

  • 动态对象年龄判定

如果在Survivor空间中年龄相同的所有对象大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入老年代,不需要等到MaxTenuringThreshold中要求的年龄。

  • 空间分配担保

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,MinorGC可以确保安全。

如果不成立,那么继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure不允许毛线,那这是要进行一次FullGC。