JVM-垃圾回收 (一文弄懂)

在Java虚拟机(JVM)中,垃圾收集(GC)是自动管理内存的重要机制。其主要任务是识别不再被引用的对象并回收其占用的内存。

目录

内存分配与回收机制

对象在Eden区的分配

大对象的处理

长期存活对象的晋升

判断死亡方法

对象循环引用问题

引用类型

对象的生命周期

垃圾回收算法

1. 标记-清除算法 (Mark-Sweep)

2. 复制算法 (Copying)

3. 标记-整理算法 (Mark-Compact)

4. 分代收集算法 (Generational Collection)

 分代回收

堆内存的结构

新生代的垃圾回收

Minor GC

老年代的垃圾回收

Major GC

Full GC

触发Full GC的条件

Full GC的影响


内存分配与回收机制

对象在Eden区的分配

在大多数情况下,新创建的对象会优先在新生代的Eden区进行分配。当Eden区的空间不足以容纳新的对象时,JVM会触发一次Minor GC。

大对象的处理

大对象是指需要连续内存空间的对象,例如数组和字符串。对于大对象,JVM会直接将其分配到老年代。这一策略是由虚拟机动态决定的,具体取决于所使用的垃圾回收器及其相关参数。将大对象直接放入老年代是一种优化措施,旨在避免将其放入新生代,从而减少新生代的垃圾回收频率和成本。

长期存活对象的晋升

为了有效管理内存回收,JVM采用了分代收集的策略,能够识别哪些对象应当放在新生代,哪些应当放在老年代。为此,JVM为每个对象分配一个年龄计数器(Age)。通常情况下,对象会在Eden区分配。如果一个对象在Eden区创建后,经过第一次Minor GC仍然存活,并且能够被Survivor区容纳,它将被移动到Survivor区(S0或S1),并将其年龄设为1(即从Eden区转移到Survivor区后,初始年龄变为1)。对象在Survivor区中,每经过一次Minor GC,其年龄就会增加1。当对象的年龄达到一定值(默认为15岁)时,它将被晋升到老年代。这个晋升的年龄阈值可以通过参数-XX:MaxTenuringThreshold进行设置。

如果对象在Survivor区经历了15次Minor GC仍未被回收,它将被转移到老年代。 

判断死亡方法

垃圾识别的关键在于确定哪些对象不再被程序使用。常用的方法包括:

引用计数: 每个对象维护一个引用计数器,当有引用指向该对象时,计数器加一,当引用被删除时,计数器减一。当计数器为零时,表示该对象不再被使用,可以被回收。

缺点:无法解决对象循环引用的问题

可达性分析:通过从根对象(如栈中的局部变量、静态变量等)开始,遍历所有可达的对象。不可达的对象即为垃圾。

优点:能够有效处理对象循环引用的问题

对象循环引用问题

对象循环引用指的是两个或多个对象之间形成了一个引用循环,但这些对象在程序中已经不再被使用。例如:

public class ReferenceCircle {
    public Object instance = null;

    public ReferenceCircle() {
        instance = this;
    }
}

ReferenceCircle a = new ReferenceCircle();
ReferenceCircle b = new ReferenceCircle();
a.instance = b;
b.instance = a;

a和b对象形成了一个循环引用。即使a和b在程序中不再被使用,引用计数算法也无法判断它们为垃圾,因为它们的引用计数永远不会为零。

引用类型

Java中有几种不同类型的引用,这些引用的强度决定了对象的可回收性:

  • 强引用:最常见的引用类型,默认情况下创建的对象都是强引用。只要强引用存在,垃圾收集器就不会回收该对象。
  • 软引用:用于描述一些有用但非必需的对象。软引用的对象在内存不足时会被回收,但在正常情况下不会被回收。
  • 弱引用:弱引用的对象在下一次垃圾收集时会被回收。适用于一些缓存场景,如ThreadLocal中的对象。
  • 虚引用:虚引用几乎不影响对象的生命周期,无法通过虚引用访问对象。它主要用于在对象被回收时收到通知。

对象的生命周期

对象的生命周期也影响其回收的时机。一般来说,以下情况会导致对象被认为是垃圾:

  • 不再被引用:对象在程序中不再有任何强引用指向时,便可以被回收。
  • 超出作用域:局部变量超出其作用域后,指向的对象可能被回收。
  • 集合类的清理:当集合类(如List、Map等)中的对象被移除时,相关对象的引用计数可能减少,从而成为垃圾。

垃圾回收算法

不同的算法适用于不同的场景和需求

1. 标记-清除算法 (Mark-Sweep)

原理

标记-清除算法分为两个阶段:

  • 标记阶段:遍历所有对象,从根对象开始,标记所有可达的对象。
  • 清除阶段:遍历堆内存,清除未被标记的对象,回收其占用的内存。

优缺点

  • 优点
    • 实现简单,易于理解。
  • 缺点
    • 存在内存碎片问题,因为清除后会留下不连续的内存空间。
    • 整个过程需要暂停应用程序,导致停顿时间较长。

2. 复制算法 (Copying)

原理

复制算法将内存划分为两个相等的区域,每次只使用其中一个区域。当需要回收时,将存活的对象复制到另一个区域,然后清空当前区域。

优缺点

  • 优点
    • 解决了内存碎片问题,因为复制后所有存活对象都在连续的内存空间中。
    • 效率较高,标记和清除的过程被简化为复制。
  • 缺点
    • 内存利用率低,因为总是要分配一半的内存作为备用区域。

3. 标记-整理算法 (Mark-Compact)

原理

标记-整理算法结合了标记-清除和复制算法的优点。首先进行标记阶段,然后将存活的对象移动到内存的一端,最后清理掉末尾的空闲空间。

优缺点

  • 优点
    • 解决了内存碎片问题,所有存活对象都在一起,便于后续分配。
  • 缺点
    • 移动对象需要更新所有引用,开销较大,可能导致停顿时间增加。

4. 分代收集算法 (Generational Collection)

原理

分代收集算法基于“对象的生命周期”这一假设,将堆内存分为新生代和老年代。新生代用于存放新创建的对象,老年代用于存放存活时间较长的对象。新生代的回收频率高,而老年代的回收频率低。

具体实现

  • 新生代:通常采用复制算法,Eden区和两个Survivor区相互交替使用。
  • 老年代:可以使用标记-清除、标记-整理等算法。

优缺点

  • 优点
    • 根据对象的生命周期优化内存回收,提高了效率。
    • 减少了老年代的垃圾回收次数,降低了停顿时间。
  • 缺点
    • 需要管理不同代的内存和回收策略,复杂度增加。

 分代回收

分代算法的核心假设是:大多数对象的生命周期是短暂的,即大多数对象在创建后不久就会被回收,而少数对象会存活较长时间。基于这一假设,JVM将堆内存划分为不同的区域,以优化垃圾回收过程。

堆内存的结构

在分代算法中,堆内存通常分为以下几个区域:

  • 新生代(Young Generation):用于存放新创建的对象。新生代通常又分为三个部分:
    • Eden区:新对象的主要分配区域。
    • Survivor区(S0和S1):用于存放经过垃圾回收后仍然存活的对象。通常有两个Survivor区,交替使用。
  • 老年代(Old Generation):用于存放存活时间较长的对象。经过多次垃圾回收仍然存活的对象会被晋升到老年代。
  • 永久代(Permanent Generation)(JDK8之后被元空间取代):用于存放类的元数据和常量池等信息。

默认情况下,Eden : S0 : S1 = 8 : 1 : 1   新生代 : 老年代 =  1 : 3

详细参考:JVM-内存结构(超详细)-CSDN博客

新生代的垃圾回收

Minor GC

新生代的垃圾回收称为Minor GC。其过程如下:

  1. 对象分配:新创建的对象首先在Eden区分配内存。
  2. 触发回收:当Eden区的空间不足以容纳新对象时,JVM会触发Minor GC。
  3. 标记存活对象:在Minor GC过程中,JVM会标记所有从根对象可达的存活对象。
  4. 对象复制:存活的对象会被复制到一个Survivor区,未被标记的对象则会被清除。
  5. 年龄计数:存活对象的年龄会增加。如果对象在Survivor区存活多次(通常是15次),则会被晋升到老年代。

老年代的垃圾回收

Major GC

老年代的垃圾回收称为Major GC,通常发生的频率较低。其过程如下:

  1. 标记存活对象:JVM会从根对象开始,标记所有存活的对象。
  2. 清除或整理:根据所使用的垃圾回收算法,可能会进行标记-清除或标记-整理的操作,清理未被标记的对象。

Full GC

Full GC是指对整个Java堆(包括新生代和老年代)进行的垃圾回收。与Minor GC(仅回收新生代)相比,Full GC的开销更大,通常会导致应用程序的停顿时间显著增加。

触发Full GC的条件

Full GC的触发条件有多种,主要包括:

  1. 老年代空间不足:当老年代的内存不足以容纳新晋升的对象时,会触发Full GC。
  2. 调用System.gc():应用程序显式调用System.gc()Runtime.getRuntime().gc()时,JVM会尝试进行Full GC。
  3. 永久代(或Metaspace)空间不足:在使用永久代的JVM中,如果永久代的空间不足以加载新的类或元数据,也会触发Full GC。
  4. 内存溢出:当JVM检测到内存溢出(OutOfMemoryError)时,可能会尝试进行Full GC以释放内存。
  5. JVM内部的优化机制:某些JVM实现可能会根据内存使用情况和其他因素自动触发Full GC。

Full GC的影响

由于Full GC涉及整个堆的回收,因此它通常会导致较长的停顿时间,影响应用的响应性和性能。因此,优化Full GC的频率和停顿时间是性能调优的重要方面。

更多JVM知识:

JVM-类加载过程-CSDN博客

JVM-类加载器-CSDN博客

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值