← 返回首页

Java垃圾回收机制详解

📂 java ⏱ 2 min 347 words

Java垃圾回收机制详解

什么是垃圾回收

垃圾回收(Garbage Collection,GC)是JVM自动管理内存的机制,负责回收不再使用的对象所占用的内存。

如何判断对象已死

引用计数法

每个对象维护一个引用计数器,当有引用指向它时计数器+1,引用失效时-1。

public class ReferenceCountingDemo {
    public static void main(String[] args) {
        Object obj = new Object(); // 引用计数 = 1
        Object another = obj;      // 引用计数 = 2
        obj = null;                // 引用计数 = 1
        another = null;            // 引用计数 = 0,可回收
    }
}

缺点:无法解决循环引用问题。

可达性分析(JVM采用)

从GC Roots出发,遍历所有引用链,不可达的对象判定为垃圾。

public class ReachabilityDemo {
    private static Object gcRoot; // 静态变量是GC Root

    public static void main(String[] args) {
        Object obj1 = new Object(); // 可达
        Object obj2 = new Object(); // 可达
        gcRoot = obj1;              // obj1通过GC Root可达
        obj2 = null;                // obj2不可达,可回收
    }
}

GC Roots包括:

垃圾回收算法

标记-清除(Mark-Sweep)

// 标记阶段:遍历所有对象,标记存活对象
// 清除阶段:遍历堆内存,回收未标记对象
// 缺点:产生内存碎片

复制算法(Copying)

// 将内存分为两块,每次只使用一块
// GC时将存活对象复制到另一块,然后清除当前块
// 新生代采用此算法(Eden:S0:S1 = 8:1:1)

标记-整理(Mark-Compact)

// 标记存活对象,然后将所有存活对象向一端移动
// 清除边界以外的内存
// 老年代采用此算法

分代收集

// 新生代:对象生命周期短,采用复制算法
// 老年代:对象生命周期长,采用标记-整理算法
public class GenerationalDemo {
    public static void main(String[] args) {
        // 短命对象,新生代回收
        for (int i = 0; i < 100000; i++) {
            String temp = "temp" + i; // 每次循环结束可回收
        }
        // 长命对象,进入老年代
        static List<Object> longLived = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            longLived.add(new Object());
        }
    }
}

垃圾收集器

Serial收集器

单线程,简单高效,Client模式下默认。

# 使用Serial收集器
java -XX:+UseSerialGC MyApp

ParNew收集器

Serial的多线程版本,配合CMS使用。

# 使用ParNew收集器
java -XX:+UseParNewGC MyApp

Parallel Scavenge收集器

关注吞吐量,适合后台计算任务。

# 使用Parallel收集器
java -XX:+UseParallelGC MyApp

# 设置吞吐量
java -XX:GCTimeRatio=99 MyApp  # GC时间不超过1%

CMS收集器(Concurrent Mark Sweep)

以最短停顿时间为目标。

# 使用CMS收集器
java -XX:+UseConcMarkSweepGC MyApp

# 设置并发线程数
java -XX:ParallelCMSThreads=4 MyApp

G1收集器(Garbage First)

JDK9+默认,兼顾吞吐量和停顿时间。

# 使用G1收集器
java -XX:+UseG1GC MyApp

# 设置最大停顿时间
java -XX:MaxGCPauseMillis=200 MyApp

# 设置堆大小
java -Xmx4g -XX:+UseG1GC MyApp

ZGC(JDK11+)

超低停顿时间(<10ms)。

# 使用ZGC
java -XX:+UseZGC MyApp

GC日志分析

# 开启GC日志(Java 9+)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

# 开启GC日志(Java 8)
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log MyApp

GC日志示例:

[2024-01-15T10:30:45.123+0800][0.123s][GC (Allocation Failure) [PSYoungGen: 32768K->5120K(38400K)] 32768K->5120K(125952K), 0.0056789 secs]

内存分配策略

// 对象优先在Eden区分配
public void edenAllocation() {
    // 小对象直接在Eden分配
    Object obj = new Object();
}

// 大对象直接进入老年代
public void bigObjectAllocation() {
    // 大于PretenureSizeThreshold的对象直接进老年代
    byte[] bigArray = new byte[4 * 1024 * 1024]; // 4MB
}

// 长期存活对象进入老年代
// 对象每经历一次Minor GC年龄+1,默认15岁进入老年代
// -XX:MaxTenuringThreshold=15

常用GC调优参数

# 堆大小
-Xms512m          # 初始堆大小
-Xmx1024m         # 最大堆大小
-Xmn256m          # 新生代大小

# 新生代比例
-XX:NewRatio=2    # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8  # Eden:S0:S1 = 8:1:1

# GC相关
-XX:+PrintGCDetails    # 打印GC详情
-XX:+PrintGCTimeStamps # 打印GC时间戳

总结

理解GC机制对于Java性能调优至关重要。选择合适的收集器和参数配置,可以在吞吐量和响应时间之间找到平衡点。