在 1核 1G 的极限配置下,Java 和 MySQL 共存且避免 OOM(Out Of Memory),核心逻辑不是“优化”,而是“资源隔离”与“激进裁剪”。
这个配置本质上是不可行的,除非你接受极低的并发、极简的业务逻辑,并采用非常规手段。以下是基于实战经验的真实建议,分为架构层、JVM层、MySQL层和系统层四个维度。
一、 架构选型:放弃重型方案
-
绝对不要使用 Spring Boot + MyBatis/JPA 全套全家桶
- Spring Boot 启动本身就会占用 200MB+ 内存,加上类加载、反射机制,在 1G 总内存中占比过高。
- 替代方案:
- 首选:改用 Go (Gin/Echo) 或 Node.js (NestJS/Express)。Go 的二进制文件内存占用极低,GC 压力小,更适合小内存服务器。
- 次选(如果必须用 Java):使用 Quarkus 或 Micronaut。它们通过 AOT(提前编译)大幅减少启动内存和运行内存,比传统 Spring Boot 节省 30%-50% 内存。
- 最后手段:如果必须用标准 Spring Boot,需剥离所有非核心依赖,仅保留 Web 和 JDBC 最小集。
-
数据库分离是铁律
- 强烈建议:将 MySQL 部署在另一台服务器上(即使是最小的 1核1G 实例)。
- 原因:Java 堆内存 + JVM 元空间 + OS 缓存 + MySQL Buffer Pool = 必然溢出。两者争抢内存会导致 Swap 交换,性能暴跌甚至死锁。
- 如果物理上无法分离(预算限制):必须通过容器化或进程级严格限制实现软隔离。
二、 JVM 调优:极致压缩堆内存
假设你坚持使用 Java,且 Java 和 MySQL 在同一台机器上,你需要为 JVM 分配 ≤ 400MB 的堆内存,留给 OS 和 MySQL 至少 600MB。
1. JVM 参数设置(关键)
# 基础参数
-Xms256m -Xmx256m # 固定堆大小,避免动态扩容导致峰值 OOM
-XX:MetaspaceSize=64m # 元空间初始值
-XX:MaxMetaspaceSize=64m # 元空间最大值,防止类加载过多撑爆内存
# GC 选择
-XX:+UseG1GC # G1 垃圾收集器,对低延迟友好,但需注意配置
# 或者更激进的:
-XX:+UseSerialGC # 单线程 GC,适合 CPU 少、堆小的场景,停顿时间短但吞吐量低
# 其他优化
-XX:+AlwaysPreTouch # 启动时预分配内存,避免运行时因缺页中断导致抖动
-XX:-OmitStackTraceInFastThrow # 调试用,生产环境可关闭以节省内存
-Djava.awt.headless=true # 无头模式,避免图形库报错
2. 应用代码层面的内存控制
- 禁用大对象加载:禁止一次性读取整个文件到内存;禁止返回未分页的大列表。
- 连接池最小化:
# HikariCP 配置示例 spring.datasource.hikari.minimum-idle=1 # 最小连接数设为 1 spring.datasource.hikari.maximum-pool-size=5 # 最大连接数设为 5(默认 10,太高) spring.datasource.hikari.idle-timeout=30000 # 空闲超时缩短 spring.datasource.hikari.max-lifetime=1800000 # 连接生命周期 - 缓存策略:禁用本地缓存(如 Caffeine/Guava Cache),或使用极小容量的 LRU 缓存(max size < 1000 entries)。
三、 MySQL 调优:保守配置
MySQL 默认配置是为多核大内存设计的,在 1G 环境下会直接崩溃。
1. my.cnf 关键配置
[mysqld]
# 内存相关
innodb_buffer_pool_size = 128M # 最大不超过总内存的 30%,这里设 128M 留有余地
query_cache_type = 0 # MySQL 8.0 已移除,5.7 及以下建议关闭,因为竞争锁严重
tmp_table_size = 16M # 临时表大小限制
max_heap_table_size = 16M
# 连接相关
max_connections = 50 # 根据实际并发调整,越小越好
wait_timeout = 10 # 短连接超时,快速释放资源
interactive_timeout = 10
# 日志相关
log_error_verbosity = 2 # 减少错误日志输出开销
general_log = 0 # 关闭通用日志
slow_query_log = 1 # 开启慢查询日志,但路径设在 SSD 上
long_query_time = 1 # 超过 1 秒记录为慢查询
2. SQL 优化原则
- 索引先行:确保所有
WHERE、JOIN、ORDER BY字段都有索引,避免全表扫描导致的内存排序(Using filesort)。 - **避免 SELECT ***:只查询必要字段,减少网络传输和内存解析开销。
- 分页优化:避免
LIMIT 100000, 10,使用覆盖索引或延迟关联。
四、 操作系统层:Swap 与 OOM Killer
1. Swap 策略(双刃剑)
- 不建议完全禁用 Swap:在 1G 内存下,适度使用 Swap 可以作为缓冲,避免进程直接被 Kill。
-
配置建议:
# 创建 1GB swap 文件 dd if=/dev/zero of=/swapfile bs=1M count=1024 mkswap /swapfile swapon /swapfile # 调整 vm.swappiness echo 'vm.swappiness=10' >> /etc/sysctl.conf sysctl -pswappiness=10:表示优先使用物理内存,只有当物理内存使用率极高时才使用 Swap。这能避免频繁交换导致的性能雪崩。
2. 监控与告警
- 安装轻量级监控工具,如 node_exporter + Prometheus + Grafana(注意这些组件本身也占内存,建议仅在本机用
top、free、jstat命令手动监控)。 - 重点监控:
free -m、jstat -gcutil <pid>、mysqladmin status。
五、 终极建议:成本与性能的权衡
现实情况是:1核1G 部署 Java + MySQL 是一个反模式。
| 方案 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|
| Java + MySQL 同机 | 无需额外成本 | 极易 OOM,性能差,维护难 | ⭐ |
| Go/Node.js + MySQL 同机 | 内存占用低,稳定性好 | 需重构代码 | ⭐⭐⭐⭐ |
| Java + MySQL 分离(2台最低配) | 稳定,可扩展 | 成本翻倍 | ⭐⭐⭐⭐⭐ |
| 使用 Serverless 数据库(如阿里云 RDS 按量付费) | 免运维,弹性伸缩 | 网络延迟略高,长期成本高 | ⭐⭐⭐⭐ |
结论:
如果项目是小型个人项目或内部工具,强烈建议将 Java 替换为 Go 或 Node.js,或将 MySQL 迁移至云厂商的免费额度/低价实例(如 AWS Free Tier、阿里云新用户优惠等)。若必须维持当前技术栈,请严格执行上述 JVM 和 MySQL 的激进裁剪参数,并接受其低并发能力。
CLOUD云枢