JVM-垃圾回收

什么是JVM的垃圾回收?

垃圾回收(Garbage Collection, GC)是Java虚拟机(JVM)自动管理内存的一种机制,其目的是自动回收不再使用的对象所占用的内存空间,以防止内存泄漏和提升内存利用效率。

运行时数据区

运行时数据区JVM的内存区域,其中包含程序计数器、java虚拟机栈、本地方法栈、方法区、堆区。

其中,程序计数器、java虚拟机栈、本地方法栈线程私有,所占内存会随线程的结束而回收,因此在大多数情况下,不需要特别关注它们的垃圾回收。

方法区和堆区是线程共享的,垃圾回收的重点主要在这两个区域,尤其是堆区。

方法区的垃圾回收

方法区的主要内容

  • 类元数据:存储类的结构信息,如字段、方法、接口等。
  • 运行时常量池:存储类文件中的常量池(包括字面量和符号引用)的运行时表示。
  • 静态变量:类的静态字段,属于类而非实例。
  • 即时编译器编译后的代码:存储JIT编译器生成的本地代码。

垃圾回收的主要目标

1. 类卸载
  • 原因:当一个类加载器被卸载时,所有由该加载器加载的类也应该被卸载,以释放内存。
  • 条件:要卸载一个类,必须确保该类的所有实例都已经被回收,并且没有对该类的任何引用(包括反射引用)存在。
  • 过程(可达性分析)
    • 标记阶段:标记所有可以访问的类。
    • 清除阶段:清除没有标记到的类,释放其占用的内存。
  • 困难:类卸载相对较为复杂,因为需要确保没有剩余的引用,这在长时间运行的应用中可能较难达成。
2. 运行时常量池回收
  • 原因:运行时常量池中的常量可能会大量增加,特别是字符串常量和符号引用。
  • 过程(可达性分析)
    • 标记阶段:标记所有可以访问的常量。
    • 清除阶段:清除未被标记的常量,释放内存。

引用计数法和可达性分析法

1.引用计数法

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。

引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但是它也存在缺点,主要有两点:

  • 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
  • 存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。
2.可达性分析法

可达性分析法(Reachability Analysis)是判断对象是否可被回收的一种算法,它通过一系列称为"GC Roots"的对象作为起点,沿着这些起点引用的路径进行遍历。如果一个对象在遍历过程中不可达,即没有任何引用链能从GC Roots到达该对象,则该对象被认为是不可达的,可以被回收。

GC Roots包括:

  1. 活动线程

    • 所有存活的线程对象包括各个线程的栈帧中的本地变量表中的引用,即当前活动线程的所有局部变量。
  2. 方法区中的静态引用

    • 类静态字段引用的对象,即由方法区中的类的静态变量所引用的对象。
  3. 方法区中的常量引用

    • 运行时常量池中的常量引用,例如字符串常量池中的字符串对象。
  4. 本地方法栈中的引用

    • JNI中的本地引用 ,包括JNI接口通过NewGlobalRef创建的全局引用。
  5. 同步锁定的对象

    • 当前被锁定的对象,即被同步块或方法锁定的对象,因为这些对象在持有者线程中被引用。

这些GC Roots构成了对象图中最初始的可达节点,从这些节点开始,垃圾回收器将追踪所有可达对象。如果一个对象未能从这些GC Roots到达,则说明它是不可达的,可以被垃圾回收。每次GC都会从这些根节点出发,确定哪些对象存活并需要保留,哪些对象可以被回收以释放内存

方法区垃圾回收的特点

  1. 回收频率低:方法区的垃圾回收频率低于堆区,因为方法区中的数据变动相对较少。
  2. 回收复杂度高:类卸载和常量池的回收需要精确管理引用,确保没有遗漏,复杂度较高。
  3. 影响性能:方法区的垃圾回收可能会对性能产生较大影响,尤其是在需要频繁加载和卸载类的情况下。

JVM对方法区垃圾回收的支持

  • 永久代(PermGen):在JDK 7及之前,方法区位于永久代。永久代的垃圾回收由Full GC执行。
  • 元空间(Metaspace):在JDK 8及之后,方法区改为元空间。元空间是直接使用本地内存而不是堆内存,垃圾回收机制有所改进。

总结

方法区的垃圾回收虽然不如堆区频繁,但依然是JVM内存管理的重要部分,尤其是在长时间运行和高动态性的应用中。主要涉及类卸载和常量池的回收,确保内存的有效利用和防止内存泄漏。

常见的引用方式

可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。除了强引用之外,Java中还设计了几种其他引用方式:

  • 软引用

  • 弱引用

  • 虚引用

  • 终结器引用

软引用

软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。

这样做有什么好处?如果对象A是一个缓存,平时会保存在内存中,如果想访问数据可以快速访问。但是如果内存不够用了,我们就可以将这部分缓存清理掉释放内存。即便缓存没了,也可以从数据库等地方获取数据,不会影响到业务正常运行,这样可以减少内存溢出产生的可能性。

特别注意:

软引用对象本身,也需要被强引用,否则软引用对象也会被回收掉。

软引用的使用方法

软引用的执行过程如下:

1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。

2.内存不足时,虚拟机尝试进行垃圾回收。

3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。

4.如果依然内存不足,抛出OutOfMemory异常。

软引用对象本身怎么回收呢?

如果软引用对象里边包含的数据已经被回收了,那么软引用对象本身其实也可以被回收了。

SoftReference提供了一套队列机制:

  1. 软引用创建时,通过构造器传入引用队列
  2. 在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
  3. 通过代码遍历引用队列,将SoftReference的强引用删除

示例:

public class SoftReferenceDemo3 {

    public static void main(String[] args) throws IOException {

        ArrayList<SoftReference> softReferences = new ArrayList<>();
        ReferenceQueue<byte[]> queues = new ReferenceQueue<byte[]>();
        for (int i = 0; i < 10; i++) {
            byte[] bytes = new byte[1024 * 1024 * 100];
            SoftReference studentRef = new SoftReference<byte[]>(bytes,queues);
            softReferences.add(studentRef);
        }

        SoftReference<byte[]> ref = null;
        int count = 0;
        while ((ref = (SoftReference<byte[]>) queues.poll()) != null) {
            count++;
        }
        System.out.println(count);

    }
}

弱引用

弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。

虚引用和终结器引用

这两种引用在常规开发中是不会使用的。

  • 虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。

  • 终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。

堆区的垃圾回收

堆区的垃圾回收(GC)是Java虚拟机(JVM)内存管理的核心部分,涉及到清理不再被使用的对象以释放内存。

1. 堆区的内存结构

堆区是JVM内存的一部分,专门用于存储所有对象实例。通常堆区被分为以下几个区域:

  • 年轻代(Young Generation)

    • 存储新创建的对象。
    • 通常分为三个部分:
      • Eden区:对象首先分配到Eden区。
      • Survivor区(S0和S1):Eden区经过垃圾回收后,存活的对象会被移动到Survivor区的一个区域。Survivor区有两个部分(S0和S1),它们交替使用。
  • 老年代(Old Generation)

    • 存储经过多次垃圾回收仍然存活的对象。
    • 老年代的垃圾回收频率较低,因为对象在这里的生命周期较长。
  • 永久代( PermGen**)**(Java 8之前版本存在,后被元空间取代,元空间存在于直接内存,不在堆中):

    • 存储类元数据、常量池、方法区等信息的一部分内存区域。永久代存在于JVM堆内存中,但其管理和内存回收机制与堆区的其他部分有所不同。

2. 垃圾回收算法

垃圾回收器使用不同的算法来管理内存。常见的算法包括:

  • 标记-清除算法(Mark-and-Sweep)

    • 标记:从GC Roots开始,遍历并标记所有可达对象。
    • 清除:回收所有未被标记的对象。
    • 优点:简单直观。
    • 缺点:清除后会产生内存碎片,可能导致内存空间不连续。
  • 复制算法(Copying)

    • .准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间),.在垃圾回收GC阶段,将From中存活对象复制到To空间,将两块空间的From和To名字互换。
    • 优点:高效的垃圾回收,减少了碎片。
    • 缺点:需要额外的内存空间作为备用区域。
  • 标记-整理算法(Mark-Compact)

    • 结合了标记-清除和复制算法的优点。首先标记所有可达对象,然后将这些对象整理到内存的一端,最后清理未被使用的内存。
    • 优点:消除了碎片问题。
    • 缺点:整理过程可能比较复杂和耗时。
  • 分代收集算法(Generational Collection)

    • 根据对象的年龄将堆区划分为不同的代。年轻代和老年代使用不同的回收算法,年轻代通常使用复制算法,而老年代使用标记-整理算法。
    • 优点:提高了回收效率,减少了老年代GC的频率。
    • 缺点:管理多个代的复杂性。

3. 垃圾回收器

由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。具体的关系图如下:

1. 年轻代-Serial垃圾回收器

Serial是是一种单线程串行回收年轻代的垃圾回收器。

  • 使用的算法

    • 年轻代:复制算法。
  • 优点

    • 单CPU处理器下吞吐量非常出色
  • 缺点

    • 多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
  • 适用场景

    • Java编写的客户端程序或者硬件配置有限的场景
2. 老年代-SerialOld垃圾回收器

SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收

-XX:+UseSerialGC 新生代、老年代都使用串行回收器。

  • 使用的算法

    • 老年代:标记-整理算法
  • 优点

    • 单CPU处理器下吞吐量非常出色
  • 缺点

    • 多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待
  • 适用场景

    • 与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
3. 年轻代-ParNew垃圾回收器

ParNew垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收

-XX:+UseParNewGC 新生代使用ParNew回收器, 老年代使用串行回收器

  • 使用的算法

    • 年轻代:复制算法。
  • 优点

    • 多CPU处理器下停顿时间较短
  • 缺点

    • 吞吐量和停顿时间不如G1,所以在JDK9之后不建议使用
  • 适用场景

    • JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
4. 老年代- CMS(Concurrent Mark Sweep)垃圾回收器

CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。

参数:XX:+UseConcMarkSweepGC

  • 使用的算法

    • 老年代:标记清除算法
  • 优点

    • 系统由于垃圾回收出现的停顿时间较短,用户体验好
  • 缺点

    1、内存碎片问题

    2、退化问题

    3、浮动垃圾问题

  • 适用场景

    • 大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
  • CMS执行步骤:

    1.初始标记,用极短的时间标记出GC Roots能直接关联到的对象。

    2.并发标记, 标记所有的对象,用户线程不需要暂停。

    3.重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。

    4.并发清理,清理死亡的对象,用户线程不需要暂停。

5. 年轻代-Parallel Scavenge垃圾回收器

Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。

  • 使用的算法

    • 年轻代:复制算法。
  • 优点

    • 吞吐量高,而且手动可控。为了提高吞吐量,虚拟机会动态调整堆的参数
  • 缺点

    • 不能保证单次的停顿时间
  • 适用场景

    • 后台任务,不需要与用户交互,并且容易产生大量的对象。比如:大数据的处理,大文件导出
  • 常用参数:

    Parallel Scavenge允许手动设置最大暂停时间和吞吐量。Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。

    • 最大暂停时间,-XX:MaxGCPauseMillis=n 设置每次垃圾回收时的最大停顿毫秒数

    • 吞吐量,-XX:GCTimeRatio=n 设置吞吐量为n(用户线程执行时间 = n/n + 1)

    • 自动调整内存大小, -XX:+UseAdaptiveSizePolicy设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小

6. 老年代-Parallel Old垃圾回收器

Parallel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集。

参数: -XX:+UseParallelGC 或

-XX:+UseParallelOldGC可以使用Parallel Scavenge + Parallel Old这种组合。

  • 使用的算法

    • 老年代:标记-整理算法
  • 优点

    • 并发收集,在多核CPU下效率较高
  • 缺点

    • 暂停时间会比较长
  • 适用场景

    • 与Parallel Scavenge配套使用
7. G1 -- Garbage First 垃圾回收器

参数1: -XX:+UseG1GC 打开G1的开关,JDK9之后默认不需要打开

参数2:-XX:MaxGCPauseMillis=毫秒值 最大暂停的时

  • 回收年代和算法

    • 年轻代+老年代
    • 复制算法
  • 优点

    • 对比较大的堆如超过6G的堆回收时,延迟可控
    • 不会产生内存碎片
    • 并发标记的SATB算法效率高
  • 缺点

    • JDK8之前还不够成熟
  • 适用场景

    • JDK8最新版本、JDK9之后建议默认使用

总结

垃圾回收器的组合关系虽然很多,但是针对几个特定的版本,比较好的组合选择如下:

JDK8及之前:

ParNew + CMS(关注暂停时间)、Parallel Scavenge + Parallel Old (关注吞吐量)、 G1(JDK8之前不建议,较大堆并且关注暂停时间)

JDK9之后:

G1(默认)

相关推荐
HUNAG-DA-PAO2 小时前
Spring AOP是什么
java·jvm·spring
No regret.3 小时前
JVM内存模型、垃圾回收机制及简单调优方式
java·开发语言·jvm
东阳马生架构12 小时前
JVM实战—2.JVM内存设置与对象分配流转
jvm
撸码到无法自拔13 小时前
深入理解.NET内存回收机制
jvm·.net
吴冰_hogan1 天前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
东阳马生架构1 天前
JVM实战—1.Java代码的运行原理
jvm
ThisIsClark2 天前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉2 天前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥2 天前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开2 天前
jvm接入prometheus监控
jvm·windows·prometheus