在Java虚拟机(JVM)中,垃圾收集(GC)是自动管理内存的重要机制。其主要任务是识别不再被引用的对象并回收其占用的内存。
目录
4. 分代收集算法 (Generational Collection)
内存分配与回收机制
对象在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。其过程如下:
- 对象分配:新创建的对象首先在Eden区分配内存。
- 触发回收:当Eden区的空间不足以容纳新对象时,JVM会触发Minor GC。
- 标记存活对象:在Minor GC过程中,JVM会标记所有从根对象可达的存活对象。
- 对象复制:存活的对象会被复制到一个Survivor区,未被标记的对象则会被清除。
- 年龄计数:存活对象的年龄会增加。如果对象在Survivor区存活多次(通常是15次),则会被晋升到老年代。
老年代的垃圾回收
Major GC
老年代的垃圾回收称为Major GC,通常发生的频率较低。其过程如下:
- 标记存活对象:JVM会从根对象开始,标记所有存活的对象。
- 清除或整理:根据所使用的垃圾回收算法,可能会进行标记-清除或标记-整理的操作,清理未被标记的对象。
Full GC
Full GC是指对整个Java堆(包括新生代和老年代)进行的垃圾回收。与Minor GC(仅回收新生代)相比,Full GC的开销更大,通常会导致应用程序的停顿时间显著增加。
触发Full GC的条件
Full GC的触发条件有多种,主要包括:
- 老年代空间不足:当老年代的内存不足以容纳新晋升的对象时,会触发Full GC。
- 调用System.gc():应用程序显式调用
System.gc()
或Runtime.getRuntime().gc()
时,JVM会尝试进行Full GC。 - 永久代(或Metaspace)空间不足:在使用永久代的JVM中,如果永久代的空间不足以加载新的类或元数据,也会触发Full GC。
- 内存溢出:当JVM检测到内存溢出(OutOfMemoryError)时,可能会尝试进行Full GC以释放内存。
- JVM内部的优化机制:某些JVM实现可能会根据内存使用情况和其他因素自动触发Full GC。
Full GC的影响
由于Full GC涉及整个堆的回收,因此它通常会导致较长的停顿时间,影响应用的响应性和性能。因此,优化Full GC的频率和停顿时间是性能调优的重要方面。
更多JVM知识: