JVM之内存管理机制(二)

前言

这一章节内容主要讲述的是垃圾收集器与内存分配策略的相关知识。

一、判断对象是否已经死亡

       在堆里面存放着几乎java世界所有的对象实例,垃圾收集器对对进行回收前,第一件事情就是要确认哪些对象还“存活”着,哪些对象已经“死去”。

1、引用计数法

      所谓引用计数法,即给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1;任何时刻计数器值为0的对象就不可能再被使用。

      客观的说,引用计数法实现简单、效率也高,在大多数情况下它是一个不错的算法。但主流的Java虚拟机并没有选择引用计数法来管理内存,其中最主要的原因就是它很难解决对象之间相互循环引用的问题。

2、可达性分析算法

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

      即使可达性分析算法中不可达的对象也并不是“非死不可”的,其至少还需经历两次标记过程。一次是可达性分析后没有与GC Roots相连的引用链,那么它会被第一次标记并进行筛选,筛选的条件是是否有必要执行finalize()方法。当有必要执行时,该对象会被放置到一个被称为F-Queue的队列中,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行。finalize()方式是对象逃脱死亡的最后一次机会,稍后GC将对该队列中的对象进行第二次小规模标记。如果对象要在finalize()中拯救自己,只要与引用链上的任何一个对象关联即可,那第二次标记会将其移出“即将回收”的集合,否则会被回收。

3、引用的分类

      JDK1.2之后,Java对引用的概念进行了扩充,分为以下4种,引用强度逐渐减弱:

      强引用

             强引用是类似于“Object obj = new Object()”这类的,只要引用还在,垃圾收集器就永远不会对其进行回收。

      软引用

             软引用是用来描述一些还有用但并非是所必须的对象。对于软引用连接的对象,在系统将要发生内存溢出时,将会把这        写对象列进回收范围之中进行第二次回收。如果回收后内存还不足,则抛出内存溢出异常。

      弱引用

             弱引用也是用来描述非必须对象的,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之        前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

      虚引用

             虚引用也成为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间够        长影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一        个系统通知。

二、垃圾回收算法

1、标记-清除(Mark-Sweep)算法

      顾名思义,该算法分为“标记”和“清除”两个阶段:首先标记处需要回收的对象,完成后统一回收。它的不足有二:一是效率问题,标记和清除两个过程的效率并不高;二是空间问题,标记清除后会产生大量的内存碎片。

JVM之内存管理机制(二)

2、复制(Copying)算法

       复制算法,它将内存按容量分为大小相等的两部分,每次只使用其中一块。当一块用完了,它将存活的对象复制到另外一块,然后再把刚才的那块清掉。这样就不存在内存碎片化问题,效率也较高,但造成内存利用率低。

       现在商业虚拟机都采用这种算法来回收新生代,但一般不会按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块survivor。Eden:Survivor = 8:1 。

JVM之内存管理机制(二)

3、标记-整理(Mark-Compact)算法

       标记-整理算法的标记步骤跟标志-清除算法的一样,但后续步骤不是对可回收对象进行直接清理,而是让所有存活的对象都想一端移动,然后清理掉便捷以外的内存。

JVM之内存管理机制(二)

4、分代收集(Genarational Collection)算法

       分代收集算法根据对象存活周期的不同将内存划分为几块。一般是把java对分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代使用复制算法,老年代使用标记-整理算法。

三、垃圾收集器

       如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里讨论的收集器基于JDK1.7 Update14之后的Hotspot的虚拟机。

1、Serial收集器

      该收集器时最基本、发展最悠久的垃圾收集器,曾经是虚拟机新生代收集的唯一选择。其是一个单线程收集器,它不仅仅一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至它收集结束。Serial对于运行在client模式下的虚拟机来说是一个很好的选择。

2、ParNew收集器

      ParNew收集器其实就是Serial收集器的多线程版本,它是运行在Server模式下虚拟机首选的新生代收集器。

3、Parallel Scavenge收集器

      其是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程的收集器。

4、Serial Old收集器

      此收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。

5、Parallel Old收集器

      该收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

6、CMS收集器

      CMS(Concurrent Mark Sweep)是一种以获取最短停顿时间为目标的收集器。

7、G1收集器

      G1(Garbage-First)收集器是当今收集器技术发展的最前沿成功之一,是一款面向服务端应用的垃圾收集器。

四、内存分配策略

1、对象优先在Eden分配

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

2、大对象直接进入老年代

        所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存复制。

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

       分代收集算法中,虚拟机为每个对象定义了一个对象年龄计数器。如果对象在Eden初始并经过一次Minor GC之后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor中,并且对象年龄设为1。对象在Survivor中每“熬过”一次Minor GC年龄就增加1,当年龄达到一定程度,默认15,就将被晋升到老年代。

4、动态对象年龄判定

       为了更好适应不同程序的内存情况,虚拟机并不用远要求对象的年龄必须达到设置的最大值才进入老年代。如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象就直接进入老年代。

5、空间分配担保

       在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否对大于新生代所有对象总空间,如果条件成立,则Minor GC确保是安全的。如果不成立,则虚拟机会查看是否设置允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管是有风险的;如果小于或者设置了不允许冒险,那这是则改为进行一次Full GC。