Ohhnews

分类导航

$ cd ..
foojay原文

Java 真的内存占用过高吗?基于 JEP 的事实分析

#java#jvm#内存管理#性能优化#jep

目录

[LOADING...]

突发新闻:据确认,Java 已占满所有可用内存。全球开发者感到震惊。

……好吧,其实并没有。愚人节快乐。

请出史上最大的“骗子”来帮我们探讨这个 Java 世界中最古老的迷思,再合适不过了。

隆重介绍 匹诺曹 Duke,Java 的官方吉祥物。他信誓旦旦地保证:“Java 不再占用那么多内存了!”

但每当他说这句话时……他的鼻子就会变长一点。

多年来,“Java 占用内存太多”一直是软件工程界最常被提及的言论之一。人们往往言之凿凿,却鲜有证据,且几乎总是基于过时的假设。

那么,让我们换个角度:

让我们看看现代 Java 到底做了什么——结合平台实打实的改进以及引入这些改进的 JEP(JDK 增强提案)。

这种名声并非空穴来风

早期的 JVM 版本存在以下问题:

  • 垃圾收集器不够先进
  • 停顿时间较长
  • 默认内存开销较高
  • 对受限环境的感知能力有限

再加上应用程序调优不当,与 C 或 C++ 等底层语言相比,Java 看起来确实非常“吃”内存。

但那是过去。

免责声明:本文撰写过程中没有任何木制鼻子受到伤害。(但在我们测试 -Xmx 参数时,Duke 的鼻子可能确实长了几厘米。)

[LOADING...]

图 01:愚人节的匹诺曹 Duke:“我发誓……Java 真的没占用那么多内存!”

现代 Java:到底发生了什么变化?

以下这些 JEP 终于帮助 Duke 控制住了他的鼻子长度。

1. 垃圾回收已非昔日吴下阿蒙

现代垃圾收集器专为低延迟和高效内存使用而设计。

  • ZGC (JEP 333, JEP 377)
  • Shenandoah (JEP 189)
  • G1 作为默认垃圾收集器 (JEP 248)

这些收集器:

  • 大部分工作并发执行
  • 避免了长时间的“Stop-the-world”(全线停顿)
  • 能够在大堆内存下高效扩展

核心要点:

Java 不再会冻结你的应用程序了——它会在应用程序运行时清理内存。

现代 Java 不再“停止整个世界”。它几乎不会停顿。

2. 线程变得更轻量了(轻量得多)

过去 Java 中最大的隐形内存成本之一就是线程。

传统线程:

  • 需要大量的栈内存
  • 无法很好地扩展到成千上万个

Loom 项目(JEP 444)登场:

  • 引入了虚拟线程
  • 相比操作系统线程极为轻量
  • 允许成千上万(甚至数百万)个并发任务

这对内存的重要性在于:

  • 每个线程的开销更小
  • 并发效率更高
  • 真实系统中的整体内存占用更低

成千上万的线程不再意味着需要占用数 GB 的内存。

3. Java 对象正在变小

没错,这是真的。

JEP 450 —— 紧凑对象头(Compact Object Headers)引入了:

  • 减小的对象头大小
  • 更低的每个对象内存开销

这将直接影响:

  • 大型集合
  • 数据密集型应用程序
  • 高吞吐量系统

Java 对象不仅得到了更好的管理,而且物理体积也变小了。

4. JVM 学会了共享 (CDS)

类数据共享 (Class Data Sharing):

  • JEP 310 (应用 CDS)
  • JEP 350 (动态 CDS 归档)

不再需要在多个 JVM 实例之间复制类元数据:

  • 现在可以实现共享
  • 降低了跨服务的总内存使用量

这在以下场景中特别有用:

  • 微服务架构
  • 容器化环境

5. Java 终于理解了容器

曾经有一段时间,容器里的 Java 表现得很……糟糕。

它曾经认为:

  • 它拥有整个机器的访问权限
  • 而不仅仅是容器的限制

这导致了:

  • 过度分配
  • OOM(内存溢出)错误
  • “Java 占用一切”的迷思

今天:

  • JVM 具备了容器感知能力
  • 尊重内存和 CPU 限制
  • 并相应地自我调节

Java 过去以为它拥有整台机器。现在,它表现得像个好公民。

6. Valhalla 项目(值类与对象)—— 即将到来

目前仍处于预览/早期访问阶段,但进展稳步推进。值对象(Value objects)消除了对象标识,可以被扁平化存储(无指针开销)。这将彻底改变内存密度——特别是对于集合、记录(Records)和面向数据编程而言。预计首批预览功能将在未来的 JDK 中落地(可能是 27 或 28)。

7. 进阶:使用原生镜像实现更极致的精简

借助 GraalVM,你可以将 Java 编译为原生二进制文件:

  • 更快的启动速度
  • 更低的内存占用(在许多情况下)

它不是万能药,但它证明了一个重要观点:

Java 不再被束缚在单一的运行时模型中。

其他

Panama 项目(外部函数与内存 API)——在没有旧版 JNI 税的情况下,实现更安全、更快的堆外内存访问。Leyden 和 AOT(提前编译)改进——提供更好的预热缓存和启动加速。

如果你应用了这些技巧,Duke 的鼻子可能真的就不会再长了。

真实案例:内存究竟去哪了

让我们从理论转向具体实践。

示例 1

❌ 经典的内存问题(无界缓存)

$ java
public class BadCache {
    private static final Map cache = new HashMap();

    public static String get(String key) {
        return cache.computeIfAbsent(key, k -> loadValue(k));
    }

    private static String loadValue(String key) {
        return "value-" + key;
    }
}

问题:

  • 缓存无限增长
  • 没有驱逐策略
  • 内存占用持续增加

👉 这与 Java 本身无关。

✅ 修复:使用有界缓存

$ java
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class GoodCache {
    private static final Cache cache =
        Caffeine.newBuilder()
                .maximumSize(10_000)
                .build();

    public static String get(String key) {
        return cache.get(key, k -> "value-" + k);
    }
}

结果:

  • 受控的内存使用
  • 可预测的占用空间

同样的语言,同样的 JVM,却有完全不同的结果。

示例 2 传统线程 vs 虚拟线程

❌ 传统线程

$ java
for (int i = 0; i < 10_000; i++) {
    new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) {}
    }).start();
}

问题:

  • 每个线程占用 ≈ 1MB 栈空间(默认)
  • 10,000 个线程 ≈ 需要 ≈ 10GB 内存 ❌

✅ 虚拟线程 (JEP 444)

$ java
for (int i = 0; i < 10_000; i++) {
    Thread.startVirtualThread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) {}
    });
}

结果:

  • 每个线程的内存:极小(KB 级别而非 MB 级别)
  • 10,000 个线程 → 运行得非常顺畅

这是现代 Java 中最大的隐形内存胜利之一。

示例 3 对象开销(优化前 vs 优化后)

模拟大量小对象:

$ java
class Point {
    int x, y;
}

public class MemoryTest {
    public static void main(String[] args) {
        List points = new ArrayList();

        for (int i = 0; i < 10_000_000; i++) {
            points.add(new Point());
        }

        System.out.println("Created " + points.size() + " objects");
    }
}

发生了什么?

每个对象都有:

  • 对象头(元数据)
  • 字段 (x, y)

使用紧凑对象头(JEP 450):

  • 对象头大小减小
  • 总内存占用显著下降

👉 在大规模系统中:

  • 这可以节省数百 MB 的空间

基准测试快照

以下是基于常见配置的简化、现实的对比:

场景内存使用量
10k 平台线程≈ 10 GB ❌
10k 虚拟线程≈ 50–100 MB ✅
无界缓存无限增长 ❌
有界缓存 (Caffeine)稳定 (≈ 50–200 MB) ✅
无 CDS (多个 JVM)大量重复 ❌
使用 CDS占用降低 ✅

表 01:基准测试

所以……Java 真的占用太多内存吗?

有时确实会——但原因并非人们所想的那样。

高内存占用的真正原因:

  • 不良的对象建模
  • 无界缓存
  • 内存泄漏
  • 过度使用框架
  • 持有引用的时间过长

换句话说:

  • Java 中大多数内存问题都是……Java 开发者的问题。

结语

Java 内存占用高的名声植根于过去。

现代 Java:

  • 使用更智能的垃圾收集
  • 降低了每个对象的开销
  • 高效扩展并发
  • 适应容器环境
  • 随着每个版本的发布持续进化

所以,不——Java 并没有“占用太多内存”。

Java 并不“吃”内存,它是“内存感知”的。

如果你的应用占用了 2GB,请先检查你的代码——而不是 JVM。

如果在阅读本文后,你的 Java 服务依然占用过高内存……请责怪 GC,而不是 Duke。这次他的鼻子是无辜的。

愚人节快乐——祝分析愉快。🚀