
在 Java 开发的职业生涯中,OutOfMemoryError(简称 OOM)就像一颗随时可能引爆的炸弹,常常在生产环境中突然爆发,给系统稳定性带来致命打击。OOM 问题不仅难以排查,更因其出现的随机性和破坏性让许多开发者头疼不已。
据统计,生产环境中约 30% 的严重故障与内存问题相关,而 OOM 更是其中的头号杀手。无论是刚入行的新手还是资深工程师,都可能在不经意间写出导致 OOM 的代码。更棘手的是,很多 OOM 问题在测试环境难以复现,却在流量高峰期突然发难,让排查工作雪上加霜。
本文将系统梳理工作中最常见的几种 OOM 问题,从底层原理到代码实践,从排查方法到解决方案,全方位剖析 OOM 的前世今生。每个案例都基于真实生产场景改编,包含可复现的代码示例和符合阿里巴巴开发规范的最佳实践,帮你彻底掌握 OOM 问题的应对之道。
Java 堆(Heap)是 OOM 问题的重灾区,几乎每个 Java 开发者都或多或少遇到过堆内存溢出的情况。堆内存用于存储对象实例,当我们创建的对象无法被垃圾回收,且总大小超过堆内存限制时,就会抛出Java heap space错误。
堆内存溢出通常有两种常见原因:
内存泄漏是最隐蔽的 OOM 原因,常见于长生命周期对象持有短生命周期对象的引用。例如:静态集合类缓存对象后未及时清理、监听器未正确移除、连接未关闭等。
实战案例:未清理的缓存导致内存泄漏运行
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 演示内存泄漏导致的堆内存溢出
* 场景:静态集合缓存大量临时对象且未清理
*/
public class HeapOOMLeakDemo {
private static final Logger logger = LoggerFactory.getLogger(HeapOOMLeakDemo.class);
// 静态集合,生命周期与应用一致
private static final List<User> userCache = new ArrayList<>();
static class User {
// 模拟用户数据,占用一定内存
private byte[] data = new byte[1024 * 1024]; // 1MB
private String username;
public User(String username) {
this.username = username;
}
}
public static void main(String[] args) {
try {
int count = 0;
while (true) {
// 不断创建用户对象并加入静态缓存
User user = new User("user-" + count);
userCache.add(user);
count++;
if (count % 100 == 0) {
logger.info("已缓存 {} 个用户对象", count);
TimeUnit.MILLISECONDS.sleep(100);
}
}
} catch (InterruptedException e) {
logger.error("线程中断", e);
Thread.currentThread().interrupt();
} catch (OutOfMemoryError e) {
logger.error("发生堆内存溢出", e);
// 实际生产中可在这里添加报警、内存dump等处理
}
}
}运行配置(限制堆大小以快速复现):
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
现象分析:程序会不断创建 User 对象并加入静态集合,由于静态集合的生命周期与应用一致,这些 User 对象永远不会被回收,最终导致堆内存溢出。日志会输出类似:
INFO HeapOOMLeakDemo - 已缓存 100 个用户对象
INFO HeapOOMLeakDemo - 已缓存 200 个用户对象
ERROR HeapOOMLeakDemo - 发生堆内存溢出
java.lang.OutOfMemoryError: Java heap space
at HeapOOMLeakDemo$User.<init>(HeapOOMLeakDemo.java:23)
at HeapOOMLeakDemo.main(HeapOOMLeakDemo.java:34)
即使没有内存泄漏,当应用需要创建的对象总大小超过堆内存限制时,也会发生 OOM。这种情况常见于处理大量数据(如批量导入、大文件解析)时。
实战案例:处理大量数据时的堆溢出
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 演示正常使用但对象过大导致的堆内存溢出
* 场景:批量处理大量数据时内存不足
*/
public class HeapOOMOverflowDemo {
private static final Logger logger = LoggerFactory.getLogger(HeapOOMOverflowDemo.class);
public static void main(String[] args) {
try {
// 模拟处理100万条记录
int batchSize = 1000000;
List<String> dataList = new ArrayList<>(batchSize);
logger.info("开始处理 {} 条数据", batchSize);
for (int i = 0; i < batchSize; i++) {
// 生成随机字符串模拟数据
String data = UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString();
dataList.add(data);
if (i % 100000 == 0) {
logger.info("已处理 {} 条数据", i);
}
}
logger.info("数据处理完成,共 {} 条", dataList.size());
} catch (OutOfMemoryError e) {
logger.error("处理数据时发生堆内存溢出", e);
}
}
}运行配置:
-Xms50m -Xmx50m -XX:+HeapDumpOnOutOfMemoryError
现象分析:程序需要处理 100 万条数据,每条数据都包含 3 个 UUID 字符串,总内存占用超过 50MB 的堆限制,导致 OOM。这种情况并非内存泄漏(数据确实在被使用),而是内存不足。
排查堆内存溢出需要结合日志分析、内存快照和代码审查,常用步骤如下:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof,让 JVM 在 OOM 时自动生成内存快照。MAT 工具使用示例:
针对不同原因的堆内存溢出,解决方案也有所不同:
// 改进:使用WeakHashMap自动回收不再被引用的对象
private static final Map<String, User> userCache = new WeakHashMap<>();
// 或使用Guava的Cache设置过期时间
private static final LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟未访问自动过期
.maximumSize(1000) // 最大缓存数量
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) {
return createUser(key); // 加载用户的逻辑
}
});合理调整堆内存大小:根据应用需求和服务器配置,设置合适的-Xms和-Xmx参数(通常两者设置为相同值避免频繁扩容)
plaintext
# 生产环境常见配置(根据实际情况调整)
-Xms2g -Xmx2g
分批处理数据:对于大量数据,采用分批处理策略,避免一次性加载全部数据到内存
// 改进:分批处理数据
public void processDataInBatches() {
int total = 1000000;
int batchSize = 1000;
int batches = (total + batchSize - 1) / batchSize;
logger.info("共需处理 {} 条数据,分 {} 批处理", total, batches);
for (int i = 0; i < batches; i++) {
int start = i * batchSize;
int end = Math.min(start + batchSize, total);
List<String> batchData = loadBatchData(start, end); // 加载当前批次数据
processBatch(batchData); // 处理当前批次
batchData.clear(); // 主动清理,帮助GC
logger.info("已完成第 {} 批处理,共处理 {} 条", i + 1, end);
}
}List<User> userList =newArrayList<>(100);// 已知大概需要存储100个元素
// 反例
List<User> userList =newArrayList<>();// 未指定初始大小,可能导致多次扩容
在 JDK 8 及以上版本中,方法区的实现由元空间(Metaspace)替代了永久代(PermGen)。元空间用于存储类信息、常量、静态变量、JIT 编译后的代码等。当元空间不足时,会抛出Metaspace相关的 OOM 错误。
元空间溢出通常与类加载有关,常见场景包括:
实战案例:动态生成类导致的元空间溢出
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 演示元空间溢出
* 场景:使用CGLIB动态生成大量代理类
*/
public class MetaspaceOOMDemo {
private static final Logger logger = LoggerFactory.getLogger(MetaspaceOOMDemo.class);
static class TargetClass {
// 目标类,将被动态代理
}
public static void main(String[] args) {
try {
int count = 0;
while (true) {
// 使用CGLIB动态生成代理类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TargetClass.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
// 生成并加载代理类
Class<?> proxyClass = enhancer.createClass();
count++;
if (count % 100 == 0) {
logger.info("已生成 {} 个代理类", count);
}
}
} catch (OutOfMemoryError e) {
logger.error("发生元空间溢出", e);
}
}
}Maven 依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
运行配置(限制元空间大小以快速复现):
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+HeapDumpOnOutOfMemoryError
现象分析:程序使用 CGLIB 不断生成 TargetClass 的代理类,每个代理类都是一个新的 Class 对象,被加载到元空间。由于元空间大小被限制为 10MB,最终会因无法加载新类而抛出 OOM:
INFO MetaspaceOOMDemo - 已生成 100 个代理类
INFO MetaspaceOOMDemo - 已生成 200 个代理类
...
ERROR MetaspaceOOMDemo - 发生元空间溢出
java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.createClass(Enhancer.java:337)
at MetaspaceOOMDemo.main(MetaspaceOOMDemo.java:27)
理解元空间的工作原理有助于我们更好地排查和解决相关 OOM 问题:
MaxMetaspaceSize限制(默认无上限,可能导致系统内存耗尽)。元空间溢出的排查相对复杂,需要结合类加载情况和元空间使用监控:
jstat -gcmetacapacity <pid>查看元空间容量和使用情况jmap -clstats <pid>查看类加载统计信息jcmd <pid> GC.class_stats获取详细的类统计(需要 JDK 10+)针对元空间溢出,主要从合理配置和代码优化两方面解决:
设置元空间初始大小和最大大小:避免元空间无限制增长,也避免初始大小过小导致频繁扩容
# 生产环境常见配置
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
其中,MetaspaceSize是元空间触发 GC 的阈值,MaxMetaspaceSize是元空间的上限。
设置类元数据容量:对于需要加载大量类的应用,可以适当调大
-XX:CompressedClassSpaceSize=128m # 压缩类空间大小(默认1G)
// 改进:缓存代理类,避免重复生成
private static final Map<Class<?>, Class<?>> proxyClassCache = new ConcurrentHashMap<>();
public Class<?> getProxyClass(Class<?> targetClass) {
return proxyClassCache.computeIfAbsent(targetClass, clazz -> {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) ->
proxy.invokeSuper(obj, args));
return enhancer.createClass();
});
}
sun.reflect.GeneratedMethodAccessor),可通过系统属性限制:
-Dsun.reflect.inflationThreshold=15 # 默认为15,超过此阈值会生成动态类
MaxMetaspaceSize,否则可能耗尽系统内存导致进程被杀死。栈溢出虽然不是严格意义上的 OutOfMemoryError,但也是常见的内存相关错误。Java 虚拟机栈用于存储方法调用栈帧(包括局部变量、操作数栈、方法返回地址等),当方法调用深度超过栈的最大深度时,就会抛出StackOverflowError。
栈溢出最常见的原因是无限递归调用,此外,单个方法占用栈空间过大也可能导致栈溢出。
实战案例:无限递归导致的栈溢出
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 演示栈溢出错误
* 场景:无限递归调用
*/
public class StackOverflowDemo {
private static final Logger logger = LoggerFactory.getLogger(StackOverflowDemo.class);
private static int recursionDepth = 0;
// 递归方法
private static void recursiveMethod() {
recursionDepth++;
// 每1000层打印一次
if (recursionDepth % 1000 == 0) {
logger.info("递归深度:{}", recursionDepth);
}
// 无限递归调用
recursiveMethod();
}
public static void main(String[] args) {
try {
logger.info("开始递归调用");
recursiveMethod();
} catch (StackOverflowError e) {
logger.error("发生栈溢出,最大递归深度:{}", recursionDepth, e);
}
}
}
运行配置(可指定栈大小):
-Xss128k # 设置每个线程的栈大小为128KB(默认通常为1MB左右)
现象分析:程序中的recursiveMethod()不断调用自身,每次调用都会在虚拟机栈中创建一个新的栈帧。当调用深度超过栈的最大容量时,就会抛出栈溢出错误:
INFO StackOverflowDemo - 开始递归调用
INFO StackOverflowDemo - 递归深度:1000
INFO StackOverflowDemo - 递归深度:2000
...
ERROR StackOverflowDemo - 发生栈溢出,最大递归深度:3999
java.lang.StackOverflowError
at StackOverflowDemo.recursiveMethod(StackOverflowDemo.java:18)
at StackOverflowDemo.recursiveMethod(StackOverflowDemo.java:22)
at StackOverflowDemo.recursiveMethod(StackOverflowDemo.java:22)
...
除了无限递归,以下场景也可能导致栈溢出:
深层次的方法调用链:例如,在多层嵌套的框架中(如深层调用的 RPC、多层拦截器),即使没有递归,过深的调用链也可能导致栈溢出。
方法内创建大型数组:局部变量存储在栈帧中,创建大型数组会占用大量栈空间
// 可能导致栈溢出的代码
publicvoidlargeArrayInStack(){
// 在栈上分配大型数组(某些JVM可能优化到堆上,但理论上可能导致栈溢出)
int[] largeArray =newint[1000000];
}
复杂的表达式计算:某些复杂的表达式可能导致操作数栈深度超过限制
栈溢出的排查相对直接,主要依靠错误堆栈信息:
StackOverflowError的堆栈信息会显示方法调用链,通过堆栈可以定位到递归或深层调用的位置。-Xss参数)。针对不同原因的栈溢出,解决方案如下:
确保递归有正确的终止条件:检查递归的终止条件是否正确,是否存在逻辑错误导致无法终止
// 改进:添加正确的终止条件
privatestaticlongfactorial(int n){
// 终止条件
if(n <=1){
return1;
}
// 递归调用
return n *factorial(n -1);
}
将递归改为迭代:对于深度较大的递归,可改为循环迭代方式,避免栈溢出
// 改进:将递归改为迭代
privatestaticlongfactorialIterative(int n){
long result =1;
for(int i =2; i <= n; i++){
result *= i;
}
return result;
}
增加递归深度限制:在递归方法中添加最大深度限制,防止无限递归
private static final int MAX_DEPTH=10000;
private static voidsafeRecursiveMethod(int depth){
if(depth >MAX_DEPTH){
logger.warn("达到最大递归深度,终止递归");
return;
}
// 业务逻辑...
safeRecursiveMethod(depth +1);
}
优化调用链结构:减少不必要的中间层,简化方法调用链
使用尾递归优化:如果必须使用递归,尽量使用尾递归(递归调用是方法的最后一个操作),某些 JVM 会对尾递归进行优化(但 Java 标准 JVM 通常不支持)
适当增大栈空间:对于确实需要较深调用栈的场景(如深度嵌套的数据结构处理),可以适当调大线程栈大小
# 增大栈大小(根据需要调整)
-Xss512k
当 JVM 无法创建新的 native 线程时,会抛出unable to create new native thread错误。这通常不是因为 JVM 内存不足,而是由于系统限制或资源耗尽导致无法创建更多线程。
这种 OOM 通常发生在以下情况:
实战案例:创建过多线程导致的 OOM
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* 演示无法创建新线程的OOM
* 场景:无限制地创建新线程
*/
public class TooManyThreadsOOMDemo {
private static final Logger logger = LoggerFactory.getLogger(TooManyThreadsOOMDemo.class);
public static void main(String[] args) {
List<Thread> threads = new ArrayList<>();
int threadCount = 0;
try {
while (true) {
// 创建新线程
Thread thread = new Thread(() -> {
try {
// 线程创建后进入休眠,不释放资源
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(thread);
thread.start();
threadCount++;
if (threadCount % 100 == 0) {
logger.info("已创建 {} 个线程", threadCount);
}
}
} catch (OutOfMemoryError e) {
logger.error("无法创建新线程,已创建线程数:{}", threadCount, e);
}
}
}现象分析:程序不断创建新线程并让它们进入长时间休眠,每个线程都会占用系统资源(包括内存和线程表项)。当线程数量达到系统限制时,JVM 无法创建新线程,抛出 OOM:
INFO TooManyThreadsOOMDemo - 已创建 100 个线程
INFO TooManyThreadsOOMDemo - 已创建 200 个线程
...
ERROR TooManyThreadsOOMDemo - 无法创建新线程,已创建线程数:32767
java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at TooManyThreadsOOMDemo.main(TooManyThreadsOOMDemo.java:27)
不同系统和配置下,可创建的最大线程数不同,主要受以下因素影响:
ulimit -u在 Linux 上的设置)-Xss参数,栈越大,能创建的线程越少)每个线程的创建都会消耗系统资源,主要包括:
-Xss参数控制,默认通常为 1MB 左右。创建 1000 个线程至少需要 1GB 内存(仅线程栈)。因此,线程并非越多越好,合理控制线程数量对系统稳定性至关重要。
排查此类 OOM 需要结合系统配置和应用线程创建逻辑:
jstack <pid>查看当前线程数量和状态top -H -p <pid>查看进程的线程数jstack输出中,查看线程名称和堆栈,确定哪些组件在创建线程ulimit -u查看用户最大进程数(线程在 Linux 上以轻量级进程实现)/proc/<pid>/limits中Max processes的限制-Xss参数设置,过大的栈大小会减少可创建的线程数解决此类 OOM 的核心是控制线程数量和优化线程使用:
线程池是控制线程数量的最佳实践,它通过复用线程减少线程创建开销,同时限制最大线程数:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 改进:使用线程池管理线程
*/
public class ThreadPoolSolution {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolSolution.class);
// 推荐:使用ThreadPoolExecutor而非Executors创建线程池,明确参数含义
private static final ExecutorService threadPool = new ThreadPoolExecutor(
10, // 核心线程数
100, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(1000), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
public static void main(String[] args) {
try {
int taskCount = 0;
while (true) {
taskCount++;
final int taskId = taskCount;
threadPool.submit(() -> {
try {
// 模拟任务执行
TimeUnit.SECONDS.sleep(1);
if (taskId % 100 == 0) {
logger.info("完成任务:{}", taskId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 控制任务提交速度,避免过快压满队列
if (taskCount % 100 == 0) {
TimeUnit.MILLISECONDS.sleep(100);
}
}
} catch (InterruptedException e) {
logger.error("主线程中断", e);
Thread.currentThread().interrupt();
} finally {
threadPool.shutdown();
}
}
}根据业务场景合理配置线程池参数:
线程泄漏是指线程创建后无法正常终止,导致线程数量不断增加:
正确处理线程中断:线程应响应中断信号,及时退出
// 正确处理中断的线程
Thread worker =newThread(()->{
while(!Thread.currentThread().isInterrupted()){
try{
// 执行任务
doWork();
// 休眠时也能响应中断
TimeUnit.MILLISECONDS.sleep(100);
}catch(InterruptedException e){
// 重新设置中断状态
Thread.currentThread().interrupt();
break;
}
}
logger.info("线程退出");
});
避免线程阻塞在无超时的操作上:如无超时的Object.wait()、BlockingQueue.take()等,应使用带超时的版本
使用守护线程(Daemon Thread):对于后台服务线程,可设置为守护线程,在主线程退出时自动终止
Thread daemonThread =newThread(()->{
// 后台任务逻辑
});
daemonThread.setDaemon(true);// 设置为守护线程
daemonThread.start();
在确实需要更多线程的情况下,可以适当调整系统限制(需谨慎,可能影响系统稳定性):
Linux 系统:
# 临时调整用户最大进程数(线程数)
ulimit-u65535
# 永久调整(需要root权限)
echo"username soft nproc 65535">> /etc/security/limits.conf
echo"username hard nproc 65535">> /etc/security/limits.conf
减小线程栈大小:在确保不发生栈溢出的前提下,适当减小-Xss参数,增加可创建的线程数
-Xss256k # 将线程栈大小设置为256KB
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
【强制】线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
【强制】线程池参数必须根据业务场景进行调整,核心线程数、最大线程数、队列大小等不能使用默认值。
【建议】线程池使用后必须正确关闭,避免资源泄漏。
【建议】线程池应命名,便于问题排查:
// 为线程池中的线程设置名称前缀
ThreadFactory namedThreadFactory =newThreadFactoryBuilder()
.setNameFormat("business-pool-%d").build();
ExecutorService pool =newThreadPoolExecutor(..., namedThreadFactory,...);
直接内存(Direct Memory)是 Java NIO 提供的一种基于操作系统直接内存的缓冲区,它不在 JVM 堆内存中,而是使用本地内存。当直接内存分配超过限制时,会抛出Direct buffer memory的 OOM 错误。
直接内存溢出通常发生在频繁使用 NIO 的场景,特别是:
DirectByteBuffer且未及时释放实战案例:大量分配直接内存导致的 OOM
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* 演示直接内存溢出
* 场景:大量分配DirectByteBuffer且未释放
*/
public class DirectMemoryOOMDemo {
private static final Logger logger = LoggerFactory.getLogger(DirectMemoryOOMDemo.class);
public static void main(String[] args) {
List<ByteBuffer> buffers = new ArrayList<>();
int count = 0;
try {
// 每次分配1MB的直接内存
int bufferSize = 1024 * 1024;
while (true) {
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize);
buffers.add(buffer);
count++;
if (count % 100 == 0) {
logger.info("已分配 {} 个直接缓冲区,共 {} MB", count, count);
}
}
} catch (OutOfMemoryError e) {
logger.error("发生直接内存溢出,已分配 {} 个缓冲区", count, e);
}
}
}运行配置(限制直接内存大小):
-XX:MaxDirectMemorySize=100m # 限制直接内存最大为100MB
现象分析:程序不断分配 1MB 的直接内存缓冲区并保存引用,由于直接内存被限制为 100MB,当分配到约 100 个缓冲区时会抛出 OOM:
INFO DirectMemoryOOMDemo - 已分配 100 个直接缓冲区,共 100 MB
ERROR DirectMemoryOOMDemo - 发生直接内存溢出,已分配 100 个缓冲区
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at DirectMemoryOOMDemo.main(DirectMemoryOOMDemo.java:25)
直接内存与堆内存相比有以下特点:
Unsafe.allocateMemory()分配,通过Unsafe.freeMemory()释放。DirectByteBuffer的回收依赖于 Cleaner(虚引用)机制,当DirectByteBuffer对象被 GC 回收时,其关联的 Cleaner 会触发直接内存的释放。-XX:MaxDirectMemorySize参数指定上限。如果未指定,默认与堆内存最大值(-Xmx)相同。MaxDirectMemorySize,会触发 Full GC 尝试回收直接内存。如果回收后仍不足,则抛出 OOM。直接内存溢出的排查相对复杂,因为它不在 JVM 堆内存中:
jconsole或jvisualvm监控直接内存使用(需 JDK 8+)sun.misc.VM.maxDirectMemory()和sun.misc.SharedSecrets.getJavaNioAccess().getDirectBufferPool().getTotalCapacity()获取直接内存限制和当前使用量ByteBuffer.allocateDirect()的地方DirectByteBuffer对象的数量和引用情况top、free等工具监控系统整体内存使用,判断是否是系统内存耗尽导致解决直接内存溢出需要从合理使用和正确释放两方面入手:
根据应用需求设置合适的直接内存上限:
# 根据实际情况调整,通常不需要超过物理内存的一半
-XX:MaxDirectMemorySize=512m
主动释放直接内存:对于不再使用的DirectByteBuffer,可以通过反射调用其cleaner的clean()方法主动释放
importsun.misc.Cleaner;
importjava.lang.reflect.Field;
importjava.nio.ByteBuffer;
// 主动释放直接内存的工具方法
publicstaticvoidfreeDirectBuffer(ByteBuffer buffer){
if(buffer.isDirect()){
try{
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner =(Cleaner) cleanerField.get(buffer);
if(cleaner !=null){
cleaner.clean();
}
}catch(Exception e){
logger.error("释放直接内存失败", e);
}
}
}
避免长时间持有 DirectByteBuffer 引用:及时将不再使用的DirectByteBuffer对象置为 null,使其能被 GC 回收,进而触发直接内存释放
使用 try-with-resources:对于实现了AutoCloseable接口的缓冲区相关资源,使用 try-with-resources 确保释放
try(FileChannel channel =newFileInputStream("largeFile.txt").getChannel()){
ByteBuffer buffer =ByteBuffer.allocateDirect(1024*1024);
while(channel.read(buffer)!=-1){
// 处理数据
buffer.clear();
}
}catch(IOException e){
logger.error("文件处理错误", e);
}
复用 DirectByteBuffer:对于频繁分配和释放直接内存的场景,使用对象池复用DirectByteBuffer
// 直接缓冲区对象池示例
publicclassDirectBufferPool{
privatefinalQueue<ByteBuffer> pool;
privatefinalint bufferSize;
privatefinalint maxSize;
publicDirectBufferPool(int bufferSize,int maxSize){
this.bufferSize = bufferSize;
this.maxSize = maxSize;
this.pool =newArrayDeque<>(maxSize);
}
// 从池中获取缓冲区
publicByteBufferborrowBuffer(){
ByteBuffer buffer = pool.poll();
return buffer !=null? buffer :ByteBuffer.allocateDirect(bufferSize);
}
// 将缓冲区归还给池
publicvoidreturnBuffer(ByteBuffer buffer){
if(buffer !=null&& buffer.capacity()== bufferSize && pool.size()< maxSize){
buffer.clear();
pool.offer(buffer);
}else{
// 不符合复用条件的缓冲区主动释放
freeDirectBuffer(buffer);
}
}
}
合理设置缓冲区大小:避免分配过大的直接缓冲区,根据实际需求调整
批量处理 IO 操作:减少 IO 操作次数,降低直接内存使用频率
许多 IO 框架(如 Netty、Jetty 等)内部使用了直接内存,需确保其配置合理:
Netty:检查ByteBufAllocator配置,避免过度分配
// 合理配置Netty的内存分配器
ServerBootstrap bootstrap =newServerBootstrap();
bootstrap.option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT);
调整框架内存参数:根据框架文档设置合理的内存上限和回收策略
直接内存和堆内存各有优劣,应根据场景选择:
无论哪种类型的 OOM,都有一些通用的排查方法和预防策略。掌握这些方法可以帮助我们快速定位问题并提前预防。
-XX:+HeapDumpOnOutOfMemoryError,获取内存快照规范编码习惯:
合理配置 JVM 参数:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
-XX:MaxDirectMemorySize=1g \
-Xss256k \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:GCLogFileSize=100m -XX:NumberOfGCLogFiles=10 -Xloggc:/var/log/gc.log \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
实施监控告警:
压力测试与容量规划:
定期检查与优化:
掌握以下工具和命令可以大大提高 OOM 问题的排查效率:
JDK 自带工具:
jps:查看 Java 进程 IDjstat:监控 JVM 统计信息(GC、类加载等)jstack:获取线程堆栈信息jmap:生成内存快照、查看内存使用统计jconsole/jvisualvm:图形化监控工具第三方分析工具:
常用命令示例:
# 查看Java进程
jps -l
# 监控GC情况(每5秒输出一次)
jstat -gcutil<pid>5000
# 生成内存快照
jmap -dump:format=b,file=heapdump.hprof <pid>
# 查看线程情况
jstack <pid>> threads.txt
# 查看类加载统计
jmap -clstats<pid>
OOM 问题是 Java 开发中绕不开的挑战,但并非无法攻克。通过本文的系统学习,我们可以看到,每种 OOM 都有其特定的成因和解决方案:
DirectByteBuffer的释放和复用,合理配置直接内存大小。预防 OOM 问题的关键在于:
最后需要强调的是,OOM 问题的排查是一个实践性很强的技能,需要结合理论知识和实际经验。在日常开发中,我们应该养成关注内存使用的习惯,定期进行代码审查和性能测试,将 OOM 问题消灭在萌芽状态。