You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中FileInputStream写入数组时内存分配失败异常原因探究

问题描述

在Docker/AWS ECS的容器化Java应用中遇到如下问题:无法将文件字节读取到堆上初始化的数组中。简化代码及异常信息如下:

FileInputStream inp = ...;
byte[] buf = new byte[1024];
inp.read(buf, 0, buf.length);  <<< 此处触发异常

异常栈:

Caused by: java.io.IOException: Cannot allocate memory
    at java.base/java.io.FileInputStream.readBytes(Native Method)
    at java.base/java.io.FileInputStream.read(FileInputStream.java:276)

我们创建1kB的堆数组作为临时缓冲区,读取文件流后再复制到native内存(因延迟考量使用Unsafe API,但OOM并非发生在此处)。该异常发生在应用接近容器内存限制时,原本预期OOM会在数组初始化(new byte[1024])时触发——因为Java会将数组初始化为全0,会将页提交到物理内存并触发OOM。但实际异常却发生在向已初始化数组复制数据时,此处本不应有额外内存分配。由于异常发生在readBytes(可能调用底层OS memcpy的native方法),推测是数组复制过程中触发的。请问为何OOM错误会在内存复制阶段而非初始化阶段触发?


问题分析

核心原因在于Linux的延迟内存分配机制,以及JVM数组初始化的实现方式,结合容器的cgroup内存限制,共同导致了这个现象:

  1. 数组初始化未实际分配物理内存
    执行new byte[1024]时,JVM仅在进程的虚拟地址空间中预留了1KB的内存区域,但Linux默认采用延迟物理内存分配——不会立刻将虚拟内存映射到实际的物理内存页。
    对于数组的全0初始化,JVM通常会利用Linux的**零页(Zero Page)**机制:这是一个全局共享的只读物理页,所有新创建的空数组都会映射到这个页,而非单独分配新的物理页。此时进程并没有真正占用额外的物理内存,只是占用了虚拟内存空间。

  2. read操作触发物理内存分配
    调用inp.read(buf, ...)时,native方法readBytes会将文件中的数据写入数组缓冲区。此时需要修改零页的内容,但零页是只读的,Linux会执行**写时复制(Copy-On-Write)**操作:为该数组分配一个新的私有物理页,将零页的内容复制过去,再写入新的数据。
    这一步才是真正向OS申请物理内存的操作。如果此时容器的内存使用已经接近cgroup设置的限制,OS无法分配新的物理页,就会返回ENOMEM错误,最终转化为Java层的IOException: Cannot allocate memory。

  3. 异常类型差异的原因
    Java的OutOfMemoryError是JVM在自身堆内存管理无法满足分配请求时抛出的。但这里的问题是OS层面拒绝了物理内存分配,而非JVM堆内存不足,因此触发的是IOException而非OOM。


内容的提问来源于stack exchange,提问作者Soham Kale

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 05:22:47