【深入理解JVM】(三)垃圾回收机制

1 概述

垃圾回收机制(Garbage COllection,GC),其实这不是JAVA语言所独有的技术,在1960年诞生于MIT的Lisp是第一门使用了GC的语言,在那个时候人们就开始思考了GC需要做的这三件事情:

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

2 GC是什么

我们的java程序在运行的时候都是在内存当中的,所以我们不能无限制的使用内存的空间,有些东西用完了没有其他的用处,也就形成了垃圾,我们要把这些垃圾清理掉,所以也就形成了垃圾回收机制。

3 GC的意义

由于有个垃圾回收机制,java中的额对象不在有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存;

内存泄露:指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有是也将其称为“对象游离”;

4 判断对象是否需要回收

4.1 引用计数法

堆中的每个对象实例都有一个引用计数器,点一个对象被创建时,且该对象实例分配给一个变量,该变量计数设置为1 ,当任何其他变量赋值为这个对象的引用时,计数加1 (a=b ,则b引用的对象实例计数器+1);但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1,任何引用计数器为0 的对象实例可以当做垃圾收集。 当一个对象的实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

这种方法实现简单,判定准确度也很高,效率也很高,综合的性能是很好的,但是有一点是它处理不了对象之间相互循环引用的问题。下面的代码,对象objA和objB互相引用,在我们不需要两个对象之后,这两个对象依旧不能被GC掉,因为此时他们的引用计数都不为0,于是无法通知GC收集回收他们。

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        testGc();
    }

    public static void testGc() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

        System.gc();
    }
}

GC打印结果

[GC (System.gc()) [PSYoungGen: 9339K->744K(76288K)] 9339K->752K(251392K), 0.0027130 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 744K->0K(76288K)] [ParOldGen: 8K->626K(175104K)] 752K->626K(251392K), [Metaspace: 3353K->3353K(1056768K)], 0.0065041 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b200000, 0x0000000770700000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b200000,0x000000076b3eb9d8,0x000000076f200000)
  from space 10752K, 0% used [0x000000076f200000,0x000000076f200000,0x000000076fc80000)
  to   space 10752K, 0% used [0x000000076fc80000,0x000000076fc80000,0x0000000770700000)
 ParOldGen       total 175104K, used 626K [0x00000006c1600000, 0x00000006cc100000, 0x000000076b200000)
  object space 175104K, 0% used [0x00000006c1600000,0x00000006c169cbb0,0x00000006cc100000)
 Metaspace       used 3370K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 374K, capacity 388K, committed 512K, reserved 1048576K

我们可以看出内存的变化:9339K->744K,这里我们可以看出来没有因为两个对象相互引用就没有回收,这也从侧面说明虚拟机并不是通过引用计数法来判断对象是否存活的。

4.2 可达性分析

可他行分析是我们通过扫描一系列的GC Roots来作为对象的起点,然后依次向下搜索,搜所走过的路径程为引用链路,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可达的。
在这里插入图片描述
由图里面我们可以看得出object5、object6、object7这三个对象相互有关联,但是依旧会被判定为可回收对象。

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

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(也就是一般说的Native方法)引用的对象

5 引用

在Java中,对引用(Reference)做了扩充,将引用分为了这四种:强引用、软引用、弱引用、虚引用这四种,这四种引用强度依次逐渐减弱。

  • 强引用
    是指创建一个对象并把这个对象赋给一个引用变量。 比如:Object object =new Object();这样的引用,只要引用还在,GC就不会进行回收被引用的对象
  • 软引用
    用来描述一些还有用但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围中进行第二次回收。如果这次回收之后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用
    用来描述非必需对象,但是它的强度比软引用更弱,被引用关联的对象只能生存到下一次GC发生之前。当GC时,无论当前内存是否足够,都会回收掉制备弱引用关联的对象
  • 虚引用
    也被称为由零引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象GC时收到一个系统通知

可以参考:JAVA的四种引用方式

6 自动垃圾收集

第一步:标记

在执行垃圾回收之前,我们需要先进行标记处哪些是我们需要进行回收的。

下图中橙色的部分就是被标记需要进行清除的。如果必须扫描系统中的所有对象,这将是一个非常耗时的过程。
在这里插入图片描述

第二步:清除和压缩

清楚:
删除掉未引用的对象,将引用的对象和指针保留到空闲空间。内存分配器保存对可用空间的引用列表,并在需要分配时搜索可用空间。
在这里插入图片描述
压缩:
为了进一步提高性能,除了删除未引用的对象之外,还可以压缩其余的引用对象。通过将引用的对象一起移动,这使得新的内存分配更容易、更快。内存分配器保存对可用空间起始的引用,然后按顺序分配内存。
在这里插入图片描述

为什么要分代垃圾收集

逐一标记和压缩 Java 虚拟机里的所有对象非常低效:分配的对象越多,垃圾回收需时就越久。不过,根据统计,大部分的对象,其实用没多久就不用了。(下图中,竖轴代表已分配的字节,而横轴代表程序运行时间)
在这里插入图片描述
存活(没被释放)的对象随运行时间越来越少。而图中左侧的那些峰值,也表明了大部分对象其实都挺短命的。

JVM分代

根据之前的规律,就可以用来提升 JVM 的效率了。方法是,把堆分成几个部分(就是所谓的分代),分别是新生代、老年代,以及永生代。
在这里插入图片描述
新对象会被分配在新生代内存。一旦新生代内存满了,就会开始对死掉的对象,进行所谓的小型垃圾回收过程。一片新生代内存里,死掉的越多,回收过程就越快;至于那些还活着的对象,此时就会老化,并最终老到进入老年代内存。

Stop the World 事件 —— 小型垃圾回收属于一种叫 “Stop the World” 的事件。在这种事件发生时,所有的程序线程都要暂停,直到事件完成(比如这里就是完成了所有回收工作)为止。

老年代用来保存长时间存活的对象。通常,设置一个阈值,当达到该年龄时,年轻代对象会被移动到老年代。最终老年代也会被回收。这个事件成为 Major GC。

Major GC 也会触发STW(Stop the World)。通常,Major GC会慢很多,因为它涉及到所有存活对象。所以,对于响应性的应用程序,应该尽量避免Major GC。还要注意,Major GC的STW的时长受年老代垃圾回收器类型的影响。

永久代包含JVM用于描述应用程序中类和方法的元数据。永久代是由JVM在运行时根据应用程序使用的类来填充的。此外,Java SE类库和方法也存储在这里。

如果JVM发现某些类不再需要,并且其他类可能需要空间,则这些类可能会被回收。
世代垃圾收集过程
现在你已经理解了为什么堆被分成不同的代,现在是时候看看这些空间是如何相互作用的。 后面的图片将介绍JVM中的对象分配和老化过程。

首先,将任何新对象分配给 eden 空间。 两个 survivor 空间都是空的。
在这里插入图片描述
当 eden 空间填满时,会触发轻微的垃圾收集。
在这里插入图片描述
引用的对象被移动到第一个 survivor 空间。 清除 eden 空间时,将删除未引用的对象。
在这里插入图片描述
在下一次Minor GC中,Eden区也会做同样的操作。删除未被引用的对象,并将被引用的对象移动到Survivor区。然而,这里,他们被移动到了第二个Survivor区(S1)。此外,第一个Survivor区(S0)中,在上一次Minor GC幸存的对象,会增加年龄,并被移动到S1中。待所有幸存对象都被移动到S1后,S0和Eden区都会被清空。注意,Survivor区中有了不同年龄的对象。
在这里插入图片描述
在下一次Minor GC中,会重复同样的操作。不过,这一次Survivor区会交换。被引用的对象移动到S0,。幸存的对象增加年龄。Eden区和S1被清空。
在这里插入图片描述
此幻灯片演示了 promotion。 在较小的GC之后,当老化的物体达到一定的年龄阈值(在该示例中为8)时,它们从年轻一代晋升到老一代。
在这里插入图片描述
随着较小的GC持续发生,物体将继续被推广到老一代空间。
在这里插入图片描述
所以这几乎涵盖了年轻一代的整个过程。 最终,将主要对老一代进行GC,清理并最终压缩该空间。
在这里插入图片描述

参考:

https://www.oschina.net/translate/java-gc
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html#t3

发布了129 篇原创文章 · 获赞 147 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq1515312832/article/details/96437064