java -- possibly out of memory or process/resources limits reached

问题:

java运行的时候异常退出:java.lang.outofMemoryError:unabletocreatenativethread: possibly out of memory or process/resources limits reached

排查:

查看 Java 进程当前已使用的线程数
知道限制后,还需要知道当前已经用了多少线程。

方法一:通过 /proc 文件系统

bash
cat /proc//status | grep Threads
输出类似:Threads: 120,表示该进程当前有 120 个线程。

方法二:使用 jstack 或 jvisualvm 等工具

bash
jstack | grep -c "^""
统计线程转储中线程名的行数。

方法三:使用 ps 命令统计某个进程的线程数

bash
ps -T -p | wc -l
(减去标题行,实际线程数为结果减 1)

现场日志:

1.查看当前进程限制:
ps -ef | grep java|grep webservice.jar
cat /proc/{pid}/limits | grep "Max processes"
结果为65535

  1. 查看当前进程已使用
    ps -ef | grep java|grep webservice.jar
    ps -T -p {pid} | wc -l
    结果为21737

  2. dmesg -T|grep oom 没有数据

  3. free -g
    total used free shared buff/cache available
    Mem: 30 13 2 0 13 15
    Swap: 15 0 15

  4. top 虚拟内存一直再涨
    虚拟内存37.2g , RES 9.4G

  5. ulimit -v
    unlimited

  6. cat /proc/sys/kernel/threads-max
    246084

java 现成泄漏定位日志:

  1. 获取线程快照:线程快照(Thread Dump)是分析问题的核心证据。它记录了某一瞬间所有线程的执行状态。使用JDK自带的 jstack 工具就可以轻松生成:
    jstack -l <Java进程PID> > thread_dump.log

  2. 分析日志:
    1)大量线程处于WAITING或TIMED_WAITING状态,但“活锁”或线程池膨胀。
    转储文件开头显示 length=6793,意味着您的JVM中创建了 6793个线程。这是一个非常巨大的数字,是当前最需要关注的问题。
    2)
    "Druid-ConnectionPool-Create-468238626" #43 [1000077] daemon prio=5 os_prio=0 cpu=418.14ms elapsed=66905.98s tid=0x00007f053d14aaf0 nid=1000077 waiting on condition [0x00007f04be9eb000]
    java.lang.Thread.State: WAITING (parking)
    at jdk.internal.misc.Unsafe.park(java.base@21.0.9/Native Method)

    • parking to wait for <0x00000005800008e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
      at java.util.concurrent.locks.LockSupport.park(java.base@21.0.9/LockSupport.java:371)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@21.0.9/AbstractQueuedSynchronizer.java:519)
      at java.util.concurrent.ForkJoinPool.unmanagedBlock(java.base@21.0.9/ForkJoinPool.java:3780)
      at java.util.concurrent.ForkJoinPool.managedBlock(java.base@21.0.9/ForkJoinPool.java:3725)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@21.0.9/AbstractQueuedSynchronizer.java:1746)
      at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2566)

代码分析

这是一个动态数据源管理类,用于管理多个数据库连接。

  1. 动态数据源路由
    继承AbstractRoutingDataSource,实现动态切换数据源

determineCurrentLookupKey() 决定当前线程使用哪个数据源

  1. 数据源加载和初始化
    java
    public void loadDataSources()
    从数据库(gdb_manager表)加载所有有效的数据源配置

为每个配置创建Druid连接池

缓存到dataSourceMap中

  1. 创建数据源连接池
    buildDataSource和buildAndTestDataSource方法:

构建MySQL JDBC URL(使用黄金分片数据库)

设置连接池参数(初始连接数、最大连接数等)

配置超时和重试机制

解密密码(使用AES)

代码问题定位

这个类的设计意图是好的——动态管理多个数据源。但其实现方式正是导致数千个Druid线程被创建的根本原因。
关键问题代码:

构造函数中调用 loadDataSources()

public DynamicDataSource(GdbManagerMapper gdbManagerMapper) {
    this.gdbManagerMapper = gdbManagerMapper;
    this.loadDataSources(); // 初始化时加载配置
}

DynamicDataSource 本身是一个Spring Bean(由 @Component 注解标记)。当Spring容器启动时,会创建这个Bean的实例。在这个过程中,loadDataSources() 方法被调用,它会查询数据库并为每条 GdbManager 记录创建一个 DruidDataSource。

loadDataSources() 方法每次都会创建新数据源

public void loadDataSources() {
    // ... 查询数据库,获取gdbManagers列表 ...
    gdbManagers.forEach(config -> {
        DataSource ds = null;
        try {
            ds = buildDataSource(config); // 每次都创建全新的DruidDataSource
            dataSourceMap.put(config.getId().toString(), ds);
        } catch (Exception e) {
            // ...
        }
    });
    // ...
}

这个方法可以在运行时被多次调用! 例如,可能有一个定时任务或一个管理接口,会定期调用 loadDataSources() 来刷新数据源配置。每次调用,它都会为每一个 config 创建一个全新的 DruidDataSource 实例。

dataSourceMap 的成员变量

private final Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>();

这个Map用来存储所有数据源。当新的数据源被创建并 put 进这个Map时,旧的数据源(如果key相同)会被覆盖。但是,被覆盖的旧 DruidDataSource 并没有被关闭! 它们变成了无人引用的对象,但它们的内部线程(CreateConnectionThread 和 DestroyConnectionThread)却依然在运行,无法被垃圾回收。

posted @ 2026-03-12 10:12  静水深耕,云停风驻  阅读(0)  评论(0)    收藏  举报