Java内存管理与垃圾回收:别让对象悄悄吃光你的堆内存

Java程序时,你有没有遇到过运行一阵子就卡顿、报 OutOfMemoryError,重启一下又好了?或者明明没存多少数据,却提示“内存不足”?这很可能不是电脑太旧,而是Java的内存管理机制在跟你“打招呼”。

Java不手动free,那对象去哪儿了?

和C/C++不同,Java不用程序员自己调 mallocfree。JVM替你管着内存——主要分两块:栈(Stack)和(Heap)。方法里的局部变量、基本类型存在栈上,用完自动消失;而 new 出来的对象,全扔进堆里。堆大,但不是无限大。对象用完了不清理,堆就会越堆越满。

垃圾回收(GC)就是那个“保洁员”

JVM有个后台线程,定期扫描堆,找出那些“没人再用”的对象,把它们占的地儿收回来。怎么判断“没人用”?最常用的是可达性分析:从一组“根对象”(比如栈里的引用、静态变量)出发,顺着引用链能走到的对象,算“活着”;走不到的,就是垃圾。

举个生活例子:你租了个小仓库(堆),放了三箱东西(对象A、B、C)。其中A箱贴了便签“正在用”,B箱的便签被你撕了还塞进了抽屉(引用被置为null),C箱连便签都没贴过(创建后就没被任何变量指向)。保洁员(GC)进来转一圈,只留下A箱,其余两箱清空腾地方。

常见GC触发时机

不一定非等到堆爆了才动手。比如:

  • 每次 new 对象发现堆快满了,先触发一次轻量级回收(Minor GC);
  • 老年代(存活得久的对象)空间紧张,可能来一次 Full GC——这时整个应用会短暂“卡住”,也就是常说的“STW”(Stop-The-World)。

别乱调 System.gc()

有人一看到内存涨了,就手痒写一行:

System.gc();
其实这就像催保洁员“快扫地!”,但人家不一定听——JVM只把它当建议,甚至直接忽略。真想减少GC压力,不如从代码入手:

  • 及时把不用的大对象引用设为 null(尤其在长生命周期对象里);
  • 避免在循环里反复 new 大对象(比如 StringBuilder 可复用,别每次都 new);
  • 慎用静态集合(如 static Map<String, Object>),容易造成对象长期驻留堆中。

一个典型泄漏场景

看这段代码:

public class Cache {
    private static List<Object> dataList = new ArrayList<>();
    
    public static void addToCache(Object obj) {
        dataList.add(obj); // 一直加,从不删
    }
}

只要这个类加载了,dataList 就一直活在方法区的静态区,里面所有 obj 永远可达——哪怕业务逻辑早就不需要它们了。堆内存只会单向增长,直到OOM。

怎么看GC在干啥?

加个JVM参数就能实时观察:
-XX:+PrintGCDetails -Xloggc:gc.log
运行后生成的日志里,能看到每次GC前后的堆大小、耗时、回收了多少——比凭感觉靠谱多了。

内存管理不是玄学,它藏在每行 new 和每个引用背后。理解GC,不是为了去调优生产环境,而是让你写的代码,从第一天起就少埋一颗雷。