问题

内存泄漏和内存溢出的区别是什么?

答案

核心概念

内存泄漏(Memory Leak)内存溢出(Memory Overflow/OutOfMemory)是两个相关但不同的概念:

  • 内存泄漏: 对象无法被GC回收,但程序不再使用,导致内存逐渐减少
  • 内存溢出: 程序申请内存时,内存空间不足,抛出OutOfMemoryError

关系: 持续的内存泄漏最终会导致内存溢出。

详细对比

维度 内存泄漏 (Memory Leak) 内存溢出 (Memory Overflow)
定义 对象不再使用但无法被GC回收 内存空间不足以分配新对象
本质 内存管理不当(逻辑错误) 内存容量不足(结果)
表现 内存占用持续增长 抛出OutOfMemoryError
时间特征 逐渐发生(慢性) 瞬间发生(急性)
是否异常 不抛异常(隐蔽) 抛出Error异常
是否必然 不一定导致OOM 一定是内存不足
解决方向 修复代码逻辑 增加内存或优化代码

内存泄漏详解

1. 什么是内存泄漏

// 内存泄漏示例
public class MemoryLeakExample {
    private static List<Object> leakList = new ArrayList<>();

    public void addObject() {
        Object obj = new Object();
        leakList.add(obj);  // 对象添加后永远不会被移除
        // obj虽然在方法外不可见,但因为被leakList引用,无法被GC回收
    }

    public static void main(String[] args) {
        MemoryLeakExample example = new MemoryLeakExample();
        while (true) {
            example.addObject();  // 持续泄漏,最终OOM
        }
    }
}

特征:

  • 对象存在但不再使用
  • 存在引用路径到GC Roots,无法回收
  • 可用内存逐渐减少
  • 不会立即抛出异常

2. 常见内存泄漏场景

场景1: 静态集合类引用:

public class StaticCollectionLeak {
    private static Map<String, Object> cache = new HashMap<>();

    public void addToCache(String key, Object value) {
        cache.put(key, value);  // 对象永远不会被移除
    }

    // 正确做法: 使用WeakHashMap或设置过期时间
    private static Map<String, Object> cacheFixed = new WeakHashMap<>();
}

场景2: 监听器未注销:

public class ListenerLeak {
    private EventSource source;

    public void registerListener() {
        source.addListener(new EventListener() {
            @Override
            public void onEvent(Event e) {
                // 处理事件
            }
        });
        // 如果不调用removeListener,listener永远不会被回收
    }

    // 正确做法: 及时注销
    public void unregisterListener(EventListener listener) {
        source.removeListener(listener);
    }
}

场景3: 内部类持有外部类引用:

public class InnerClassLeak {
    private byte[] data = new byte[1024 * 1024];  // 1MB

    public Object createInnerObject() {
        // 非静态内部类持有外部类引用
        return new InnerClass();
    }

    class InnerClass {
        public void doSomething() {
            // 自动持有InnerClassLeak.this引用
        }
    }

    // 正确做法: 使用静态内部类
    static class StaticInnerClass {
        public void doSomething() {
            // 不持有外部类引用
        }
    }
}

场景4: ThreadLocal未清理:

public class ThreadLocalLeak {
    private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();

    public void setThreadLocalData() {
        threadLocal.set(new byte[1024 * 1024]);  // 1MB
        // 如果使用线程池,线程不销毁,ThreadLocal中的数据一直存在
    }

    // 正确做法: 使用后及时清理
    public void cleanThreadLocal() {
        try {
            threadLocal.set(new byte[1024 * 1024]);
            // 使用数据
        } finally {
            threadLocal.remove();  // 清理ThreadLocal
        }
    }
}

场景5: 资源未关闭:

public class ResourceLeak {
    public void readFile(String path) throws IOException {
        FileInputStream fis = new FileInputStream(path);
        // 如果发生异常,fis未关闭,导致文件句柄泄漏
        fis.read();
    }

    // 正确做法: 使用try-with-resources
    public void readFileCorrect(String path) throws IOException {
        try (FileInputStream fis = new FileInputStream(path)) {
            fis.read();
        }  // 自动关闭资源
    }
}

场景6: Hash值改变导致无法移除:

public class HashChangeLeak {
    static class MutableKey {
        private int value;

        public MutableKey(int value) {
            this.value = value;
        }

        @Override
        public int hashCode() {
            return value;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof MutableKey && ((MutableKey) obj).value == this.value;
        }

        public void setValue(int value) {
            this.value = value;  // 修改hash值
        }
    }

    public static void main(String[] args) {
        Map<MutableKey, String> map = new HashMap<>();
        MutableKey key = new MutableKey(1);
        map.put(key, "value");

        key.setValue(2);  // 修改了key的hash值
        map.remove(key);  // 无法移除,因为hash值变了,找不到原位置

        System.out.println(map.size());  // 输出1,对象仍在map中(泄漏)
    }
}

3. 排查内存泄漏

工具1: jmap + MAT:

# 1. 生成堆转储
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# 2. 使用MAT(Memory Analyzer Tool)分析
# - Leak Suspects Report: 自动识别泄漏
# - Dominator Tree: 查看大对象
# - Histogram: 统计对象实例数
# - Path to GC Roots: 查看引用链

工具2: VisualVM:

# 实时监控堆内存
jvisualvm

# 功能:
# - 监控内存使用趋势
# - 生成堆转储
# - 对比多个堆转储(找出增长的对象)

工具3: JProfiler / YourKit:

# 专业的性能分析工具
# - 实时监控对象分配
# - 追踪引用链
# - 检测内存泄漏

内存溢出详解

1. 什么是内存溢出

/**
 * VM Args: -Xmx20m -Xms20m
 */
public class MemoryOverflowExample {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]);  // 每次分配1MB
        }
        // 很快会抛出: java.lang.OutOfMemoryError: Java heap space
    }
}

特征:

  • 内存空间耗尽
  • 抛出OutOfMemoryError
  • 程序无法继续执行
  • 可能是泄漏导致,也可能是正常需求

2. 内存溢出的原因

原因1: 内存泄漏累积:

// 长期运行的应用,内存泄漏累积导致OOM
public class LeakToOOM {
    private static List<Object> leak = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            leak.add(new byte[1024 * 1024]);  // 每秒泄漏1MB
            Thread.sleep(1000);
            // 最终导致OOM
        }
    }
}

原因2: 数据量过大:

// 一次性加载大量数据
public class BigDataOOM {
    public static void main(String[] args) {
        // 尝试一次性加载1GB数据到内存
        List<User> users = userDao.loadAllUsers();  // 假设100万用户
        // 如果堆内存 < 数据量,直接OOM
    }

    // 正确做法: 分批处理
    public void processInBatches() {
        int batchSize = 1000;
        int offset = 0;
        while (true) {
            List<User> batch = userDao.loadUsers(offset, batchSize);
            if (batch.isEmpty()) break;
            processBatch(batch);
            offset += batchSize;
        }
    }
}

原因3: 堆内存配置过小:

# 错误配置
java -Xmx128m -jar large-app.jar  # 大应用配置太小

# 正确配置
java -Xmx4g -jar large-app.jar

两者关系

内存泄漏 → 可用内存持续减少 → 最终内存不足 → 内存溢出
   ↓              ↓                    ↓              ↓
逻辑错误        缓慢发生              达到临界点      抛出OOM

举例说明:

public class LeakToOverflow {
    private static List<Connection> connections = new ArrayList<>();

    public static void getConnection() {
        Connection conn = createConnection();
        connections.add(conn);  // 泄漏: 连接用完未移除
        // 每次调用都泄漏一个连接对象
    }

    public static void main(String[] args) {
        // 第1次调用: 可用内存 = 100MB
        // 第100次调用: 可用内存 = 90MB (内存泄漏)
        // 第1000次调用: 可用内存 = 50MB (继续泄漏)
        // 第10000次调用: 可用内存 = 0MB (内存溢出: OOM)
        while (true) {
            getConnection();
        }
    }
}

实战案例

案例1: 定位内存泄漏

# 步骤1: 发现内存持续增长
jstat -gc <pid> 1000 10  # 每秒输出一次,共10次
# 观察到堆使用量持续增长,Full GC后也不下降

# 步骤2: 生成两次堆转储,对比差异
jmap -dump:live,format=b,file=/tmp/heap1.hprof <pid>
# 等待5分钟
jmap -dump:live,format=b,file=/tmp/heap2.hprof <pid>

# 步骤3: 使用MAT对比两个堆转储
# Histogram -> Compare -> 找出增长最多的对象类型

# 步骤4: 查看引用链
# List with selected class -> Path to GC Roots -> 找出为何无法回收

案例2: 紧急OOM处理

# 1. 自动生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/oom.hprof

# 2. OOM发生后,分析堆转储
# 使用MAT打开oom.hprof
# - Leak Suspects Report: 查看疑似泄漏点
# - Dominator Tree: 查看占用内存最多的对象

# 3. 临时扩大内存恢复服务
-Xmx8g  # 从4G扩大到8G

# 4. 长期修复代码中的泄漏

预防措施

1. 代码规范

// 规范1: 及时关闭资源
try (Connection conn = getConnection()) {
    // 使用连接
}  // 自动关闭

// 规范2: 清理ThreadLocal
try {
    threadLocal.set(value);
    // 使用
} finally {
    threadLocal.remove();
}

// 规范3: 使用弱引用
Map<String, Object> cache = new WeakHashMap<>();  // 允许GC回收

// 规范4: 及时注销监听器
source.addListener(listener);
// 使用完毕
source.removeListener(listener);

2. 监控告警

# 设置内存使用率告警
# 当堆内存使用率 > 80% 时告警

# 监控Full GC频率
# 当Full GC频率过高(如每分钟多次)时告警

# 监控GC耗时
# 当单次GC耗时 > 1s 时告警

3. 压测验证

// 压测时观察内存变化
// 1. 长时间压测(24小时)
// 2. 观察堆内存使用趋势
// 3. 如果内存持续增长,说明存在泄漏

面试总结

内存泄漏(Memory Leak):

  • 定义: 对象不再使用但无法被GC回收
  • 原因: 持有不必要的引用(静态集合、监听器、ThreadLocal等)
  • 表现: 内存占用持续增长,不抛异常
  • 排查: jmap + MAT,对比堆转储,查看引用链
  • 解决: 修复代码逻辑,及时释放引用

内存溢出(Memory Overflow):

  • 定义: 内存空间不足以分配新对象
  • 原因: 内存泄漏累积、数据量过大、配置过小
  • 表现: 抛出OutOfMemoryError
  • 排查: 分析OOM时的堆转储
  • 解决: 增加内存、修复泄漏、分批处理数据

关系: 内存泄漏是原因,内存溢出是结果。持续的内存泄漏最终会导致内存溢出。

关键点:

  • 内存泄漏是逻辑错误(代码问题)
  • 内存溢出是资源不足(容量问题)
  • 预防胜于治疗,规范代码、监控告警、压测验证