Java编程中的垃圾回收机制详解
2021-01-261.6k 阅读
Java垃圾回收机制概述
在Java编程中,垃圾回收(Garbage Collection,GC)机制是一个至关重要的特性。它自动管理内存,让开发者无需手动释放不再使用的内存空间。这大大降低了因内存管理不当而导致的错误,如内存泄漏和悬空指针等问题。
Java运行时系统会周期性地检查不再被程序使用的对象,并释放它们占用的内存。垃圾回收器负责在后台运行,跟踪对象的生命周期,判断哪些对象可以被回收。
垃圾回收机制的工作原理
- 对象的可达性分析
- Java使用可达性分析算法来判断对象是否存活。该算法以一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到该对象不可达),则证明此对象是不可用的,垃圾回收器会将其标记为可回收对象。
- 常见的GC Roots对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象,比如方法参数、局部变量等。
- 方法区中类静态属性引用的对象,例如类的静态成员变量。
- 方法区中常量引用的对象,像字符串常量池中的字符串对象。
- 本地方法栈中JNI(即Native方法)引用的对象。
下面通过一段简单的Java代码来理解:
public class GarbageCollectionExample {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2; // obj1原来指向的对象不再有引用链到GC Roots
// 此时原来obj1指向的对象就成为了垃圾回收的候选对象
}
}
在上述代码中,最初obj1
和obj2
分别指向不同的Object
对象。当执行obj1 = obj2
后,obj1
原来指向的对象没有任何引用链连接到GC Roots,因此该对象符合垃圾回收的条件。
- 垃圾回收的时机
- 垃圾回收器何时运行并没有确定的时间点。Java虚拟机(JVM)会根据堆内存的使用情况、系统资源等因素来决定是否启动垃圾回收。通常,当堆内存快满时,垃圾回收器会被触发。
- 开发者也可以通过调用
System.gc()
方法来建议JVM进行垃圾回收,但这只是一个建议,JVM并不一定会立即执行垃圾回收操作。例如:
public class SuggestGCExample {
public static void main(String[] args) {
for (int i = 0; i < 1000000; i++) {
new Object();
}
System.gc(); // 建议JVM进行垃圾回收
}
}
在这段代码中,创建了大量的Object
对象,然后调用System.gc()
建议JVM进行垃圾回收。但JVM可能会根据自身的策略选择是否立即执行。
垃圾回收算法
- 标记 - 清除算法(Mark - Sweep)
- 标记阶段:垃圾回收器从GC Roots开始遍历,标记所有可达的对象。
- 清除阶段:遍历堆内存,回收所有未被标记的对象,即不可达对象。
- 该算法的优点是实现简单。然而,它存在两个主要缺点:
- 空间碎片化:回收后的内存空间会产生大量不连续的碎片。例如,假设堆内存中有对象A、B、C,A和C是可达对象,B是不可达对象。回收B后,A和C之间会产生一个空闲块。如果后续需要分配一个大于该空闲块的对象,即使整个堆内存还有足够的空间,也可能因为空间不连续而导致分配失败。
- 效率问题:标记和清除过程都需要遍历整个堆内存,当堆内存很大时,效率较低。
- 复制算法(Copying)
- 原理:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次性清理掉。
- 例如,假设有内存块S1和S2,初始时使用S1。当S1快满时,垃圾回收器将S1中存活的对象复制到S2,然后清空S1。
- 优点:
- 高效:复制算法只需遍历一次存活对象,并且清理内存时是一次性操作,效率较高。
- 没有空间碎片化问题:因为存活对象都被复制到一块连续的内存区域。
- 缺点:
- 内存浪费:需要两倍的内存空间来实现,因为始终有一半的内存处于未使用状态。
- 标记 - 整理算法(Mark - Compact)
- 标记阶段:与标记 - 清除算法的标记阶段相同,从GC Roots开始标记所有可达对象。
- 整理阶段:让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 例如,假设堆内存中有对象A、B、C,A和C是可达对象,B是不可达对象。标记阶段后,将A和C向一端移动,然后清理掉A和C移动后空出的区域。
- 优点:
- 解决空间碎片化问题:通过整理,使得内存空间连续,有利于后续对象的分配。
- 相对高效:相比标记 - 清除算法,虽然多了整理的步骤,但整体效率在处理大堆时比标记 - 清除算法要好。
- 缺点:
- 整理过程开销较大:需要移动对象,对于大型对象或对象数量较多时,移动操作的开销较大。
- 分代收集算法(Generational Collection)
- 原理:根据对象存活周期的不同将堆内存划分为不同的区域,一般分为新生代(Young Generation)、老年代(Old Generation)和永久代(在Java 8及以后,永久代被元空间MetaSpace取代)。
- 新生代:
- 大多数新创建的对象都在新生代。新生代又分为一个较大的Eden区和两个较小的Survivor区(一般称为Survivor0和Survivor1)。
- 当Eden区满时,会触发Minor GC(也叫新生代垃圾回收)。在Minor GC时,Eden区和其中一个Survivor区中存活的对象会被复制到另一个Survivor区,而未被复制的对象则被回收。如果一个对象在多次Minor GC后仍然存活,它会被晋升到老年代。
- 例如:
public class GenerationalGCExample {
public static void main(String[] args) {
byte[] smallObject = new byte[1024 * 100]; // 假设这个对象在Eden区分配
byte[] largeObject = new byte[1024 * 1024 * 2]; // 假设大对象直接分配到老年代(这里只是示例,实际分配策略可能更复杂)
}
}
- 老年代:
- 存放经过多次新生代垃圾回收后仍然存活的对象。当老年代空间不足时,会触发Major GC(也叫Full GC),回收老年代的垃圾对象。Major GC通常比Minor GC慢,因为它需要处理整个堆内存,包括新生代和老年代。
- 永久代/元空间:
- 在Java 8之前,永久代用于存储类信息、常量、静态变量等。在Java 8及以后,使用元空间替代永久代,元空间使用本地内存而不是堆内存。永久代/元空间的垃圾回收主要针对常量池的回收和类卸载。
Java垃圾回收器
- Serial垃圾回收器
- 特点:
- 单线程垃圾回收器,在进行垃圾回收时,会暂停所有的应用线程(Stop - The - World,STW)。
- 适用于单核处理器环境或小内存应用场景。它的优点是简单高效,因为单线程执行,没有多线程协调的开销。
- 使用方式:可以通过
-XX:+UseSerialGC
参数启用。例如,在启动Java程序时,使用java -XX:+UseSerialGC -jar yourApp.jar
。
- 特点:
- ParNew垃圾回收器
- 特点:
- 是Serial垃圾回收器的多线程版本,同样会在垃圾回收时暂停应用线程(STW)。
- 它使用多个线程并行执行垃圾回收,在多核处理器环境下性能比Serial垃圾回收器更好。
- 使用方式:通过
-XX:+UseParNewGC
参数启用。例如,java -XX:+UseParNewGC -jar yourApp.jar
。
- 特点:
- Parallel Scavenge垃圾回收器
- 特点:
- 也是多线程垃圾回收器,目标是达到一个可控的吞吐量。吞吐量是指应用程序运行时间与总运行时间(应用程序运行时间 + 垃圾回收时间)的比值。
- 它可以通过
-XX:MaxGCPauseMillis
参数设置最大垃圾回收停顿时间,或者通过-XX:GCTimeRatio
参数设置吞吐量大小。例如,-XX:GCTimeRatio = 99
表示允许垃圾回收时间占总时间的1%,应用程序运行时间占99%。
- 使用方式:通过
-XX:+UseParallelGC
参数启用。例如,java -XX:+UseParallelGC -jar yourApp.jar
。
- 特点:
- Parallel Old垃圾回收器
- 特点:
- 是Parallel Scavenge垃圾回收器针对老年代的版本,同样关注吞吐量。它与Parallel Scavenge垃圾回收器配合使用,在多核处理器和大内存环境下能提供较好的性能。
- 使用方式:通过
-XX:+UseParallelOldGC
参数启用。例如,java -XX:+UseParallelOldGC -jar yourApp.jar
。
- 特点:
- CMS(Concurrent Mark Sweep)垃圾回收器
- 特点:
- 以获取最短回收停顿时间为目标的垃圾回收器。它尽量减少应用程序的停顿时间,在垃圾回收过程中,大部分时间可以与应用程序并发执行。
- 回收过程分为四个阶段:
- 初始标记(Initial Mark):暂停所有应用线程,标记与GC Roots直接相连的对象,这个阶段速度很快。
- 并发标记(Concurrent Mark):与应用程序并发执行,从初始标记的对象开始,标记所有可达对象。
- 重新标记(Remark):暂停应用线程,修正并发标记期间因应用程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段比初始标记稍慢。
- 并发清除(Concurrent Sweep):与应用程序并发执行,回收所有未被标记的对象。
- 缺点:
- 会产生浮动垃圾:在并发清除阶段,应用程序继续运行可能会产生新的垃圾对象,这些垃圾对象在本次垃圾回收中无法被回收,只能留到下一次回收。
- 空间碎片化:由于使用标记 - 清除算法,会导致空间碎片化问题。
- 使用方式:通过
-XX:+UseConcMarkSweepGC
参数启用。例如,java -XX:+UseConcMarkSweepGC -jar yourApp.jar
。
- 特点:
- G1(Garbage - First)垃圾回收器
- 特点:
- 面向服务器端应用的垃圾回收器,适用于多核处理器和大内存环境。它将堆内存划分为多个大小相等的Region,这些Region可以动态地被划分成新生代或老年代。
- 可预测的停顿:G1垃圾回收器可以通过参数
-XX:MaxGCPauseMillis
设置最大停顿时间,并且尽量满足这个目标。 - 回收过程:
- 初始标记(Initial Mark):暂停应用线程,标记与GC Roots直接相连的对象,与CMS的初始标记类似。
- 并发标记(Concurrent Mark):与应用程序并发执行,标记所有可达对象。
- 最终标记(Final Mark):暂停应用线程,处理并发标记阶段结束后仍遗留的少量标记任务。
- 筛选回收(Live Data Counting and Evacuation):暂停应用线程,对各个Region的回收价值和成本进行排序,根据设置的停顿时间,选择价值高的Region进行回收。
- 使用方式:通过
-XX:+UseG1GC
参数启用。例如,java -XX:+UseG1GC -jar yourApp.jar
。
- 特点:
垃圾回收机制相关的调优
- 堆内存大小调整
- 可以通过
-Xms
和-Xmx
参数分别设置堆内存的初始大小和最大大小。例如,-Xms512m -Xmx1024m
表示堆内存初始大小为512MB,最大为1024MB。如果初始大小设置过小,可能会频繁触发垃圾回收;如果最大大小设置过大,可能会导致系统内存不足。
- 可以通过
- 垃圾回收器选择
- 根据应用程序的特点选择合适的垃圾回收器。例如,对于响应时间敏感的应用,如Web应用,CMS或G1垃圾回收器可能更合适;对于计算密集型的应用,Parallel Scavenge和Parallel Old垃圾回收器可能能提供更好的吞吐量。
- 调优示例
- 假设一个Web应用,在使用默认垃圾回收器时响应时间较长。通过分析,发现垃圾回收停顿时间较长影响了响应。可以尝试启用CMS垃圾回收器,通过设置
-XX:+UseConcMarkSweepGC
参数。同时,根据服务器的内存情况,合理调整堆内存大小,如-Xms1024m -Xmx2048m
。在调整后,再次进行性能测试,观察响应时间是否得到改善。如果仍然不理想,可以进一步调整CMS垃圾回收器的相关参数,如-XX:CMSInitiatingOccupancyFraction
,该参数用于设置老年代空间使用达到多少百分比时开始CMS垃圾回收,根据实际情况进行优化。
- 假设一个Web应用,在使用默认垃圾回收器时响应时间较长。通过分析,发现垃圾回收停顿时间较长影响了响应。可以尝试启用CMS垃圾回收器,通过设置
与垃圾回收相关的对象引用类型
- 强引用(Strong Reference)
- 定义:这是最常见的引用类型,如
Object obj = new Object();
中,obj
对新创建的Object
对象的引用就是强引用。只要强引用存在,垃圾回收器永远不会回收被引用的对象。 - 示例:
- 定义:这是最常见的引用类型,如
public class StrongReferenceExample {
public static void main(String[] args) {
Object strongRef = new Object();
// 只要strongRef存在,它指向的对象不会被垃圾回收
strongRef = null; // 手动将强引用置为null,此时对象可能被垃圾回收
}
}
- 软引用(Soft Reference)
- 定义:用来描述一些还有用但并非必需的对象。在系统将要发生内存溢出之前,会把这些对象列入回收范围进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
- 使用场景:常用于缓存场景,比如图片缓存。当内存充足时,缓存中的图片可以通过软引用保持;当内存紧张时,缓存中的图片可以被回收以释放内存。
- 示例:
import java.lang.ref.SoftReference;
public class SoftReferenceExample {
public static void main(String[] args) {
SoftReference<String> softRef = new SoftReference<>(new String("Hello, Soft Reference"));
String value = softRef.get();
if (value!= null) {
System.out.println("Value from soft reference: " + value);
} else {
System.out.println("Soft reference object has been garbage collected.");
}
}
}
- 弱引用(Weak Reference)
- 定义:弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 使用场景:例如在Java的
WeakHashMap
中,当一个键对象不再被其他地方引用(只有WeakHashMap
中的弱引用),那么在垃圾回收时,这个键值对会被自动移除。 - 示例:
import java.lang.ref.WeakReference;
public class WeakReferenceExample {
public static void main(String[] args) {
WeakReference<String> weakRef = new WeakReference<>(new String("Hello, Weak Reference"));
String value = weakRef.get();
if (value!= null) {
System.out.println("Value from weak reference: " + value);
} else {
System.out.println("Weak reference object has been garbage collected.");
}
System.gc(); // 建议垃圾回收
value = weakRef.get();
if (value!= null) {
System.out.println("Value from weak reference after GC: " + value);
} else {
System.out.println("Weak reference object has been garbage collected after GC.");
}
}
}
- 虚引用(Phantom Reference)
- 定义:也称为幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
- 使用场景:在对象被回收时进行一些特殊的清理操作,比如资源释放等。
- 示例:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceExample {
public static void main(String[] args) {
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>(new String("Hello, Phantom Reference"), referenceQueue);
System.out.println(phantomRef.get()); // 总是返回null
// 当关联的对象被回收时,虚引用会被放入引用队列
System.gc();
try {
java.lang.ref.Reference<?> ref = referenceQueue.remove(1000);
if (ref!= null) {
System.out.println("Object has been garbage collected.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过深入了解Java的垃圾回收机制,包括其原理、算法、垃圾回收器以及相关的调优和对象引用类型,开发者可以更好地编写高效、稳定的Java程序,避免内存相关的问题,提升应用程序的性能。