一、OOM问题到底是个啥玩意儿

咱们程序员最怕看到的错误之一就是"OutOfMemoryError"了。这玩意儿就像是你家冰箱太小,但是你又拼命往里面塞东西,最后冰箱门都关不上了。在Java世界里,JVM就是那个冰箱,而我们的对象就是那些食物。

举个例子,假设我们有个程序不停地往List里加数据:

// 技术栈:Java 8
public class OOMDemo {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            // 每次分配1MB的内存
            byte[] data = new byte[1024 * 1024];
            list.add(data);
            System.out.println("已分配内存: " + list.size() + "MB");
        }
    }
}

运行这个程序,很快你就会看到熟悉的OOM错误。这就像是你不停地往冰箱里塞西瓜,最后冰箱炸了。

二、怎么知道内存被谁吃了

当出现OOM时,我们首先要搞清楚是谁在"偷吃"内存。Java提供了一些很给力的工具:

  1. jmap:可以生成堆内存快照
  2. jvisualvm:图形化查看内存使用情况
  3. jstat:实时监控内存使用

举个实际例子,我们可以这样使用jmap:

# 先找到Java进程ID
jps -l

# 然后生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>

拿到heap.hprof文件后,可以用MAT(Memory Analyzer Tool)来分析。这就像是对你的冰箱拍个X光,看看里面到底塞了些什么。

三、常见的内存泄漏场景

3.1 静态集合滥用

// 技术栈:Java 8
public class StaticCollectionLeak {
    private static List<Object> staticList = new ArrayList<>();
    
    public void addToStaticList(Object obj) {
        staticList.add(obj);  // 这个对象永远不会被GC回收
    }
}

这种问题就像是你把食物放进了一个永远不会清理的储物箱,时间长了自然就满了。

3.2 未关闭的资源

// 技术栈:Java 8
public class ResourceLeak {
    public void readFile() {
        InputStream is = null;
        try {
            is = new FileInputStream("largefile.txt");
            // 处理文件
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 忘记关闭流了!
    }
}

每次调用这个方法都会泄漏一点资源,就像是你开了水龙头忘记关,最后水漫金山。

3.3 缓存失控

// 技术栈:Java 8
public class CacheProblem {
    private Map<String, Object> cache = new HashMap<>();
    
    public void addToCache(String key, Object value) {
        cache.put(key, value);  // 这个缓存会无限增长
    }
}

这种情况需要用WeakHashMap或者设置缓存大小限制,就像是你家储物间得有规矩,不能什么都往里塞。

四、内存调优实战指南

4.1 JVM参数调优

最基本的JVM内存参数:

-Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError

这相当于给你的冰箱设置了最小和最大容量,并且在冰箱爆炸前拍个照留证据。

4.2 选择合适的GC算法

对于不同场景,GC算法选择很重要:

# 适合吞吐量优先的应用
-XX:+UseParallelGC

# 适合低延迟的应用
-XX:+UseG1GC

这就像是你选择不同的清洁工,有的干活快但会打扰你,有的干活慢但很安静。

4.3 对象池技术

对于频繁创建销毁的对象,可以使用对象池:

// 技术栈:Java 8 + Apache Commons Pool
public class ObjectPoolDemo {
    private GenericObjectPool<ExpensiveObject> pool;
    
    public ObjectPoolDemo() {
        pool = new GenericObjectPool<>(new ExpensiveObjectFactory());
        pool.setMaxTotal(100);  // 限制最大数量
    }
    
    public void doWork() {
        ExpensiveObject obj = pool.borrowObject();
        try {
            // 使用对象
        } finally {
            pool.returnObject(obj);
        }
    }
}

这就像是共享单车,大家轮流用,不用每人买一辆。

五、预防胜于治疗

5.1 代码审查要点

  1. 检查所有静态集合的使用
  2. 确保所有资源都有try-with-resources
  3. 缓存必须有大小限制和过期策略

5.2 监控告警设置

在生产环境,我们要设置内存使用监控:

// 技术栈:Java 8 + Spring Boot Actuator
@Configuration
public class MemoryMonitor {
    @Scheduled(fixedRate = 5000)
    public void checkMemory() {
        long max = Runtime.getRuntime().maxMemory();
        long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        double percentage = used * 100.0 / max;
        if (percentage > 80) {
            // 发送告警
        }
    }
}

这就像是在冰箱里装个报警器,快满的时候会滴滴叫。

六、总结与建议

内存问题就像高血压,早期没症状,等有症状时可能已经晚了。所以我们要:

  1. 开发阶段就要注意内存使用
  2. 测试阶段要做压力测试
  3. 生产环境要有完善的监控
  4. 定期进行内存分析

记住,没有一劳永逸的配置,只有持续优化的过程。就像管理你家的储物空间,得经常整理才行。