Java垃圾回收详解
Java垃圾回收(Garbage Collection,GC)是Java虚拟机自动内存管理的核心机制,它负责自动回收不再使用的对象占用的内存空间。垃圾回收机制使得Java程序员无需手动管理内存,大大降低了内存泄漏和内存溢出的风险,提高了开发效率和程序稳定性。
Java垃圾回收 = 自动内存管理 + 多种回收算法 + 分代收集策略 + 并发回收机制 + 性能优化
1. 垃圾回收基础概念
1.1 什么是垃圾回收?
垃圾回收是Java虚拟机自动内存管理的核心机制,它通过自动识别和回收不再使用的对象来管理堆内存。
- 基本概念
- 对比手动内存管理
- 发展历史
垃圾回收定义: 自动识别并清除不再使用的内存区域,使该内存可被重新使用的过程。
主要目标:
- 内存管理自动化:无需程序员手动释放内存
- 提高内存利用率:回收不再使用的对象占用的内存
- 减少内存碎片:通过内存整理提高内存利用效率
- 降低开发难度:简化内存管理,减少内存泄露和溢出风险
工作原理:
- 标记阶段:识别哪些对象是"垃圾"(不再被使用的对象)
- 回收阶段:回收垃圾对象占用的内存空间
- 整理阶段:(可选)重新排列存活对象,减少内存碎片
| 特性 | 垃圾回收 | 手动内存管理 |
|---|---|---|
| 开发效率 | 高 - 无需考虑内存释放 | 低 - 需要手动分配和释放 |
| 错误风险 | 低 - 自动管理减少错误 | 高 - 容易导致内存泄漏或悬空指针 |
| 性能控制 | 弱 - 回收时机不完全可控 | 强 - 精确控制内存释放时机 |
| 内存使用 | 较高 - GC需要额外内存 | 较低 - 无额外开销 |
| 暂停时间 | 存在STW暂停 | 无系统级暂停 |
| 代码复杂度 | 低 - 内存管理透明 | 高 - 需手动跟踪内存 |
| 适用场景 | 大多数业务应用 | 系统底层、实时系统 |
典型的手动内存管理语言: C、C++ 典型的垃圾回收语言: Java、C#、Python、JavaScript
1// Java - 自动垃圾回收2public class AutomaticMemory {3 public void createObjects() {4 for (int i = 0; i < 1000; i++) {5 Object obj = new Object(); // 不再引用时自动回收6 }7 } // 方法结束时,局部变量obj自动失去引用,对象可被回收8}910// C++ - 手动内存管理11class ManualMemory {12public:13 void createObjects() {14 for (int i = 0; i < 1000; i++) {15 Object* obj = new Object(); // 必须手动释放16 delete obj; // 不释放会造成内存泄漏17 }18 }19};垃圾回收技术发展时间线:
- 1959: Lisp首次引入垃圾回收概念
- 1960年代: 引用计数法被广泛使用
- 1970年代: 标记-清除和标记-整理算法出现
- 1980年代: 分代收集理论形成
- 1995: Java诞生,采用自动垃圾回收
- 2000年代初: CMS并发收集器推出
- 2000年代中: G1收集器研发
- 2010年代: ZGC、Shenandoah等低延迟收集器问世
JVM垃圾回收器发展:
主要技术突破:
- 从单线程到多线程并行收集
- 从STW暂停到并发收集
- 从全堆扫描到增量式收集
- 从固定分代到区域化内存布局
垃圾回收的核心要素
1public class GarbageCollectionCore {2 3 // ========== 垃圾回收的基本原理 ==========4 // 1. 自动识别:自动识别哪些对象不再被使用5 // 2. 自动回收:自动回收不再使用对象占用的内存6 // 3. 内存整理:整理内存碎片,提高内存利用率7 // 4. 性能优化:在回收效率和停顿时间间找到平衡8 9 // ========== 垃圾回收的优势 ==========10 // 1. 自动管理:程序员无需手动释放内存11 // 2. 防止泄漏:自动处理内存泄漏问题12 // 3. 提高效率:减少内存管理相关的bug13 // 4. 简化开发:专注于业务逻辑开发14}1.2 垃圾回收的重要性
| 重要性 | 具体体现 | 业务价值 |
|---|---|---|
| 自动内存管理 | 无需手动释放内存 | 降低开发复杂度,提高开发效率 |
| 防止内存泄漏 | 自动回收无用对象 | 提高程序稳定性,减少系统故障 |
| 内存碎片整理 | 整理内存碎片 | 提高内存利用率,优化性能 |
| 性能优化 | 多种回收算法和策略 | 根据应用特点选择最优方案 |
1.3 垃圾回收的设计原则
垃圾回收机制的设计遵循以下几个核心原则:
自动化原则
完全自动的内存管理,无需程序员干预
高效性原则
在保证回收效果的前提下,尽量减少对程序执行的影响
可预测性原则
垃圾回收的行为应该是可预测的,便于性能调优
适应性原则
能够根据应用特点自动调整回收策略
1public class GCDesignPrinciples {2 3 /**4 * 自动化示例5 */6 public static void automationExample() {7 // 程序员无需手动管理内存8 List<String> list = new ArrayList<>();9 for (int i = 0; i < 1000000; i++) {10 list.add("item" + i);11 }12 13 // 当list不再被引用时,垃圾回收器会自动回收14 list = null;15 16 // 无需手动调用类似free()的方法17 // 垃圾回收器会在合适的时机自动回收18 }19 20 /**21 * 高效性示例22 */23 public static void efficiencyExample() {24 // 垃圾回收器会选择合适的时机进行回收25 // 1. 内存不足时26 // 2. 系统空闲时27 // 3. 可预测的停顿时间28 29 // 现代垃圾回收器都支持并发回收30 // 减少对程序执行的影响31 }32 33 /**34 * 可预测性示例35 */36 public static void predictabilityExample() {37 // 垃圾回收器提供可预测的停顿时间38 // 例如G1收集器的MaxGCPauseMillis参数39 // -XX:MaxGCPauseMillis=20040 41 // 可以通过参数调整回收行为42 // 使垃圾回收更符合应用需求43 }44 45 /**46 * 适应性示例47 */48 public static void adaptabilityExample() {49 // 垃圾回收器会根据应用特点调整策略50 // 1. 对象生命周期51 // 2. 内存分配模式52 // 3. 系统负载情况53 54 // 分代收集就是适应性的体现55 // 不同代使用不同的回收策略56 }57}2. 对象存活判断
2.1 引用计数法
引用计数法是最简单的对象存活判断算法,为每个对象维护一个引用计数器。
引用计数法原理
1public class ReferenceCountingExample {2 3 /**4 * 引用计数法实现5 */6 public static void referenceCountingDemo() {7 // 创建对象,引用计数为18 Object obj1 = new Object();9 10 // 增加引用,引用计数为211 Object obj2 = obj1;12 13 // 减少引用,引用计数为114 obj2 = null;15 16 // 减少引用,引用计数为0,对象可以被回收17 obj1 = null;18 }19 20 /**21 * 引用计数法的优缺点22 */23 public static void referenceCountingAnalysis() {24 // 优点:25 // 1. 实现简单26 // 2. 回收及时27 // 3. 没有停顿时间28 29 // 缺点:30 // 1. 无法解决循环引用问题31 // 2. 计数器更新开销大32 // 3. 空间开销33 34 // 循环引用示例35 Node node1 = new Node();36 Node node2 = new Node();37 38 // 形成循环引用39 node1.next = node2;40 node2.next = node1;41 42 // 即使外部引用被清除,引用计数也不为043 node1 = null;44 node2 = null;45 // 此时两个对象都无法被回收46 }47}4849// 节点类,用于演示循环引用50class Node {51 Node next;52}引用计数法的局限性
| 局限性 | 具体表现 | 影响 |
|---|---|---|
| 循环引用 | 对象间相互引用形成环 | 无法回收循环引用的对象 |
| 性能开销 | 每次引用赋值都要更新计数器 | 影响程序执行效率 |
| 空间开销 | 每个对象都需要计数器字段 | 增加内存使用 |
2.2 可达性分析
可达性分析是Java虚拟机采用的垃圾回收算法,通过GC Roots作为起始点进行搜索。
可达性分析原理
1public class ReachabilityAnalysisExample {2 3 /**4 * GC Roots示例5 */6 public static void gcRootsExample() {7 // 1. 虚拟机栈中的局部变量8 Object localVar = new Object();9 10 // 2. 方法区中静态变量11 static Object staticVar = new Object();12 13 // 3. 方法区中常量14 final Object finalVar = new Object();15 16 // 4. 本地方法栈中的变量17 // native方法中的变量18 19 // 5. 活跃线程中的对象20 Thread.currentThread();21 }22 23 /**24 * 可达性分析过程25 */26 public static void reachabilityAnalysisProcess() {27 // 可达性分析过程:28 // 1. 从GC Roots开始搜索29 // 2. 搜索过程中经过的对象标记为可达30 // 3. 搜索结束后,未被标记的对象为垃圾31 32 // 示例:对象引用关系33 Object root = new Object(); // GC Root34 Object obj1 = new Object(); // 可达对象35 Object obj2 = new Object(); // 可达对象36 Object obj3 = new Object(); // 不可达对象37 38 // 建立引用关系39 root.ref = obj1;40 obj1.ref = obj2;41 // obj3没有引用指向它,不可达42 43 // 可达性分析结果:44 // root -> obj1 -> obj2 (可达)45 // obj3 (不可达,将被回收)46 }47 48 /**49 * 可达性分析的优势50 */51 public static void reachabilityAnalysisAdvantages() {52 // 优势:53 // 1. 可以解决循环引用问题54 // 2. 准确性高55 // 3. 实现相对简单56 57 // 循环引用示例58 Node node1 = new Node();59 Node node2 = new Node();60 61 // 形成循环引用62 node1.next = node2;63 node2.next = node1;64 65 // 如果没有GC Root指向这个循环,整个循环都会被回收66 node1 = null;67 node2 = null;68 // 可达性分析可以正确识别这种情况69 }70}GC Roots的类型
| GC Roots类型 | 具体内容 | 说明 |
|---|---|---|
| 虚拟机栈中的局部变量 | 方法中的局部变量 | 正在执行的方法中的对象引用 |
| 方法区中的静态变量 | 类的静态字段 | 全局静态对象引用 |
| 方法区中的常量 | final修饰的常量 | 常量池中的对象引用 |
| 本地方法栈中的变量 | native方法中的变量 | JNI调用中的对象引用 |
| 活跃线程中的对象 | Thread对象 | 当前活跃的线程对象 |
2.3 引用类型详解
Java提供了四种引用类型,用于灵活控制对象的生命周期。
强引用(Strong Reference)
1public class StrongReferenceExample {2 3 /**4 * 强引用特点5 */6 public static void strongReferenceCharacteristics() {7 // 强引用是最常见的引用类型8 Object obj = new Object(); // 强引用9 10 // 只要强引用存在,对象就不会被回收11 System.gc(); // 手动触发GC12 System.out.println("Object still exists: " + (obj != null));13 14 // 将引用设为null,对象可以被回收15 obj = null;16 System.gc();17 // 此时对象可以被垃圾收集器回收18 }19 20 /**21 * 强引用的应用场景22 */23 public static void strongReferenceUsage() {24 // 1. 普通对象引用25 String str = "Hello World";26 27 // 2. 集合中的对象28 List<String> list = new ArrayList<>();29 list.add("item");30 31 // 3. 静态变量32 static Object staticObj = new Object();33 34 // 4. 方法参数35 processObject(new Object());36 }37 38 private static void processObject(Object obj) {39 // obj是强引用,方法执行期间不会被回收40 System.out.println("Processing: " + obj);41 }42}软引用(Soft Reference)
1public class SoftReferenceExample {2 3 /**4 * 软引用特点5 */6 public static void softReferenceCharacteristics() {7 // 创建软引用8 SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);9 10 // 获取软引用指向的对象11 byte[] data = softRef.get();12 if (data != null) {13 System.out.println("Data available: " + data.length);14 }15 16 // 模拟内存不足17 List<byte[]> list = new ArrayList<>();18 try {19 while (true) {20 list.add(new byte[1024 * 1024]); // 分配大量内存21 }22 } catch (OutOfMemoryError e) {23 System.out.println("OutOfMemoryError occurred");24 }25 26 // 检查软引用是否被回收27 data = softRef.get();28 if (data == null) {29 System.out.println("Soft reference was cleared");30 }31 }32 33 /**34 * 软引用的应用场景35 */36 public static void softReferenceUsage() {37 // 1. 内存敏感的缓存38 Map<String, SoftReference<byte[]>> cache = new HashMap<>();39 40 // 2. 图片缓存41 Map<String, SoftReference<BufferedImage>> imageCache = new HashMap<>();42 43 // 3. 网页缓存44 Map<String, SoftReference<String>> pageCache = new HashMap<>();45 }46}弱引用(Weak Reference)
1public class WeakReferenceExample {2 3 /**4 * 弱引用特点5 */6 public static void weakReferenceCharacteristics() {7 // 创建弱引用8 WeakReference<Object> weakRef = new WeakReference<>(new Object());9 10 // 获取弱引用指向的对象11 Object obj = weakRef.get();12 System.out.println("Object before GC: " + (obj != null));13 14 // 触发垃圾回收15 System.gc();16 17 // 检查弱引用是否被回收18 obj = weakRef.get();19 System.out.println("Object after GC: " + (obj != null));20 }21 22 /**23 * 弱引用的应用场景24 */25 public static void weakReferenceUsage() {26 // 1. WeakHashMap27 WeakHashMap<Object, String> weakMap = new WeakHashMap<>();28 29 // 2. ThreadLocal中的Entry30 ThreadLocal<String> threadLocal = new ThreadLocal<>();31 32 // 3. 监听器模式33 WeakReference<EventListener> listenerRef = new WeakReference<>(new EventListener());34 }35}虚引用(Phantom Reference)
1public class PhantomReferenceExample {2 3 /**4 * 虚引用特点5 */6 public static void phantomReferenceCharacteristics() {7 // 创建引用队列8 ReferenceQueue<Object> queue = new ReferenceQueue<>();9 10 // 创建虚引用11 PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);12 13 // 虚引用无法获取对象实例14 Object obj = phantomRef.get();15 System.out.println("Phantom reference get: " + obj); // null16 17 // 启动监控线程18 Thread monitorThread = new Thread(() -> {19 try {20 Reference<?> ref = queue.remove();21 System.out.println("Object was garbage collected");22 } catch (InterruptedException e) {23 e.printStackTrace();24 }25 });26 monitorThread.start();27 28 // 触发垃圾回收29 System.gc();30 }31 32 /**33 * 虚引用的应用场景34 */35 public static void phantomReferenceUsage() {36 // 1. DirectByteBuffer回收监控37 // 2. 对象回收状态跟踪38 // 3. 资源清理39 }40}3. 垃圾回收算法详解
垃圾回收算法是JVM内存管理的核心,各种算法都有其特定优缺点和适用场景。随着技术发展,垃圾回收算法从简单的标记-清除,到复制、标记-整理,再到分代收集,不断演进,以适应不同应用场景的需求。
- 标记-清除
- 复制算法
- 标记-整理
- 分代收集
标记-清除算法(Mark-Sweep)
标记-清除算法是最基础的垃圾回收算法,分为标记和清除两个阶段。
算法原理:
- 标记阶段: 从GC Roots开始遍历,标记所有可达对象
- 清除阶段: 遍历整个堆,回收所有未被标记的对象
优缺点分析:
| 优点 | 缺点 |
|---|---|
| 实现简单,易于理解 | 效率不高,需要扫描两次堆 |
| 不需要额外内存空间 | 产生大量内存碎片 |
| 不需要移动对象 | 碎片化导致大对象分配困难 |
适用场景:
- 老年代中对象存活率高的情况
- 内存碎片不敏感的场景
- 适合作为其他算法的基础
1void markSweep() {2 // 标记阶段3 for (Object root : GC_ROOTS) {4 mark(root);5 }6 7 // 清除阶段8 for (MemoryBlock block : HEAP) {9 if (!block.isMarked()) {10 free(block);11 } else {12 block.unmark();13 }14 }15}1617void mark(Object obj) {18 if (obj == null || obj.isMarked()) return;19 20 obj.setMarked(true);21 for (Object ref : obj.getReferences()) {22 mark(ref);23 }24}复制算法(Copying)
复制算法将内存分为两块相等的区域(From空间和To空间),每次只使用其中一块,回收时将存活对象复制到另一块区域。
算法原理:
- 内存划分: 将可用内存划分为两块等大的区域
- 对象分配: 只在其中一个区域(From空间)分配对象
- 复制转移: 当From空间用尽时,将存活对象复制到To空间
- 角色互换: 两个空间角色互换,To空间变为新的From空间
优缺点分析:
| 优点 | 缺点 |
|---|---|
| 高效,只需扫描一次存活对象 | 内存利用率只有50% |
| 无内存碎片,分配高效 | 需要额外空间进行复制 |
| 复制后内存连续 | 对象存活率高时复制开销大 |
适用场景:
- 新生代垃圾回收(对象存活率低)
- Eden区和Survivor区的设计基于此算法
- 存活对象较少的内存区域
1void copying() {2 Address toSpace = TO_SPACE_START;3 4 // 标记并复制存活对象5 for (Object root : GC_ROOTS) {6 toSpace = copy(root, toSpace);7 }8 9 // 清空From空间10 clear(FROM_SPACE_START, FROM_SPACE_END);11 12 // 交换From和To空间13 swap(FROM_SPACE, TO_SPACE);14}1516Address copy(Object obj, Address dest) {17 if (obj == null || obj.isCopied()) 18 return dest;19 20 // 复制对象到To空间21 Address newAddress = dest;22 memcpy(dest, obj, obj.size());23 dest += obj.size();24 25 // 设置转发指针26 obj.setForwardingAddress(newAddress);27 28 // 递归复制引用对象29 for (Object ref : getReferences(newAddress)) {30 dest = copy(ref, dest);31 }32 33 return dest;34}标记-整理算法(Mark-Compact)
标记-整理算法结合了标记-清除和复制算法的优点,不会产生内存碎片,也不需要额外的内存空间。
算法原理:
- 标记阶段: 与标记-清除算法相同,标记所有可达对象
- 计算地址: 计算所有存活对象的新位置
- 整理阶段: 移动存活对象到新位置,使它们紧凑排列
- 更新引用: 更新所有对象引用为新地址
优缺点分析:
| 优点 | 缺点 |
|---|---|
| 无内存碎片 | 效率较低,需要移动对象 |
| 内存利用率高 | 需要更新所有对象引用 |
| 适合对象存活率高的场景 | 停顿时间较长 |
适用场景:
- 老年代垃圾回收
- 对象存活率高的区域
- 对内存碎片敏感的应用
1void markCompact() {2 // 标记阶段3 for (Object root : GC_ROOTS) {4 mark(root);5 }6 7 // 计算新地址8 Address freePointer = HEAP_START;9 for (Object obj : HEAP) {10 if (obj.isMarked()) {11 obj.setForwardAddress(freePointer);12 freePointer += obj.size();13 }14 }15 16 // 移动对象并更新引用17 for (Object obj : HEAP) {18 if (obj.isMarked()) {19 moveObject(obj, obj.getForwardAddress());20 updateReferences(obj);21 obj.unmark();22 }23 }24}分代收集算法
分代收集算法根据对象存活周期的不同,将内存分为几个代,对不同的代采用不同的收集策略。
算法原理:
-
分代假设:
- 大部分对象很快就会死亡(朝生夕死)
- 存活时间长的对象很可能继续存活
-
内存划分:
- 新生代:存放新创建的对象,使用复制算法
- 老年代:存放长期存活的对象,使用标记-整理算法
- 永久代/元空间:存放类信息等,使用标记-清除算法
-
回收策略:
- Minor GC:只回收新生代
- Major GC:只回收老年代
- Full GC:回收整个堆内存
分代内存占比:
| 区域 | 占比 | 存放对象 | 回收频率 | 回收算法 |
|---|---|---|---|---|
| Eden | 新生代的80% | 新创建对象 | 高 | 复制算法 |
| Survivor | 新生代的20% | 存活时间短的对象 | 高 | 复制算法 |
| 老年代 | 堆的60-80% | 长期存活对象 | 低 | 标记-整理 |
适用场景:
- 几乎所有的商业JVM都采用分代收集
- 综合了各种算法的优点
- 适用于大多数Java应用程序
1void generationalCollection() {2 // 判断是否需要Minor GC3 if (youngGenerationFull()) {4 minorGC();5 }6 7 // 判断是否需要Major GC8 if (oldGenerationFull() || tooManyPromotions()) {9 majorGC();10 }11}1213void minorGC() {14 // 使用复制算法回收新生代15 evacuateEden();16 evacuateSurvivorSpace();17 swapSurvivorSpaces();18}1920void majorGC() {21 // 使用标记-整理算法回收老年代22 markOldGeneration();23 compactOldGeneration();24}3.5 算法对比与选择
不同的垃圾回收算法有各自的优缺点,如何选择取决于应用场景和性能需求。
| 算法 | 空间效率 | 时间效率 | 内存碎片 | 移动对象 | 适用场景 |
|---|---|---|---|---|---|
| 标记-清除 | 高 | 中 | 有 | 否 | 老年代,存活率高 |
| 复制 | 低 | 高 | 无 | 是 | 新生代,存活率低 |
| 标记-整理 | 高 | 低 | 无 | 是 | 老年代,内存碎片敏感 |
| 分代收集 | 高 | 高 | 低 | 部分 | 综合场景,大多数应用 |
4. 垃圾收集器详解
垃圾收集器是垃圾回收算法的具体实现,不同的垃圾收集器适用于不同的场景。随着JVM的发展,垃圾收集器也在不断演进,从Serial到Parallel,再到CMS,最后到G1、ZGC、Shenandoah,性能不断提升,暂停时间不断缩短。
- Serial/Serial Old
- Parallel收集器族
- CMS收集器
- G1收集器
Serial/Serial Old收集器
Serial收集器是最古老、最基础的垃圾收集器,它是一个单线程的收集器。Serial Old是其老年代版本。
Serial收集器特点:
- 单线程执行:只使用一个线程进行垃圾收集
- Stop-The-World:收集过程中必须暂停所有用户线程
- 高效简洁:没有线程切换开销,单CPU环境下效率最高
- 内存占用小:对资源要求最低的收集器
适用场景:
- 客户端应用:如桌面应用程序
- 单核CPU环境
- 内存较小(100MB左右)的嵌入式设备
- 对暂停时间不敏感的批处理任务
启用参数: -XX:+UseSerialGC
工作流程:
1void serialGC() {2 // 1. 暂停所有用户线程3 stopAllThreads();4 5 // 2. 执行垃圾收集6 if (needYoungGC()) {7 // 新生代收集 - 复制算法8 evacuateEden(); // 复制Eden区存活对象到Survivor9 evacuateFromSurvivor(); // 复制From Survivor存活对象到To Survivor10 swapSurvivorSpaces(); // 交换From和To Survivor11 }12 13 if (needOldGC()) {14 // 老年代收集 - 标记-整理算法15 markOldObjects(); // 标记存活对象16 compactOldHeap(); // 整理老年代空间17 }18 19 // 3. 恢复所有用户线程20 resumeAllThreads();21}Parallel Scavenge/Parallel Old收集器
Parallel收集器族包括新生代的Parallel Scavenge和老年代的Parallel Old,它们都是多线程并行的收集器,注重高吞吐量。
Parallel收集器特点:
- 多线程并行:利用多核CPU提高收集效率
- 高吞吐量:关注CPU利用率,适合后台运算
- 可控制暂停时间:通过参数调整暂停时间目标
- 自适应调节:根据系统运行情况自动调整参数
适用场景:
- 多核服务器环境
- 对吞吐量要求较高的后台应用
- 批处理系统、科学计算应用
- 不太关心停顿时间的场景
关键参数:
-XX:+UseParallelGC:使用Parallel Scavenge + Serial Old-XX:+UseParallelOldGC:使用Parallel Scavenge + Parallel Old-XX:ParallelGCThreads=n:设置并行收集线程数-XX:MaxGCPauseMillis=n:设置最大GC停顿时间-XX:GCTimeRatio=n:设置吞吐量大小
1void parallelGCWithAdaptive() {2 // 目标最大停顿时间3 int targetPauseTime = getMaxGCPauseMillis(); // -XX:MaxGCPauseMillis4 5 // 目标吞吐量6 int throughputGoal = getGCTimeRatio(); // -XX:GCTimeRatio7 8 // 根据历史GC数据调整新生代大小9 if (actualPauseTime > targetPauseTime) {10 // 减小新生代大小以减少停顿时间11 decreaseYoungGenSize();12 } else if (actualThroughput < throughputGoal) {13 // 增加新生代大小以提高吞吐量14 increaseYoungGenSize();15 }16}CMS收集器 (Concurrent Mark Sweep)
CMS是一种以获取最短回收停顿时间为目标的收集器,适合对响应时间有要求的应用。
CMS收集器特点:
- 低延迟:关注停顿时间,多数工作与用户线程并发执行
- 并发收集:标记和清除阶段与用户线程并发执行
- 标记-清除算法:会产生空间碎片
- CPU敏感:占用部分CPU资源,总吞吐量会下降
- 无法处理浮动垃圾:并发清理阶段产生的新垃圾
CMS执行过程:
- 初始标记:STW,仅标记GC Roots能直接关联的对象
- 并发标记:与用户线程同时执行,遍历对象图
- 并发预清理:与用户线程同时执行,处理并发标记阶段引用变化的对象
- 重新标记:STW,修正并发标记期间因用户程序运行导致的标记变动
- 并发清除:与用户线程同时执行,清除没有标记的对象
- 并发重置:与用户线程同时执行,重置CMS内部数据结构
适用场景:
- Web应用、接口服务等交互系统
- 对停顿时间敏感的场景
- 中等规模堆内存的应用
- 具有多CPU资源的应用服务器
关键参数:
-XX:+UseConcMarkSweepGC:使用CMS收集器-XX:CMSInitiatingOccupancyFraction=n:设置老年代使用率触发CMS的阈值-XX:+UseCMSCompactAtFullCollection:Full GC时进行碎片整理-XX:+CMSClassUnloadingEnabled:允许类卸载
1class CMSChallenges {2 /**3 * 内存碎片问题4 */5 void fragmentationIssue() {6 // CMS使用标记-清除算法,会产生内存碎片7 // 解决方案:8 // 1. -XX:+UseCMSCompactAtFullCollection9 // 在Full GC时进行碎片整理10 // 2. -XX:CMSFullGCsBeforeCompaction=n11 // 每隔n次Full GC后进行一次碎片整理12 }13 14 /**15 * 并发模式失败16 */17 void concurrentModeFailure() {18 // 当CMS正在进行时,老年代空间不足,会触发并发模式失败19 // 切换到Serial Old进行Full GC,STW时间长20 21 // 解决方案:22 // 1. 增大老年代空间23 // 2. 调低CMSInitiatingOccupancyFraction,提前启动CMS24 // 3. 增加并发线程数(-XX:ConcGCThreads)25 }26 27 /**28 * 浮动垃圾问题29 */30 void floatingGarbageIssue() {31 // 并发清理阶段用户线程产生的垃圾无法清理32 // 需要预留空间应对浮动垃圾33 34 // 解决方案:35 // 保守设置CMSInitiatingOccupancyFraction值36 // 默认为92%,一般建议70%-80%37 }38}G1收集器 (Garbage First)
G1是面向服务端的垃圾收集器,JDK 9开始成为默认收集器,设计目标是替代CMS。
G1收集器特点:
- 区域化内存布局:将堆分为多个大小相等的Region
- 并行与并发:充分利用多核CPU
- 分代收集:保留分代概念,但不再物理隔离
- 可预测的停顿时间:建立可预测的停顿时间模型
- 增量式收集:逐步收集整个堆,而不是一次性收集
G1收集过程:
- 年轻代收集:收集所有Eden和Survivor区域
- 并发标记:标记整个堆中存活的对象
- 混合收集:收集所有Eden、Survivor以及部分Old区域
- 必要时Full GC:如果并发收集失败,回退到Full GC
适用场景:
- 大内存多核服务器应用
- 需要低停顿时间的应用
- 需要大内存(超过4GB)的应用
- 对响应时间有较高要求的应用
关键参数:
-XX:+UseG1GC:使用G1收集器-XX:MaxGCPauseMillis=n:设置最大暂停时间目标-XX:G1HeapRegionSize=n:设置Region大小(1MB到32MB,必须是2的幂)-XX:InitiatingHeapOccupancyPercent=n:设置触发并发标记的堆占用率阈值
1class G1CollectionStrategy {2 /**3 * 回收集选择策略4 */5 void collectionSetSelection() {6 // G1会计算每个Region的回收价值:7 // 回收价值 = 回收所获得的空间 / 回收所需时间8 9 List<Region> regions = getAllRegions();10 11 // 根据回收价值对Region排序12 regions.sort(byEvacuationEfficiency());13 14 // 从高到低选择Region,直到达到停顿时间目标15 List<Region> collectionSet = new ArrayList<>();16 long estimatedTime = 0;17 18 for (Region region : regions) {19 if (estimatedTime + region.getEstimatedEvacTime() > maxPauseTime) {20 break;21 }22 collectionSet.add(region);23 estimatedTime += region.getEstimatedEvacTime();24 }25 26 // 回收选定的Region27 evacuateRegions(collectionSet);28 }29 30 /**31 * Remember Set (RSet)32 */33 void rememberSetUsage() {34 // G1中每个Region都维护了RSet35 // RSet记录了哪些外部Region引用了本Region中的对象36 // 这样在收集某个Region时,只需扫描其RSet,而不用扫描整个堆37 }38}4.6 垃圾收集器对比与选择
不同的垃圾收集器有各自的优缺点,适合不同的应用场景。选择合适的垃圾收集器对应用性能至关重要。
| 收集器 | 收集范围 | 线程数 | 算法 | 特点 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 新生代 | 单线程 | 复制 | 简单高效,内存占用小 | 客户端应用,单CPU环境 |
| Serial Old | 老年代 | 单线程 | 标记-整理 | 与Serial配合使用 | 客户端应用,单CPU环境 |
| ParNew | 新生代 | 多线程 | 复制 | Serial的多线程版本 | 与CMS配合使用 |
| Parallel Scavenge | 新生代 | 多线程 | 复制 | 高吞吐量 | 后台运算,批处理 |
| Parallel Old | 老年代 | 多线程 | 标记-整理 | 高吞吐量 | 与Parallel Scavenge配合使用 |
| CMS | 老年代 | 多线程 | 标记-清除 | 低延迟,并发收集 | Web应用,交互系统 |
| G1 | 全堆 | 多线程 | 整体标记-整理 局部复制 | 低延迟,可预测停顿 | 大内存服务器应用 |
| ZGC | 全堆 | 多线程 | 标记-整理 | 超低延迟(< 10ms) | 大内存,延迟敏感应用 |
| Shenandoah | 全堆 | 多线程 | 标记-整理 | 超低延迟 | 大内存,延迟敏感应用 |
- 响应时间优先: G1、ZGC、Shenandoah
- 吞吐量优先: Parallel Scavenge + Parallel Old
- 内存较小: Serial + Serial Old
- 大内存且对延迟敏感: G1、ZGC
- 通用场景: G1 (JDK 9+默认)
5. 实际应用场景
5.1 垃圾收集器选择策略
1public class GarbageCollectorSelection {2 3 /**4 * 根据应用特点选择收集器5 */6 public static void selectCollectorByApplication() {7 // 1. 客户端应用8 // 推荐:Serial收集器9 // 原因:内存小,停顿时间要求不高10 11 // 2. 后台计算应用12 // 推荐:Parallel Scavenge + Parallel Old13 // 原因:关注吞吐量,停顿时间要求不高14 15 // 3. Web应用16 // 推荐:ParNew + CMS 或 G117 // 原因:对响应时间有要求,多CPU环境18 19 // 4. 实时应用20 // 推荐:G1 或 ZGC21 // 原因:对停顿时间要求极高22 23 // 5. 大数据应用24 // 推荐:G125 // 原因:大堆内存,对停顿时间有要求26 }27 28 /**29 * 根据硬件环境选择收集器30 */31 public static void selectCollectorByHardware() {32 // 1. 单CPU环境33 // 推荐:Serial收集器34 // 原因:多线程收集器无法发挥优势35 36 // 2. 多CPU环境37 // 推荐:并行收集器38 // 原因:可以充分利用多核优势39 40 // 3. 大内存环境(> 4GB)41 // 推荐:G1 或 ZGC42 // 原因:适合大堆内存43 44 // 4. 小内存环境(< 1GB)45 // 推荐:Serial 或 Parallel收集器46 // 原因:简单高效47 }48 49 /**50 * 根据性能要求选择收集器51 */52 public static void selectCollectorByPerformance() {53 // 1. 高吞吐量要求54 // 推荐:Parallel Scavenge + Parallel Old55 // 参数:-XX:GCTimeRatio=9956 57 // 2. 低停顿时间要求58 // 推荐:G1 或 ZGC59 // 参数:-XX:MaxGCPauseMillis=20060 61 // 3. 平衡要求62 // 推荐:G1收集器63 // 原因:吞吐量和停顿时间都较好64 }65}5.2 垃圾回收性能优化
1public class GarbageCollectionOptimization {2 3 /**4 * 内存分配优化5 */6 public static void memoryAllocationOptimization() {7 // 1. 对象池模式8 ObjectPool pool = new ObjectPool();9 Object obj = pool.borrow();10 try {11 // 使用对象12 } finally {13 pool.returnObject(obj);14 }15 16 // 2. 预分配容量17 List<String> list = new ArrayList<>(1000);18 19 // 3. 避免大对象创建20 // 使用分块处理大数组21 processLargeArrayInChunks();22 23 // 4. 及时释放引用24 obj = null; // 帮助垃圾回收25 }26 27 /**28 * GC参数优化29 */30 public static void gcParameterOptimization() {31 // 1. 堆内存大小优化32 // -Xms4g -Xmx4g // 避免动态调整33 34 // 2. 新生代大小优化35 // -Xmn1g // 根据对象生命周期调整36 37 // 3. Survivor比例优化38 // -XX:SurvivorRatio=8 // 根据对象存活率调整39 40 // 4. 对象年龄阈值优化41 // -XX:MaxTenuringThreshold=15 // 根据对象存活时间调整42 }43 44 /**45 * 监控和分析46 */47 public static void monitoringAndAnalysis() {48 // 1. GC日志分析49 // -XX:+PrintGCDetails50 // -XX:+PrintGCTimeStamps51 // -Xloggc:gc.log52 53 // 2. 性能监控54 // 使用JVisualVM、JProfiler等工具55 56 // 3. 关键指标57 // - GC频率58 // - GC停顿时间59 // - 内存使用率60 // - 对象分配速率61 }62}6364// 简单的对象池实现65class ObjectPool {66 private Queue<Object> pool = new LinkedList<>();67 68 public Object borrow() {69 return pool.poll() != null ? pool.poll() : new Object();70 }71 72 public void returnObject(Object obj) {73 pool.offer(obj);74 }75}6. 最佳实践总结
6.1 垃圾回收调优原则
- 先分析,后调优:使用监控工具分析性能瓶颈
- 逐步调优:每次只调整一个参数,观察效果
- 监控验证:调优后要持续监控,验证效果
- 回归测试:确保调优不影响功能
1public class GCTuningPrinciples {2 3 /**4 * 调优流程5 */6 public static void tuningProcess() {7 // 1. 性能测试8 PerformanceBaseline baseline = establishBaseline();9 10 // 2. 监控分析11 PerformanceBottleneck bottleneck = analyzeBottleneck();12 13 // 3. 参数调优14 TuningPlan plan = createTuningPlan(bottleneck);15 applyTuningPlan(plan);16 17 // 4. 验证测试18 PerformanceResult result = validateTuning();19 20 // 5. 持续监控21 setupContinuousMonitoring();22 }23 24 /**25 * 常见调优误区26 */27 public static void commonTuningMistakes() {28 // 1. 盲目增加堆内存29 // 问题:可能导致GC停顿时间过长30 // 解决:根据应用特点合理设置31 32 // 2. 过度优化33 // 问题:可能影响系统稳定性34 // 解决:在性能和稳定性间找到平衡35 36 // 3. 忽略监控37 // 问题:调优后不持续监控38 // 解决:建立完善的监控体系39 40 // 4. 参数照搬41 // 问题:不同应用场景需要不同参数42 // 解决:根据应用特点调整参数43 }44}6.2 常见问题解决
1public class CommonProblemSolutions {2 3 /**4 * 频繁GC问题5 */6 public static void frequentGCProblem() {7 // 问题表现:8 // 1. GC频率过高9 // 2. 应用响应时间波动10 // 3. 系统负载高11 12 // 解决方案:13 // 1. 增加堆内存大小14 // 2. 优化对象分配15 // 3. 调整新生代比例16 // 4. 选择合适的收集器17 }18 19 /**20 * 长时间停顿问题21 */22 public static void longPauseProblem() {23 // 问题表现:24 // 1. GC停顿时间过长25 // 2. 应用响应时间不稳定26 // 3. 用户体验差27 28 // 解决方案:29 // 1. 使用低停顿时间收集器(G1、ZGC)30 // 2. 调整停顿时间目标31 // 3. 优化对象分配32 // 4. 减少大对象创建33 }34 35 /**36 * 内存泄漏问题37 */38 public static void memoryLeakProblem() {39 // 问题表现:40 // 1. 内存使用率持续上升41 // 2. 频繁Full GC42 // 3. 最终OOM43 44 // 解决方案:45 // 1. 使用MAT分析堆转储46 // 2. 检查静态集合47 // 3. 检查监听器注册48 // 4. 检查ThreadLocal使用49 }50}7. 总结
Java垃圾回收是Java虚拟机自动内存管理的核心机制,它通过自动识别和回收不再使用的对象来管理堆内存。垃圾回收机制使得Java程序员无需手动管理内存,大大降低了内存泄漏和内存溢出的风险,提高了开发效率和程序稳定性。
在实际应用中,需要根据应用特点、硬件环境和性能要求选择合适的垃圾收集器,并通过合理的参数配置和持续监控来优化垃圾回收性能。
通过深入理解垃圾回收的原理和机制,我们可以:
- 选择合适的收集器:根据应用特点选择最合适的垃圾收集器
- 优化性能参数:通过合理的参数配置提高垃圾回收效率
- 解决性能问题:快速定位和解决垃圾回收相关的性能问题
- 提高系统稳定性:避免内存泄漏和内存溢出问题
8. 面试题精选
8.1 基础概念题
Q: 什么是垃圾回收?
A: 垃圾回收是Java虚拟机自动内存管理的核心机制,它负责自动识别和回收不再使用的对象占用的内存空间。垃圾回收的主要目的是:
- 自动内存管理:程序员无需手动释放内存
- 防止内存泄漏:自动回收无用对象
- 内存碎片整理:整理内存碎片,提高内存利用率
- 性能优化:在回收效率和停顿时间间找到平衡
Q: 如何判断对象是否存活?
A: Java虚拟机使用可达性分析算法来判断对象是否存活:
-
可达性分析:
- 从GC Roots开始搜索
- 搜索不到的对象标记为垃圾
- GC Roots包括:栈中局部变量、静态变量、常量、本地方法栈等
-
引用计数法(Java不使用):
- 为每个对象添加引用计数器
- 引用时计数器+1,引用失效时-1
- 计数器为0时回收
- 缺点:无法解决循环引用问题
8.2 算法原理题
Q: 常见的垃圾回收算法有哪些?
A: 常见的垃圾回收算法包括:
-
标记-清除算法:
- 分为标记和清除两个阶段
- 优点:实现简单,不需要额外空间
- 缺点:效率不高,会产生内存碎片
-
复制算法:
- 将内存分为两块,每次只使用其中一块
- 优点:效率高,没有内存碎片
- 缺点:内存利用率只有50%
-
标记-整理算法:
- 结合了标记-清除和复制算法的优点
- 优点:没有内存碎片,内存利用率高
- 缺点:效率相对较低
-
分代收集算法:
- 根据对象存活周期分代处理
- 新生代使用复制算法
- 老年代使用标记-整理算法
Q: 分代收集算法的原理是什么?
A: 分代收集算法基于以下假设:
-
分代假设:
- 大部分对象都是朝生夕死的
- 经过多次垃圾回收的对象更可能继续存活
- 不同代的对象有不同的特点
-
内存分代:
- 新生代:存放新创建的对象,使用复制算法
- 老年代:存放存活时间长的对象,使用标记-整理算法
- 元空间:存放类信息、常量等
-
对象晋升:
- 对象年龄:每经过一次Minor GC,对象年龄+1
- 晋升条件:年龄达到阈值或Survivor空间不足
- 晋升过程:从新生代晋升到老年代
8.3 收集器选择题
Q: 如何选择合适的垃圾收集器?
A: 垃圾收集器的选择需要考虑以下因素:
-
应用特点:
- 客户端应用:Serial收集器
- 后台计算应用:Parallel Scavenge + Parallel Old
- Web应用:ParNew + CMS 或 G1
- 实时应用:G1 或 ZGC
-
硬件环境:
- 单CPU环境:Serial收集器
- 多CPU环境:并行收集器
- 大内存环境:G1 或 ZGC
- 小内存环境:Serial 或 Parallel收集器
-
性能要求:
- 高吞吐量:Parallel Scavenge + Parallel Old
- 低停顿时间:G1 或 ZGC
- 平衡要求:G1收集器
Q: G1收集器的特点是什么?
A: G1收集器的主要特点包括:
- 并发收集:大部分工作与用户线程并发执行
- 可预测的停顿时间:可以设置停顿时间目标
- Region布局:将堆空间分为多个大小相等的Region
- 优先回收:优先回收垃圾最多的Region
- 标记-整理算法:使用标记-整理算法避免内存碎片
8.4 性能调优题
Q: 如何优化垃圾回收性能?
A: 垃圾回收性能优化的方法包括:
-
内存分配优化:
- 使用对象池减少对象创建
- 预分配集合容量
- 避免大对象创建
- 及时释放对象引用
-
GC参数优化:
- 合理设置堆内存大小
- 优化新生代大小
- 调整Survivor比例
- 设置对象年龄阈值
-
收集器选择:
- 根据应用特点选择合适的收集器
- 调整收集器参数
- 监控收集器性能
-
监控和分析:
- 分析GC日志
- 监控关键指标
- 识别性能瓶颈
Q: 如何分析GC日志?
A: GC日志分析的主要步骤包括:
-
查看GC类型和频率:
- Young GC频率是否过高
- Full GC频率是否过高
- 混合GC的使用情况
-
分析停顿时间:
- 平均停顿时间
- 最大停顿时间
- 停顿时间分布
-
检查内存使用情况:
- 堆内存使用率
- 老年代使用率
- 新生代使用率
-
观察对象分配情况:
- 对象分配速率
- 对象存活时间
- 对象年龄分布
8.5 实践应用题
Q: 如何排查内存泄漏?
A: 内存泄漏排查的主要步骤包括:
-
监控内存使用趋势:
- 观察内存使用是否持续增长
- 分析GC日志中的内存回收情况
- 监控堆内存使用率
-
生成堆转储文件:
- 使用
jmap -dump:format=b,file=heap.hprof <pid> - 在OOM时自动生成堆转储
- 选择合适的时间点生成堆转储
- 使用
-
分析堆转储文件:
- 使用MAT等工具分析
- 查看大对象和对象引用关系
- 识别内存泄漏点
-
常见内存泄漏原因:
- 静态集合持有对象引用
- 监听器未正确移除
- 数据库连接未关闭
- 内部类持有外部类引用
- ThreadLocal使用不当
Q: 在高并发场景下如何优化垃圾回收?
A: 高并发场景下的垃圾回收优化策略:
-
收集器选择:
- 使用G1GC或ZGC
- 设置合适的停顿时间目标
- 优化Region大小
-
内存配置优化:
- 合理设置堆内存大小
- 优化新生代比例
- 使用TLAB优化对象分配
-
对象分配优化:
- 使用对象池减少对象创建
- 优化字符串操作
- 减少临时对象创建
-
监控和调优:
- 持续监控GC性能
- 分析性能瓶颈
- 及时调整参数
- 理解垃圾回收原理:掌握可达性分析、分代收集等核心概念
- 熟悉各种算法:了解标记-清除、复制、标记-整理等算法
- 掌握收集器特点:熟悉各种垃圾收集器的适用场景和特点
- 具备调优能力:能够根据应用特点选择合适的收集器和参数
- 问题诊断能力:能够分析GC日志、排查内存泄漏等常见问题
- 实践经验:具备实际的垃圾回收调优经验
通过本章的学习,你应该已经掌握了Java垃圾回收的核心概念、算法原理和最佳实践。垃圾回收是Java开发中的重要知识,深入理解其原理和机制,对于编写高效、稳定的Java程序至关重要。在实际工作中,需要根据具体的应用场景和性能要求,选择合适的垃圾收集器,并通过合理的调优来保证系统的性能表现。
评论