Java GC 垃圾回收

60 阅读4分钟

Java GC 垃圾回收

记住:GC就像家里的扫地机器人,会自动清理垃圾,你只需要不要制造太多垃圾就行了!

1. GC 基础概念

什么是垃圾回收?

  • 定义:自动管理内存,清理不再使用的对象
  • 目的:防止内存泄漏,避免手动内存管理
  • 时机:JVM自动触发,无需程序员干预

垃圾对象的判定

// 示例:什么是垃圾对象
public void example() {
    String temp = "临时字符串";  // 方法结束后成为垃圾
    temp = null;              // 显式置空,立即成为垃圾
    
    List<String> list = new ArrayList<>();
    // list 在方法结束前一直被引用,不是垃圾
}

2. Java 内存结构

堆内存 (Heap)
├── 年轻代 (Young Generation)
│   ├── Eden 区          ← 新对象分配
│   ├── Survivor 0 (S0)  ← 存活对象
│   └── Survivor 1 (S1)  ← 存活对象
└── 老年代 (Old Generation) ← 长期存活对象

方法区 (Method Area / Metaspace)
└── 类信息、常量池等

3. GC 算法类型

3.1 复制算法 (Copying)

  • 适用:年轻代
  • 原理:将存活对象复制到另一块内存
  • 优点:无内存碎片,效率高
  • 缺点:浪费一半内存空间
使用前:[A][垃圾][B][垃圾][C]
复制后:[A][B][C][   空闲区域   ]

3.2 标记-清除算法 (Mark-Sweep)

  • 过程:标记垃圾对象 → 清除垃圾对象
  • 优点:不浪费内存
  • 缺点:产生内存碎片

3.3 标记-整理算法 (Mark-Compact)

  • 适用:老年代
  • 过程:标记 → 整理 → 清除
  • 优点:无内存碎片
  • 缺点:整理过程耗时

4. GC 类型

4.1 Minor GC (年轻代GC)

// 触发条件示例
public void createManyObjects() {
    for (int i = 0; i < 100000; i++) {
        String str = "对象" + i;  // 大量对象创建
        // Eden区满了触发Minor GC
    }
}
  • 频率:高
  • 耗时:短(几毫秒到几十毫秒)
  • 影响:小

4.2 Major GC (老年代GC)

  • 触发:老年代空间不足
  • 频率:低
  • 耗时:长
  • 影响:可能造成应用暂停

4.3 Full GC (完整GC)

  • 范围:整个堆 + 方法区
  • 触发
    • 老年代空间不足
    • 方法区空间不足
    • 显式调用 System.gc()
  • 影响:最大,应尽量避免

5. 常见 GC 收集器

5.1 Serial GC

-XX:+UseSerialGC
  • 特点:单线程
  • 适用:小型应用、客户端应用

5.2 Parallel GC (默认)

-XX:+UseParallelGC
  • 特点:多线程并行
  • 适用:服务端应用

5.3 G1 GC

-XX:+UseG1GC
  • 特点:低延迟、可预测停顿时间
  • 适用:大内存应用

6. GC 优化策略

6.1 减少对象创建

// ❌ 不好的做法
public String buildString(List<String> items) {
    String result = "";
    for (String item : items) {
        result += item;  // 每次都创建新的String对象
    }
    return result;
}

// ✅ 好的做法
public String buildString(List<String> items) {
    StringBuilder sb = new StringBuilder();
    for (String item : items) {
        sb.append(item);  // 重用StringBuilder对象
    }
    return sb.toString();
}

6.2 合理设置集合初始容量

// ❌ 不好的做法
List<String> list = new ArrayList<>();  // 默认容量10,可能需要多次扩容

// ✅ 好的做法
List<String> list = new ArrayList<>(1000);  // 预设容量,避免扩容

6.3 及时释放大对象引用

public void processLargeData() {
    List<String> largeList = loadLargeData();
    
    // 处理数据...
    processData(largeList);
    
    // 及时释放引用
    largeList = null;  // 帮助GC回收
}

7. GC 监控与调优

7.1 JVM 参数

# 打印GC信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps

# 设置堆大小
-Xms2g -Xmx4g

# 设置年轻代大小
-Xmn1g

# 设置GC收集器
-XX:+UseG1GC

7.2 监控工具

  • jstat:命令行GC统计
  • jvisualvm:可视化监控
  • GCViewer:GC日志分析
  • Arthas:在线诊断工具

7.3 关键指标

// 示例:监控这些指标
public class GCMetrics {
    // 1. GC频率:Minor GC 和 Full GC 的频率
    // 2. GC耗时:每次GC的停顿时间
    // 3. 内存使用率:堆内存使用情况
    // 4. 吞吐量:应用运行时间 / (应用运行时间 + GC时间)
}

8. 实际案例分析

8.1 代码中的GC考虑点

public Map<Long, JSONObject> getIndexDataInfoByIds(String appkey, String index, DataType dataType, String ids) {
    Map<Long, JSONObject> result = Maps.newHashMap();  // ✅ 局部变量,方法结束后可被回收
    
    String apiBasePath = "";  // ✅ 局部变量,生命周期短
    
    // ⚠️ 这些是临时对象,会产生GC压力:
    String url = String.format(apiBasePath, indexType, ids);  // 字符串格式化产生新对象
    JSONObject responseObj = JSONObject.parseObject(response);  // JSON解析产生新对象
    
    return result;  // 返回后,局部变量成为垃圾
}

8.2 优化建议

// 可以考虑的优化:
private static final ObjectMapper objectMapper = new ObjectMapper();  // ✅ 静态复用,减少对象创建

// 对于频繁调用的方法,可以考虑对象池
private static final Map<String, String> URL_CACHE = new ConcurrentHashMap<>();  // 缓存URL模板

9. 最佳实践总结

9.1 编码习惯

  1. 避免频繁创建临时对象
  2. 合理使用StringBuilder
  3. 及时释放大对象引用
  4. 避免在循环中创建对象

9.2 JVM调优

  1. 根据应用特点选择GC收集器
  2. 合理设置堆内存大小
  3. 监控GC日志,及时发现问题
  4. 避免显式调用System.gc()

9.3 性能目标

  • Minor GC频率:可以较高,但单次耗时要短
  • Full GC频率:应该很低,最好避免
  • GC总耗时:占应用总运行时间的比例要小
  • 内存使用率:保持在合理范围,避免频繁GC