【Jvm】垃圾回收

2019-06-25  本文已影响0人  w1992wishes

本篇结构:

一、判断对象已死

1.1、引用计数法

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

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

如下面代码所示:除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}

1.2、可达性分析算法

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

![ps:https://img.haomeiwen.com/i7017386/ad06836eda979158.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在Java中,可以作为GC Roots的对象包括下面几种:

二、java 四种引用

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

JDK1.2 以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。

2.1、强引用

Java 中默认声明的就是强引用,比如:

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,Jvm 也会直接抛出 OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,Jvm 就可以适时的回收对象了。

2.2、软引用(SoftReference)

软引用是用来描述一些非必需但仍有用的对象,在 Java 中用 java.lang.ref.SoftReference 类来表示。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

// 获取页面进行浏览
Browser prev = new Browser();
// 浏览完毕后置为软引用
SoftReference sr = new SoftReference(prev);
// 还没有被回收器回收,直接获取
if (sr.get() != null) {    
    rev = (Browser) sr.get();
} else {    
    // 由于内存吃紧,所以对软引用的对象回收了    
    // 重新构建    
    prev = new Browser();    
    sr = new SoftReference(prev);
}

2.3、弱引用(WeakReference)

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

private static void testWeakReference() {    
    for (int i = 0; i < 10; i++) {
        byte[] buff = new byte[1024 * 1024];
        WeakReference<byte[]> sr = new WeakReference<>(buff);
        list.add(sr);
    }   System.gc(); //主动通知垃圾回收
    for(int i=0; i < list.size(); i++){
        Object obj = ((WeakReference) list.get(i)).get();
        System.out.println(obj);
    }
}

输出 null。

2.4、虚引用(PhantomReference)

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

虚引用主要用来跟踪对象被垃圾回收的活动

public class PhantomReference<T> extends Reference<T> {
    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

2.5、引用队列(ReferenceQueue)

引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

与软引用、弱引用不同,虚引用必须和引用队列一起使用。

三、堆内存划分

堆内存被分成新生代和年老代两个部分。

3.1、新生代

新生代使用复制和标记-清除垃圾收集算法,研究表明,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from 和 Survivor to 三部分,其占新生代内存容量默认比例分别为8:1:1,其中 Survivor from 和 Survivor to 总有一个区域是空白,只有 Eden 和其中一个 Survivor 总共90%的新生代容量用于为新创建的对象分配内存,只有10%的 Survivor 内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的 Survivor 内存区域中,Eden 和非空白的 Survivor 进行标记-清理回收,两个 Survivor 区域是轮换的。

新生代中98%情况下空白 Survivor 都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白 Survivor 空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。

Java 虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。

使用 java 虚拟机 -Xmn 参数可以指定新生代内存大小。

3.2、年老代

年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。

Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

当新生代中无足够空间为对象创建分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代空间无法在扩展时,堆就会产生OutOfMemoryError异常。

java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。

3.3、堆内存示示意图

Hotspot JDK1.8 之前的堆内存示意图:

image

从上图可以看出堆内存的分为新生代、老年代和永久代。新生代又被进一步分为:Eden 区+Survior1 区+Survior2 区。值得注意的是,在 JDK 1.8 中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

四、垃圾收集算法

image

4.1、标记-清除算法

最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。

标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

image

4.2、复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

复制算法的缺点显而易见,可使用的内存降为原来一半。

image

4.3、标记-整理算法

标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。

标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。

复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。

image

4.4、分代收集算法

根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

当前虚拟机的垃圾手机都采用分代收集算法,比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

五、对象在堆中的分配

5.1、对象优先在 eden 区分配

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

新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

测试:

/**
 * -XX:+PrintGCDetails
 */
public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2;
        allocation1 = new byte[59800 * 1024];
        //allocation2 = new byte[900*1024];
    }
}

添加的 VM 参数:-XX:+PrintGCDetails 打印 GC 信息。

Heap
 PSYoungGen      total 76288K, used 65536K [0x000000076b400000, 0x0000000770900000, 0x00000007c0000000)
  eden space 65536K, 100% used [0x000000076b400000,0x000000076f400000,0x000000076f400000)
  from space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x0000000770900000)
  to   space 10752K, 0% used [0x000000076f400000,0x000000076f400000,0x000000076fe80000)
 ParOldGen       total 175104K, used 0K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
  object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1c00000,0x00000006cc700000)
 Metaspace       used 3261K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 392K, committed 512K, reserved 1048576K

可以看出eden区内存几乎已经被分配完全,假如再为 allocation2 分配内存会出现什么情况呢?

image

简单解释: 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。GC 期间虚拟机又发现 allocation1 无法存入Survior空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在eden区分配内存。

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

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

为什么要这样呢?

为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

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

虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

5.4、动态对象年龄判定

为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。

六、垃圾收集器

6.1、Serial 收集器

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择。

6.1.1、特点

它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法。

image

6.1.2、应用场景

依然是HotSpot在Client模式下默认的新生代收集器,也有优于其他收集器的地方:

6.1.3、设置参数

-XX:+UseSerialGC:添加该参数来显式的使用串行垃圾收集器;

6.2、ParNew 收集器

6.2.1、特点

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等)和Serial收集器完全一样。

新生代采用复制算法,老年代采用标记-整理算法。

image

6.2.2、应用场景

在Server模式下,ParNew 收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与 CMS 收集器配合工作;

但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

6.2.3、设置参数

6.2.4、为什么只有 ParNew 能与 CMS 收集器配合

CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;

因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码。

6.3、Parallel Scavenge 收集器

6.3.1、特点

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法。

image

6.3.2、应用场景

高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;

当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。

6.3.2、设置参数

Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量:

这是一种值得推荐的方式:

这也是Parallel Scavenge收集器与ParNew收集器一个重要区别。

6.4、Serial Old 收集器

Serial Old是 Serial 收集器的老年代版本;它主要有两大用途:一种用途是在JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

它主要:

6.5、Parallel Old 收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

JDK1.6 中才开始提供,特点有:

"-XX:+UseParallelOldGC":指定使用Parallel Old收集器。

6.6、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

image

6.6.1、特点

6.6.2、应用场景

6.6.3、设置参数

-XX:+UseConcMarkSweepGC:指定使用CMS收集器。

6.6.4、CMS 收集器 3 个明显的缺点

6.7、G1 收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

G1收集器运作过程:

6.7.1、特点

被视为 JDK1.7中HotSpot 虚拟机的一个重要进化特征。它具备特点:

6.7.2、应用场景

用来替换掉JDK1.5中的CMS收集器,在下面的情况时,使用G1可能比CMS好:

6.7.3、设置参数

七、参考资料:

上一篇下一篇

猜你喜欢

热点阅读