深刻理解Java虚拟机:垃圾收集器与内存分配策略

 

3.2 对象已死吗

判断一个对象是否可被回收

  • 1.引用计数法

对堆中每一个对象添加一个引用计数器;当对象被引用时,引用计数器加1;当引用被置为空或离开做用域时,引用计数减1。简单但效率低,没法解决相互引用的问题。算法

public class ReferenceCountingGC {

  public Object instance = null;

  public static void main(String[] args) {
    ReferenceCountingGC objectA = new ReferenceCountingGC();
    ReferenceCountingGC objectB = new ReferenceCountingGC();
    objectA.instance = objectB;
    objectB.instance = objectA;
  }
}
  • 2.可达性分析算法(根搜索算法)

利用JVM维护的对象引用图,从根节点( GC Roots)开始遍历对象的引用图,同时标记遍历到的对象。当遍历结束后,未被标记的对象就是目前已不被使用的对象,能够被回收了。在Java语言中,可做为GC Roots的对象包括下面几种:数组

  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI引用的对象

引用类型

  • 1.强引用

被强引用关联的对象不会被回收。安全

使用 new 一个新对象的方式来建立强引用。多线程

Object obj = new Object();2.并发

  • 2.软引用

被软引用关联的对象只有在内存不够的状况下才会被回收。ide

使用 SoftReference 类来建立软引用。性能

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
  • 3.弱引用

被弱引用关联的对象必定会被回收,也就是说它只能存活到下一次垃圾回收发生以前。this

使用 WeakReference 类来实现弱引用。线程

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;  // 使对象只被弱引用关联
  • 4.虚引用

又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用取得一个对象。

为一个对象设置虚引用关联的惟一目的就是能在这个对象被回收时收到一个系统通知。

使用 PhantomReference 来实现虚引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;  // 使对象只被虚引用关联

finalize()

若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法:

  1. 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”。此对象将被回收。
  2. 若是这个对象被断定为有必要执行finalize()方法【重写了finalize()方法,且该方法没有被调用过】,那么这个对象将会放置在一个叫作F-Queue的队列之中,并在稍后由一个由虚拟机自动创建的、 低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样作的缘由是,若是一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的状况),将极可能会致使F-Queue队列中其余对象永久处于等待,甚至致使整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身——只要从新与引用链上的任何一个对象创建关联便可,譬如把本身(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;若是对象这时候尚未逃脱,那基本上它就真的被回收了。

/**
 * 此代码演示了2点:
 * 1.对象能够在被GC时自我拯救。
 * 2.这种自救的机会只有一次,由于一个对象的finalize()方法最多只会被系统自动调用一次
 */
public class FinalizeEscapeGC {

      public static FinalizeEscapeGC SAVE_HOOK = null;

      public void isAlive() {
            System.out.println("yes,i am still alive:)");
      }

      @Override
      protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("finalize mehtod executed!");
            FinalizeEscapeGC.SAVE_HOOK = this;
      }

      public static void main(String[] args) throws InterruptedException {
            SAVE_HOOK = new FinalizeEscapeGC();
            // 对象第一次成功拯救本身
            SAVE_HOOK = null;
            System.gc();
            // 由于finalize方法优先级很低,因此暂停0.5秒以等待它
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                  SAVE_HOOK.isAlive();
            } else {
                  System.out.println("no,i am dead:(");
            }
            // 下面这段代码与上面的彻底相同,可是此次自救却失败了
            SAVE_HOOK = null;
            System.gc();
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                  SAVE_HOOK.isAlive();
            } else {
                  System.out.println("no,i am dead:(");
            }
      }
}

Console:
finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(

回收方法区

永久代的垃圾收集主要回收两部份内容:废弃常量和无用的类。类须要同时知足下面3个条件才能算是“无用的类”:

  1. 该类全部的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

 

3.3. 垃圾收集算法

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

这是最基础的垃圾回收算法,之因此说它是最基础的是由于它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出全部须要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

2.Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

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

该算法标记阶段和Mark-Sweep同样,可是在完成标记以后,它不是直接清理可回收对象,而是将存活对象都向一端移动,而后清理掉端边界之外的内存。虽然能够大大简化消除碎片的工做,可是每次处理都会带来性能的损失。

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。

  • 新生代使用:复制算法
  • 老年代使用:标记 - 清除 或 标记 - 整理 算法

 

3.5 垃圾收集器

1.Serial

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,而且在它进行垃圾收集时,必须暂停全部用户线程,直到垃圾收集结束。Serial收集器是针对新生代的收集器,采用的是Copying算法,它简单高效,对于限定单个CPU环境来讲,没有线程交互的开销,能够得到最高的单线程垃圾收集效率,所以Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

2.ParNew

ParNew收集器是Serial收集器的多线程版本,也使用Copying算法 ,除了使用多个线程进行垃圾收集外,其他的行为和Serial收集器彻底同样。ParNew收集器默认开启和CPU数目相同的线程数,能够经过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。ParNew垃圾收集器是不少java虚拟机运行在Server模式下新生代的默认垃圾收集器。

3.Parallel Scavenge

Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不须要暂停其余用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不一样,它主要是为了达到一个可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。高吞吐量能够最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适用于在后台运算而不须要太多交互的任务。

4.Serial Old

Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优势是实现简单高效,可是缺点是会给用户带来停顿。在Server模式下,主要有两个用途:a.在JDK1.5以前版本中与新生代的Parallel Scavenge收集器搭配使用。b.做为年老代中使用CMS收集器的后备垃圾收集方案。

5.Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程的Mark-Compact算法。

6.CMS

CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

7.G1

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。采用Mark-Compact算法,所以它是一款并行与并发收集器,而且它能创建可预测的停顿时间模型。

 

3.6 内存分配与回收策略

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动做,由于Java对象大多都具有朝生夕灭的特性,因此Minor GC很是频繁,通常回收速度也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,常常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 Major GC的速度通常会比Minor GC慢10倍以上。

内存分配策略

  • 1.对象优先在Eden区分配

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

  • 2.大对象直接在老年代中分配

大对象直接在老年代中分配,JVM提供参数设置 –XX:PretenureSizeThreshold,当对象须要的空间大于该值时,直接在老年代分配(目的:避免在Eden和Survivor区之间发生大量的copy,新生代GC采用复制算法)。

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

长期存活的对象将进入老年代,若是对象在Eden出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC,年龄就增长1岁,当它的年龄增长到必定程度(默认为15岁)时,就会被晋升到老年代中。-XX:MaxTenuringThreshold 用来定义年龄的阈值

  • 4.动态对象年龄断定

虚拟机并非永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄≥该年龄的对象就能够直接进入老年代。

  • 5.空间分配担保

在发生 Minor GC 以前,虚拟机先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是条件成立的话,那么 Minor GC 能够确认是安全的。若是不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否容许担保失败,若是容许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,将尝试着进行一次 Minor GC;若是小于,或者 HandlePromotionFailure 设置不容许冒险,那么就要进行一次 Full GC。

GC触发条件

Minor GC:其触发条件很是简单,当 Eden 空间满时,就将触发一次 Minor GC。

Full GC:相对复杂,有如下条件:

  • 1.调用 System.gc()

只是建议虚拟机执行 Full GC,可是虚拟机不必定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

  • 2.老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了不以上缘由引发的 Full GC,应当尽可能不要建立过大的对象以及数组。除此以外,能够经过 -Xmn 虚拟机参数调大新生代的大小,让对象尽可能在新生代被回收掉,不进入老年代。还能够经过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  • 3.空间分配担保失败

使用复制算法的 Minor GC 须要老年代的内存空间做担保,若是担保失败会执行一次 Full GC。

  • 4.JDK 1.7 及之前的永久代空间不足

在 JDK 1.7 及之前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的状况下也会执行 Full GC。若是通过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上缘由引发的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

  • 5.Concurrent Mode Failure

执行 CMS GC 的过程当中同时有对象要放入老年代,而此时老年代空间不足(多是 GC 过程当中浮动垃圾过多致使暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

相关文章
相关标签/搜索