GC垃圾回收

文章目录

  • GC垃圾回收
  • 一、垃圾回收概述
    • 1、什么是垃圾?
    • 2、什么是垃圾回收?
    • 3、为什么需要垃圾回收?
    • 4、Java垃圾回收机制
    • 5、Java垃圾回收区域
  • 二、对象存活判断
    • 1、引用计数算法(Python)
      • 1)基本思路
      • 2)优缺点
      • 3)循环引用
      • 4)小结
    • 2、可达性分析算法(Java)
      • 1)基本思路
      • 2)GC Roots
    • 3、finalization 机制
      • 1)特点
      • 2)对象的三种状态
      • 3)回收对象的2次标记
      • 4)举例演示
  • 三、垃圾回收算法(基础)
    • 1、标记-清除算法
      • 1)核心思想
      • 2)优缺点
      • 3)空闲列表(Free List)
    • 2、复制算法
      • 1)核心思想
      • 2)优缺点
    • 3、标记-压缩算法
      • 1)核心思想
      • 2)优缺点
      • 3)指针碰撞(Bump The Point)
    • 4、小结
  • 四、垃圾回收算法(衍生)
    • 1、分代收集算法
    • 2、增量收集算法
    • 3、分区算法
  • 五、垃圾回收相关概念
    • 1、System.gc()
    • 2、内存溢出(OOM)
      • 0)概述
      • 1)栈的OOM
      • 2)堆的OOM
      • 3)方法区的OOM
      • 4)直接内存的OOM
    • 3、内存泄露(Memory Leak)
    • 4、STW(Stop The World)
    • 5、垃圾回收的并行与并发
    • 6、安全点 与 安全区域
      • 1)安全点(Safe Point)
      • 2)中断
      • 3)安全区域(Safe Resion)
  • 六、几种引用类型
    • 1、强引用 - 不回收
    • 2、软引用 - 内存不足即回收
      • 1)示例:软引用
      • 2)示例:软引用 & 引用队列
      • 3)应用场景举例
    • 3、弱引用 - 发现即回收
      • 1)示例:弱引用
      • 2)示例:弱引用 & 引用队列
    • 4、虚引用 - 对象回收追踪
      • 1)示例:虚引用
    • 5、终结器引用
    • 6、小结

GC垃圾回收

一、垃圾回收概述

1、什么是垃圾?

An object is considered garbage when it can no longer be reached from any pointer in the running program

如果一个对象在运行程序中没有任何指针指向它,那这个对象就是需要被回收的垃圾。

2、什么是垃圾回收?

垃圾回收,简称GC(Garbage Collect)

  • 释放“垃圾”占用的空间,防止内存溢出。
  • 清除整理内存碎片,以便JVM将整理出的内存分配给新的对象。

3、为什么需要垃圾回收?

如果不及时对内存中的垃圾进行清理,那么垃圾对象所占的内存空间就不会释放,随着不断地分配内存空间而不进行回收,内存迟早都会被消耗完,从而导致内存溢出等问题,影响程序的正常使用。

因此,垃圾回收几乎已经成为了现代开发语言必备的标准。除了 Java 以外,C#、Python、Ruby 等语言都使用了自动垃圾回收的思想

4、Java垃圾回收机制

自动内存管理机制,无需开发人员手动参与内存的分配与回收。

  • 将程序员从繁重的内存管理中释放出来,可以专注于业务开发。
  • 如果没有垃圾回收,java 也会和 c 一样,各种悬垂指针,野指针,泄露问题让你头疼不已。

但是垃圾收集不是万能的,当垃圾收集成为系统达到更高并发量的瓶颈时,就需要我们人为介入进行监控与调优。

5、Java垃圾回收区域

GC 主要关注于 方法区 中的垃圾收集。其中,堆空间是GC的重点区域

  • 频繁收集的:堆空间的 Young 区
  • 较少收集的:堆空间的 Old 区
  • 基本不收集:方法区

二、对象存活判断

在堆里存放着几乎所有的 Java 对象实例,在执行垃圾回收之前,首先要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间。

简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

判断对象是否死亡,主要有两种算法:引用计数法可达性分析算法(主流的商业虚拟机基本使用后者)

1、引用计数算法(Python)

1)基本思路

引用计数算法(Reference Counting)比较简单,即每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。

  • 对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;
  • 当引用失效/释放时,引用计数器就减 1。
  • 只要对象 A 的引用计数器的值为 0,即表示对象 A 没有被使用,可进行回收。

2)优缺点

优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。

缺点:

  • 需要单独的字段存储计数器,增加了空间的开销
  • 每次赋值都需要更新计数器,增加了时间的开销
  • 无法处理循环引用,这是一条致命缺陷,导致在 Java 的垃圾回收器中没有使用这类算法。

3)循环引用

当 p 的指针断开时,内部的引用形成一个循环,这就是循环引用(rc表示reference count)

GC垃圾回收_第1张图片

4)小结

Java 并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。

但是引用计数算法仍然是很多语言的资源回收选择,例如 Python。Python 如何解决循环引用?

  • 手动解除:在合适的时机,解除引用关系。
  • 使用弱引用 weakref,weakref 是 Python 提供的标准库,旨在解决循环引用。

具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。

2、可达性分析算法(Java)

可达性分析算法,也叫追踪性垃圾收集(Tracing Garbage Collection),不仅同样具备实现简单和执行高效等特点,还解决了在引用计数算法中循环引用的问题,防止内存泄漏的发生。是 Java、C#选择的垃圾收集算法。

1)基本思路

以根对象集合(GC Roots)为起始点,按照从上至下的方式,搜索被 GC Roots 所连接的目标对象是否可达。

  • 内存中的存活对象都会被GC Roots直接或间接的连接着,搜索所走过的路径称为引用链(Reference Chain)。
  • 当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可达的,会被标记为可回收的对象。

如下图:对象 5,6,7 虽然相互关联,但是他们到GC Roots是不可达的,所以它们会被判定为不可达对象。

GC垃圾回收_第2张图片

注意:不可达对象 ≠ 可回收对象。不可达对象 变为 可回收对象,至少要经过两次标记过程。(具体见 finalization )

2)GC Roots

在 Java 语言中,GC Roots 包括以下几类元素:

  • 虚拟机栈中

    • 栈帧中的 局部变量表 引用的对象。例如:各个线程被调用的方法中使用到的参数、局部变量等。
  • 本地方法栈中

    • 本地方法 引用的对象
  • 方法区中

    • 静态变量和常量。
  • 同步锁 synchronized 持有的对象

  • Java 虚拟机内部的引用。

    • 基本数据类型对应的 Class 对象
    • 一些常驻的异常对象(如:NullPointerException、OutOfMemoryError)
    • 系统类加载器。
    • 反映 java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

小技巧

GC Roots 采用栈方式存放变量和指针。

所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个 Root。

注意点1

除了这些固定的 GC Roots 集合以外,根据 垃圾收集器 以及 回收的内存区域 的不同,还可以有其他对象“临时性”地加入,共同构成完整 GC Roots 集合。比如:分代收集 和 局部回收(PartialGC)。

如果只针对 Java 堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入 GCRoots 集合中去考虑,才能保证可达性分析的准确性。

例如:只回收Java 堆中的年轻代时,需要考虑年轻代中的对象是否被老年代中的对象引用,此时就要将老年代对象加入GC Roots考虑。

注意点2

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致 GC 进行时必须“stop The World”的一个重要原因。

即使是号称(几乎)不会发生停顿的CMS收集器,枚举根节点时,也是必须要停顿的。

3、finalization 机制

Java 语言提供了对象终止(finalization)机制,来允许开发人员提供对象被回收之前的自定义处理逻辑

  • 垃圾回收一个对象之前,总会先调用这个对象的 finalize() 方法。

前提:需要重写 Object 的 finalize() 方法,否则不会调用。

1)特点

  • 回收一个对象之前,总会先调用这个对象的 finalize() 方法。
  • finalize() 方法可能会导致对象复活。(不可达 --> 可达)
  • finalize() 方法只会被调用一次。(手动调用除外)
  • finalize()方法的执行完全由 GC 线程决定,如果不发生 GC,finalize()方法将没有机会执行。(手动调用除外)
  • finalize() 方法一般进行一些资源释放和清理,用于在对象被回收时进行资源释放。(如关闭套接字、连接等)

虽然finalize()方法可以手动调用,但是并不推荐,应该交给垃圾回收机制调用。

2)对象的三种状态

由于 finalize()方法的存在,虚拟机中的对象一般处于3种可能的状态

  1. 可触及的:对象是可达的。(从 GC Roots 可以到达这个对象)
  2. 可复活的:对象不可达,但是对象有可能在 finalize() 中“复活”(即重新变为可达的)
  3. 不可触及的:对象不可达,而且已经调用过finalize() 方法且没有“复活”(finalize()只会被调用一次)

对象只有处在「不可触及的」状态时,才可以被回收。可以说**finalize()方法是对象逃脱死亡的最后机会**。

3)回收对象的2次标记

判定一个对象 obj 是否可回收,至少要经历两次标记过程

  1. 如果 obj 到 GC Roots 没有引用链,则进行第一次标记。(可复活的)

  2. 判断此对象是否有必要执行 finalize()方法。

    • 没有重写 finalize()方法,则进行第二次标记。(不可触及的)

    • 已经调过 finalize()方法,则进行第二次标记。(不可触及的)

    • 重写了 finalize()方法 并且 还未执行过,则

      将 obj 插入到 F-Queue 队列中,由一个 JVM 自动创建的、低优先级的 Finalizer 线程执行其 finalize()方法。

  3. 执行 finalize()方法时,判断对象是否复活(不可达 --> 可达)。

    • 如果 obj 在 finalize()方法中复活了,则移出“即将回收”集合(可触及的)
      • 后续 obj 如果再次不可达,则直接变为不可触及的,不会再复活。(只会执行一次 finalize()方法)
    • 如果 obj 在 finalize()方法中没有复活,则进行第二次标记。(不可触及的)

如果 obj 进行了两次标记,变为不可触及的,则可以被回收。

4)举例演示

public class CanReliveObj {
    // 类变量,属于GC Roots的一部分
    public static CanReliveObj canReliveObj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        canReliveObj = this;
    }

    public static void main(String[] args) throws InterruptedException {
        canReliveObj = new CanReliveObj();
        
        System.out.println("-----------------第一次gc操作------------");
        canReliveObj = null;
        System.gc();
        // 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
        Thread.sleep(2000);
        if (canReliveObj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }

        System.out.println("-----------------第二次gc操作------------");
        canReliveObj = null;
        System.gc();
        // 下面代码和上面代码是一样的,但是 canReliveObj却自救失败了
        Thread.sleep(2000);
        if (canReliveObj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }

    }
}
-----------------第一次gc操作------------
调用当前类重写的finalize()方法
obj is still alive
-----------------第二次gc操作------------
obj is dead

第一次 GC 后,对象通过finalize()方法复活了;但finalize()方法只会被调用一次,所以第二次该对象被 GC 了。

三、垃圾回收算法(基础)

当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。

1、标记-清除算法

1)核心思想

当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作:

  • 标记:从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象
  • 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收。

GC垃圾回收_第3张图片

何为清除?

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。

2)优缺点

【缺点】

  • 标记和清除的效率都不高。

  • 在进行 GC 的时候,需要停止整个应用程序,用户体验较差。

  • 对象被回收之后,会产生大量不连续的内存碎片,需要维护一个「空闲列表」。

    当需要分配较大对象时,由于找不到合适的空闲内存而不得不再次触发垃圾回收动作。

3)空闲列表(Free List)

  • 列表记录了哪些内存块是可用的。
  • 分配的时候,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。

2、复制算法

1)核心思想

  1. 将内存划分为大小相等的两部分,每次只使用其中一半
  2. 当第一块内存用完了,就把存活的对象复制到另一块内存上,然后清除剩余可回收的对象

GC垃圾回收_第4张图片

2)优缺点

【优点】

  • 不存在「标记-清除算法」的内存碎片问题。
  • 只需要移动堆顶指针,按顺序分配内存即可,简单高效。

【缺点】

  • 浪费了一半的内存。

  • 在对象存活率达到一定程度时,复制的开销是不可忽视的。

    极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。

3、标记-压缩算法

「复制算法」的高效性建立在存活对象少、垃圾对象多的前提下的,适合新生代,但不适合老年代这种存活对象多的。

「标记-清除算法」的确可以应用在老年代中,但是该算法不仅执行效率低,而且还会产生内存碎片。

「标记-压缩算法」在「标记-清除算法」的基础上进行改进,更加适合老年代。

1)核心思想

  • 标记:与「标记-清除算法」一样,均是遍历GC Roots,然后将存活的对象进行标记。
  • 整理:将所有存活的对象按内存地址次序依次排列,然后将末端内存地址之后的内存全部回收。

GC垃圾回收_第5张图片

2)优缺点

【优点】

  • 不存在「标记-清除算法」的内存碎片问题。
  • 不存在「复制算法」内存减半的高额代价。
  • 给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。

【缺点】

  • 从效率上来说,「标记-压缩算法」要低于「复制算法」。
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
  • 移动过程中,需要全程暂停用户应用程序。即:STW

3)指针碰撞(Bump The Point)

  • 所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器。
  • 分配内存就是把指针移动一段与对象大小相等的距离。

4、小结

标记-清除 标记-压缩 复制
速率 中等 最慢 最快
空间开销 少(有内存碎片) 少(无内存碎片) 通常需要存活对象的 2 倍空间(无内存碎片)
移动对象

效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。

而为了尽量兼顾上面提到的三个指标,「标记-压缩算法」相对来说更平滑一些,但是效率上不尽如人意,它比「复制算法」多了一个标记的阶段,比「标记-清除算法」多了一个整理内存的阶段。

四、垃圾回收算法(衍生)

上一章说到的3种算法,都具有自己独特的优势和特点,没有一种算法可以完全替代其他算法。

1、分代收集算法

不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

现代商业虚拟机中的 GC 大多都采用 分代收集算法。

  • 新生代中:区域较小,对象的存活率较低,回收频繁。所以选用「复制算法」。
  • 老年代中,区域较大,对象的存活率较高,回收相对不是很频繁,所以使用「标记-清除」或「标记-压缩」算法。

2、增量收集算法

之前说到的算法,在垃圾回收过程中,应用软件将处于一种 Stop the World 的状态。在 STW 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果GC时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性

为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting)算法的诞生。

【基本思想】

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成

总的来说,增量收集算法仍然是基于3种基础的算法,只是通过对线程间冲突的妥善处理,允许GC线程以分阶段的方式完成工作。

【缺点】

由于在垃圾回收过程中,间断性地还执行了应用程序代码,虽然能减少系统的停顿时间,但是线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量下降

3、分区算法

一般来说,在相同条件下,堆空间越大,一次 GC 所需要的时间就越长,产生的停顿也越长。

为了更好地控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。

每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

GC垃圾回收_第6张图片

分代收集算法根据对象的生命周期将对空间划分为两个部分进行回收,而分区算法则将整个堆空间划分成连续的不同小区间。

五、垃圾回收相关概念

1、System.gc()

默认情况下,通过调用 System.gc()显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

然而 System.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效)

public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc(); // 提醒JVM的垃圾回收器执行gc,但是不确定是否马上执行gc
        // System.runFinalization();  // 强制执行使用引用的对象的finalize()方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重写了finalize()");
    }
}

上述代码中,finalize()不一定会执行,说明了System.gc() 无法保证对垃圾收集器的调用

下面看一下手动调用 System.gc() 的几个案例:

/* -XX:+PrintGCDetails */
public class LocalVarGC {
    public static void main(String[] args) {
        LocalVarGC gcClass = new LocalVarGC();
        gcClass.localVarGC2();
    }

    public void localVarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024]; // 10m
        System.gc(); // 新生代10m -> 老年代10m
    }

    public void localVarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc(); // 10m被回收
    }

    public void localVarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc(); // 新生代10m -> 老年代10m
    }

    public void localVarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 10;
        System.gc();  // 10m被回收(buffer变量的slot被后来的value占住,buffer内存被回收)
    }

    public void localVarGC5() {
        localVarGC1();
        System.gc();    // 10m被回收
    }
}

2、内存溢出(OOM)

不能申请到足够的内存进行使用,就会发生内存溢出 OOM(Out Of Memory)。

内存溢出 是引发程序崩溃的罪魁祸首之一。

0)概述

javadoc 中对 OutOfMemoryError 的解释是:没有空闲内存,并且垃圾收集器也无法提供更多内存

首先说没有空闲内存的情况:

  • Java 虚拟机的堆内存不够
    • 可能存在内存泄漏问题。
    • 堆内存的大小不合理。比如需要处理大量的数据,而堆内存设置过小(可以通过参数-Xms-Xmx来调整)
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
    • 对于老版本的 Oracle JDK,永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 intern 字符串缓存占用太多空间,也会导致 OOM 问题。
    • 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观。

在抛出 OOM 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。

  • 例如:在引用机制分析中,涉及到 JVM 会去尝试回收软引用指向的对象等。
  • java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。

当然,也不是在任何情况下垃圾收集器都会被触发的

  • 比如,我们分配一个超过堆的最大值的超大对象,JVM 可以判断出垃圾收集并不能解决这个问题,就会直接抛出 OOM。

大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。

由于 GC 一直在发展,所以一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况。

1)栈的OOM

虚拟机栈扩展时,无法申请到足够的内存,抛出OutOfMemoryError(以下代码谨慎使用,可能会引起电脑卡死)

package stack;

/**
 * 栈内存溢出: OOM
 * VM options 设置栈的大小:-Xss2m
 **/
public class StackOOM {

    public static void main(String[] args) {
        StackOOM stackOOM = new StackOOM();
        stackOOM.stackLeakByThread();
    }

    // 不断创建线程 -> 不断创建Java虚拟机栈 -> 不断申请内存 -> 内存溢出
    public void stackLeakByThread() {
        while (true) {
            Thread t = new Thread(new Runnable() {
                public void run() {
                    dontStop();
                }
            });
            t.start();
        }
    }

    private void dontStop() {
        while (true) {
        }
    }

}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:719)
	at stack.StackOOM.stackLeakByThread(StackOOM.java:22)
	at stack.StackOOM.main(StackOOM.java:11)

2)堆的OOM

一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出 OutOfMemoryError 异常。

public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture {
    private byte[] bytes;

    public Picture(int length) {
        this.bytes = new byte[length];
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at heap.Picture.<init>(OOMTest.java:26)
	at heap.OOMTest.main(OOMTest.java:16)

3)方法区的OOM

永久代:当 JVM 加载的类信息容量超过了 MaxPermsize,就会报java.lang.OutOfMemoryError: PermGen space

元空间:MaxMetaspaceSize默认无限制,JVM耗尽所有的可用系统内存,才会报OutOfMemoryError: Metaspace异常

/**
 * jdk6/7中:
 * -XX:PermSize=10m -XX:MaxPermSize=10m
 *
 * jdk8中:
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 *
 */
public class OOMTest extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            OOMTest test = new OOMTest();
            for (int i = 0; i < 10000; i++) {
                //创建ClassWriter对象,用于生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                //指明版本号,修饰符,类名,包名,父类,接口
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回byte[]
                byte[] code = classWriter.toByteArray();
                //类的加载
                test.defineClass("Class" + i, code, 0, code.length);//Class对象
                j++;
            }
        } finally {
            System.out.println(j);
        }
    }
}
3331
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:757)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:636)
	at methodArea.OOMTest.main(OOMTest.java:20)

4)直接内存的OOM

由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

/**
 * 本地内存的OOM: OutOfMemoryError: Direct buffer memory
 */
public class DirectBufferOOM {
    private static final int BUFFER = 1024 * 1024 * 20;//20MB

    public static void main(String[] args) {
        ArrayList<ByteBuffer> list = new ArrayList<>();

        int count = 0;
        try {
            while(true){
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
                list.add(byteBuffer);
                count++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            System.out.println(count);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.directbuffer.DirectBufferOOM.main(BufferTest2.java:21)

3、内存泄露(Memory Leak)

内存泄漏(Memory Leak)是指在程序中没有释放那些已经没有用处的堆内存,从而造成系统内存的浪费。

GC垃圾回收_第7张图片

严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。

  • 单例对象如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
  • 一些提供 close 的资源未关闭导致的内存泄漏

但很多时候,一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,可以看做宽泛意义上的“内存泄漏”

  • 例如一些可以作为局部变量的却定义为全局变量、web程序一些对象的会话级别设置过大等等

尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终可能出现 OutOfMemory 异常,导致程序崩溃。

4、STW(Stop The World)

Stop-the-World,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时,整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。

可达性分析算法中,枚举根节点(GC Roots)会导致所有 Java 执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行
  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁的中断会让用户体验不好,所以要减少 STW 的发生。

STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 STW 的情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

开发中不要用 System.gc() ,会导致 Stop-the-World 的发生。

5、垃圾回收的并行与并发

  • 并行(Parallel)

    多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。如 ParNew、Parallel Scavenge、Parallel Old;

  • 串行(Serial)

    单线程执行。如果内存不够,则程序暂停,启动 JM 垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

  • 并发(Concurrent)

    用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。用户程序在继续运行,而垃圾收集程序线程运行于另一个 CPU 上;如:CMS、G1

并发 vs 并行

  • 并发,指的是多个事情,在同一时间段内同时发生了。

  • 并行,指的是多个事情,在同一时间点上同时发生了。

  • 并发的多个任务之间是互相抢占资源的。

  • 并行的多个任务之间是不互相抢占资源的。

  • 只有在多 CPU 或者一个 CPU 多核的情况中,才会发生并行。

6、安全点 与 安全区域

1)安全点(Safe Point)

程序执行时,并非在任何地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始 GC,这些位置称为 安全点(Safe Point)。

安全点的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致太频繁从而导致运行时的性能问题。

大部分指令的执行时间都非常短暂,一般会选择一些执行时间较长的指令作为 Safe Point,如方法调用、循环跳转和异常跳转等。

2)中断

如何在 GC 发生时,检查所有线程都跑到最近的安全点停顿下来呢?

  • 抢先式中断:(目前没有虚拟机采用了)

    首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。

  • 主动式中断

    设置一个中断标志,各个线程运行到 Safe Point 的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

3)安全区域(Safe Resion)

Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safe Point。

但是,程序“不执行”的时候呢?例如线程处于 Sleep 状态或 Blocked 状态,这时候线程无法响应 JVM 的中断请求,“走”到安全点去中断挂起,JVM 也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域(Safe Region)是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的。

安全区域的执行流程:

  1. 当线程运行到 Safe Region 的代码时,会标识已经进入了 Safe Relgion的线程。

    如果这段时间内发生 GC,JVM 会忽略标识为 Safe Region 状态的线程。

  2. 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC

    如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开 Safe Region 的信号为止;

六、几种引用类型

JDK 1.2 以前,若一个对象不被任何变量引用,程序就无法再使用这个对象。也就是说,只有对象处于可达状态时,程序才能使用它。

JDK 1.2 开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

1、强引用 - 不回收

# 定义
	把一个对象赋给一个引用变量,这个引用变量就是一个强引用。

# 回收机制
	只要强引用还存在,垃圾收集器就永远不会回收被引用的对象。

# 缺点
	当内存空间不足时,虚拟机就算抛出 OOM 异常,也不会回收强引用所指向对象。
	因此强引用是造成 Java 内存泄漏的主要原因之一。

把一个对象赋给一个引用变量,这个引用变量就是一个强引用:

// 强引用
Object strongReference = new Object();

强引用 **不使用时 **,需要弱化从而使GC能够回收:

// 显式地设置strongReference对象为null
strongReference = null;

ArrayList的clear方法,在调用时会将每个elementData数组元素置为null,使得GC可以回收elementData数组中的元素。

/**
 * ArrayList的clear方法
 */
public class ArrayList<E>{
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }
}

2、软引用 - 内存不足即回收

# 定义
	软引用用来描述一些还有用,但非必需的对象。

# 回收机制
	当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。

# 应用场景
	软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。
	如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

软引用需要用 SoftReference 类来实现。

// 写法1
Object strongReference = new Object();
SoftReference<Object> softReference = new SoftReference<>(strongReference);
strongReference = null;

// 写法2
SoftReference<Object> softReference = new SoftReference<>(new Object());

1)示例:软引用

在 JVM 内存不足时,会清理软引用对象

/*-Xms10m -Xmx10m*/
public class SoftReferenceTest {
    public static void main(String[] args) {
        SoftReference<Object> softReference = new SoftReference<>(new Object()); // 软引用

        System.out.println("Before GC:");
        System.out.println(softReference.get());  // 从软引用中获取数据

        System.out.println("---目前内存还不紧张---");
        System.gc();
        System.out.println("After First GC:");
        System.out.println(softReference.get()); // 由于堆空间内存足够,所以不会回收软引用的可达对象。

        System.out.println("---下面开始内存紧张了---");
        try {
            byte[] b = new byte[1024 * 1024 * 11];  // 使系统GC
        } finally {
            System.out.println("After Second GC:");
            System.out.println(softReference.get()); // 在报OOM之前,垃圾回收器会回收软引用的可达对象。
        }
    }
}
Before GC:
java.lang.Object@232204a1
---目前内存还不紧张---
After First GC:
java.lang.Object@232204a1
---下面开始内存紧张了---
After Second GC:
null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at reference.SoftReferenceTest.main(SoftReferenceTest.java:24)

2)示例:软引用 & 引用队列

软引用可以配合 引用队列ReferenceQueue 使用:当软引用的对象被垃圾回收时,软引用将被添加到引用队列中。

通过 引用队列 可以跟踪对象的回收情况。

/*-Xms10m -Xmx10m*/
public class SoftReferenceQueueTest {
    public static void main(String[] args) {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); // 引用队列
        SoftReference<Object> softReference = new SoftReference<>(new Object(), referenceQueue); // 软引用
        System.out.println("softReference:" + softReference);
        
        System.out.println("Before GC:" + softReference.get());
        
        try {
            byte[] b = new byte[1024 * 1024 * 11];  // 使系统GC
        } finally {
            System.out.println("After GC:" + softReference.get());

            // 软引用的对象被垃圾回收时,软引用将被添加到引用队列中。
            Reference<?> reference = referenceQueue.poll();
            if (reference != null) {
                System.out.println(reference + "被回收了");
            }
        }
    }
}
softReference:java.lang.ref.SoftReference@42a57993
Before GCjava.lang.Object@75b84c92
After GCnull
java.lang.ref.SoftReference@42a57993被回收了
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at reference.SoftReferenceQueueTest.main(SoftReferenceQueueTest.java:18)

3)应用场景举例

软引用可用来实现内存敏感的高速缓存。例如:浏览器的后退按钮

public class SoftReferenceUseTest {
    public static void main(String[] args) {
        // 获取浏览器对象进行浏览
        Browser browser = new Browser();
        // 从后台程序加载浏览页面
        BrowserPage page = browser.getPage();
        // 将浏览完毕的页面置为软引用
        SoftReference softReference = new SoftReference(page);

        // 回退或者再次浏览此页面时
        if (softReference.get() != null) {
            // 内存充足,还没有被回收器回收,直接获取缓存
            page = softReference.get();
        } else {
            // 内存不足,软引用的对象已经回收
            page = browser.getPage();
            // 重新构建软引用
            softReference = new SoftReference(page);
        }
    }
}

3、弱引用 - 发现即回收

# 定义
	弱引用也是用来描述那些非必需对象,它比软引用的生存期更短。

# 回收机制
	在系统GC时,只要发现弱引用,不管 JVM 的内存空间是否足够,都会回收只被弱引用关联的对象。
	但是,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

# 应用场景
	缓存、ThreadLocal

弱引用需要用 WeakReference 类来实现

// 写法1
Object strongReference = new Object();
WeakReference<Object> weakReference = new WeakReference<>(strongReference);
strongReference = null;

// 写法2
WeakReference<Object> weakReference = new WeakReference<>(new Object());

1)示例:弱引用

在系统GC时,只要发现弱引用,不管 JVM 的内存空间是否足够,都会回收只被弱引用关联的对象。

public class WeakReferenceTest {
    public static void main(String[] args) {
        WeakReference<Object> weakReference = new WeakReference<>(new Object()); // 软引用
        System.out.println("Before GC:" + weakReference.get()); // GC之前
        System.gc();
        System.out.println("After GC:" + weakReference.get());  // 只要发生GC,就会回收弱引用
    }
}
Before GCjava.lang.Object@232204a1
After GCnull

2)示例:弱引用 & 引用队列

弱引用也可以配合 引用队列ReferenceQueue 使用:当弱引用的对象被垃圾回收时,弱引用将被添加到引用队列中。

通过 引用队列 可以跟踪对象的回收情况。

public class WeakReferenceQueueTest {
    public static void main(String[] args) {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); // 引用队列
        WeakReference<Object> weakReference = new WeakReference<>(new Object(), referenceQueue); // 软引用
        System.out.println("weakReference:" + weakReference);

        System.out.println("Before GC:" + weakReference.get()); // GC之前
        System.gc();
        System.out.println("After GC:" + weakReference.get());  // 只要发生GC,就会回收弱引用

        // 弱引用的对象被垃圾回收时,弱引用将被添加到引用队列中。
        Reference<?> reference = referenceQueue.poll();
        if (reference != null) {
            System.out.println(reference + "被回收了");
        }
    }
}
weakReference:java.lang.ref.WeakReference@42a57993
Before GCjava.lang.Object@75b84c92
After GCnull
java.lang.ref.WeakReference@42a57993被回收了

4、虚引用 - 对象回收追踪

# 定义
	虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。

# 回收机制
	如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

# 特点
	不能单独使用,也无法通过虚引用来获取被引用的对象。
	当试图通过虚引用的get()方法取得对象时,总是null。即通过虚引用无法获取到我们的数据

# 作用	
	虚引用的主要作用是跟踪对象被垃圾回收的状态。

虚引用需要 PhantomReference 类来实现,并且要求必须与一个引用队列关联

// 写法1
Object strongReference = new Object();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(strongReference, referenceQueue);
strongReference = null;

// 写法2
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(new Object(), referenceQueue);

1)示例:虚引用

虚引用 必须 配合 引用队列ReferenceQueue 使用:当虚引用的对象被垃圾回收时,虚引用将被添加到引用队列中。

通过 引用队列 可以跟踪对象的回收情况。

public class PhantomReferenceTest {

    static PhantomReferenceTest obj;
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;

    public static void main(String[] args) {
        // 检查虚引用回收的线程
        checkRefThread().start();

        // 构造 PhantomReferenceTest 的虚引用,并指定引用队列
        phantomQueue = new ReferenceQueue<>();
        obj = new PhantomReferenceTest();
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue);
        System.out.println(phantomRef);
        obj = null;

        // 获取虚引用 -> null
        System.out.println(phantomRef.get());

        try {
            System.out.println("第 1 次GC");
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj is null");
            } else {
                System.out.println("obj is not null");
            }

            System.out.println("第 2 次GC");
            obj = null;
            System.gc();
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj is null");
            } else {
                System.out.println("obj is not null");
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static Thread checkRefThread() {
        Thread t = new Thread(() -> {
            while (true) {
                if (phantomQueue != null) {
                    Reference<? extends PhantomReferenceTest> reference = phantomQueue.poll();
                    if (reference != null) {
                        System.out.println(reference + "被回收了");
                    }
                }
            }
        });
        t.setDaemon(true);
        return t;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类的finalize()方法");
        obj = this; // 复活当前对象
    }

}
java.lang.ref.PhantomReference@776ec8df
null1GC
调用当前类的finalize()方法
obj is not null2GC
java.lang.ref.PhantomReference@776ec8df被回收了
obj is null
  1. 第一次尝试获取虚引用的值,发现无法获取的,这是因为虚引用是无法直接获取对象的值

  2. 第一次GC,因为会调用finalize方法,将对象复活了,所以对象没有被回收

  3. 调用第二次GC操作的时候,因为finalize方法只能执行一次,所以就触发了GC操作,将对象回收了

    同时会将虚引用存入到引用队列中。

5、终结器引用

class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

它用于实现对象的 finalize() 方法,也可以称为终结器引用。

无需手动编码,其内部配合引用队列使用。

GC 时,终结器引用入队。由 Finalizer 线程通过终结器引用找到被引用对象调用它的 finalize()方法,第二次 GC 时才回收被引用的对象

6、小结

引用类型 垃圾回收时间 用途 生存时间
强引用 从来不回收 对象的一般状态 JVM停止运行时终止
软引用 内存不足时 对象缓存 内存不足时终止
弱引用 垃圾回收时 对象缓存 垃圾回收后终止
虚引用 垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止

你可能感兴趣的:(JVM,jvm)