java虚拟机JVM--java虚拟机垃圾的回收机制详解

时间:2021-11-27 10:27:46

前言

java语言的一个很大的特点,就是JVM可以自动回收垃圾,从而回收内存。这也是它相对于c的一个优势。
由此引出我们的思考:

  • 1.JVM如何判断这个对象是不是垃圾的?
  • 2.JVM是如何回收垃圾的?
  • 3.JVM什么时候调用GC回收?
  • 4.JVM的内存如何分配最高效?

我们一个一个问题的看,先看第一个问题。

一、JVM是如何判断这个对象到底是不是垃圾的

这里主要有两个算法去解决这个问题:1.引用计数法; 2.可达性分析算法

1.1 引用计数法

引用计数法就是给对象添加一个引用计数器, 当有一个其他对象应用了当前对象, 当前对象的计数器就 +1, 当别的对象不再引用当前对象, 引用计数器就 -1。如果引用计数器的值为0, 则就判定为垃圾对象。

这个算法简单且高效, 但是却有一个致命缺陷, 如果有两个对象相互引用了,而这两个对象对进程来说都不会再使用, 那么这两个对象都应该作为垃圾对象被回收掉, 但是由于他们的计数器都不为0, 因此不会被判定为垃圾对象。如果这两个对象都特别大, 就会造成严重的内存泄露。所以引出了另一个算法:可达性分析算法。

1.2 可达性分析算法

基本思想:通过一系列的被称为”GC Roots”的对象作为起点,从这些节点往下搜索, 搜索过的路径成为引用链,当一个对象到Gc Roots节点没有任何的引用链时, 则此对象就是无用的,可以被回收。
java虚拟机JVM--java虚拟机垃圾的回收机制详解

可以作为GC Roots的对象主要有以下四种:

  • 虚拟机栈(栈帧)中引用的对象:存在于栈中的对象, 说明正在或即将被使用
  • 本地方法栈中JNI引用的对象:存在于栈中,说明正在或即将被使用
  • 方法区中静态属性引用的对象:和静态生命周期相同
  • 方法区中常量引用的对象:和常量生命周期相同

    通过以上两种方法,就可以判断一个对象是不是可以被当做垃圾回收了。但是,这就结束了吗?可达性分析算法还不够完美。

1.3 强软弱虚

默认情况下, GC Roots可达的对象, 都不会被回收, 这种对象, 我们称之为强引用对象。

但是, 实际开发时,我们希望某个很占内存的对象,在内存足够时,我们希望它一直存在,这样我们需要用到时, 就可以很快的获取到数据。但是当内存不足时,又可以被回收,从而释放内存。针对这种情况,从JDK1.2起,就把引用分为了 强引用、软引用、弱引用、虚引用 四种:

  • 强引用:默认的引用类型,医药GC Roots可达, 就不会被回收;
  • 软引用:在将要发生内存溢出前,会被回收;
  • 弱引用:对象在下一次GC被回收
  • 虚引用:最弱的引用, 也被称为幽灵引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。为一个对象设置虚引用的唯一目的就是在这个对象被垃圾回收器回收时收到一个系统通知。虚引用必须和引用队列 (ReferenceQueue)联合使用 ,这样,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中,从而可以追踪这个对象的回收情况。

    虚引用代码示例:

    //创建一个字符串对象
    String temp = “虚引用测试字符串对象”;
    ReferenceQueue queue = new ReferenceQueue<>();
    //为这个字符串设置虚引用
    PhantomReference phReference = new PhantomReference(temp, queue);

    new Thread(new Runnable() {

    @Override
    public void run() {
        while (true) {
    
            //判断虚引用是否已经加入了队列,如果是,则说明字符串对象将要被回收了
            if (phReference.isEnqueued()) {
                //在字符串对象被回收前, do something
                break;
            }
        }
    }
    

    }).start();

    通过引用计数法+可达性分析+强软弱引, 可以很好的判断是不是垃圾对象了。我们接着看第二个问题~

二、JVM是如何回收垃圾的?

我们判断了那些对象时垃圾对象后, 接着就该回收了, 那我们是如何回收垃圾的呢, 这就不得不提到垃圾回收算法,主要有四种:标记-清除算法, 复制算法,标记整理算法,分代收集算法。

2.1 标记-清除算法(Mark-Sweep)

分为标记和清除两个阶段:首先标记所有需要被回收的对象,再标记完成后统一回收所有被标记的对象。

这个算法实现起来比较容易, 但是也有很大的缺陷,主要问题有两个:第一个是效率问题, 标记和清除两个多次的效率都不高;第二个是空间问题,标记清除后,会产生大量不连续的内存碎片, 内存碎片会导致后面需要给较大对象分配内存时, 发现没有足够的连续内存而不得不在内存没有使用完毕的情况下, 再一次发起垃圾收集的动作,导致频繁触发GC。

2.2 复制算法(Copying)

复制算法相对于标记清除算法, 效率高很多。复制算法将内存分为大小相等的两块,每次只是用其中一块,当第一块内存的空间用完了时, 触发GC回收, 然后将还存活的对象直接复制到第二块内存, 然后把第一块内存直接清空掉,这样,就不容易出现内存碎片了。

但是,复制算法的缺点也很明显:把能够使用的内存缩减为原来的一半。而且,该算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低, 反之,如果存活对象少,则效率出奇的高。(这也是为什么新生代中为什么采用了复制算法,新生代每次存活下来的对象都比较少, jVM内存模型将在下一篇博客提到)

2.3 标记整理算法(Mark-Compact)

为了解决复制算法的缺陷,充分利用内存空间, 又有了mark-compact算法, 该算法标记阶标记所有需要被回收的对象,,但是在完成标记之后不是直接清理可回收对象,而是将存活的对象都移向一端,然后清理掉端边界以外的所有内存,这样就只留下存活对象。

2.4 分代收集算法(Generational Collection)

分代收集算法是当前商用虚拟机锁采用的算法(即主流虚拟机都用这个), 它的核心思想是将堆区划分为老年代和新生代,老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以在不同代的采取不同的最适合的收集算法。相当于是前面几种算法的综合。

2.5 垃圾收集器

垃圾收集的算法有了,还需要有具体的实现,通常GC是一个低优先级的守护线程。 下面介绍HotSpot虚拟机提供的几种垃圾收集器:

1 Serial/Serial Old

最古老的收集器,是一个单线程收集器,用它进行垃圾回收时,必须暂停所有用户线程。Serial是针对新生代的收集器,采用Copying算法;而Serial Old是针对老生代的收集器,采用Mark-Compact算法。优点是简单高效,缺点是需要暂停用户线程。

2. ParNew

Seral/Serial Old的多线程版本,使用多个线程进行垃圾收集。

3. Parallel Scavenge

新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

4. Parallel Old

Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。

5. CMS

Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。

6. G1

G1(Garbage First)收集器技术的前沿成果,是面向服务端的收集器,能充分利用CPU和多核环境。是一款并行与并发收集器,它能够建立可预测的停顿时间模型。

三、JVM什么时候调用GC回收?

通过前面的分析,已经可以得出结论了:当堆区的内存不够用时, 或者没有连续内存可以容纳一个较大的对象时, 就会触发垃圾回收。

四、JVM的内存如何分配最高效?

还剩下最后一个问题,java内存是如何分配的, 怎么分配内存才是最高效的? 这就要介绍到新生代、老年代等概念了。本章的概念已经介绍得够多了,这个问题放到下一篇博客去分析, 下篇将结合本文讲述的垃圾回收机制,去介绍java虚拟机内存内存结构。

今天就到这里了,喜欢的朋友, 麻烦点个赞吧~~