深入理解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.软引用

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

使用 SoftReference 类来创建软引用。

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

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

使用 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。

猜你喜欢

转载自www.cnblogs.com/zengzhihua/p/9708528.html
今日推荐