theme: default themeName: "默认主题" title: "Linux服务器内存总不够用?这套优化方案帮你省下一半资源"
Linux服务器内存总不够用?这套优化方案帮你省下一半资源
"老师,我8G内存的服务器,跑了两个Java服务就满了,怎么办?加内存还是换服务器?"
这是我收到最多的问题之一。我的回答通常是:先别花钱,让我看看你的内存都去哪了。
十次里面有八次,问题不是内存不够,而是内存被浪费了。今天把完整的内存优化方法论写出来,按这个做,你的服务器至少能省出30%-50%的内存。
第一步:搞清楚内存去哪了
不测量就优化等于蒙着眼开车。先看内存分布:
# 总体内存使用
free -h
各进程内存占用TOP20
ps aux --sort=-%mem head -21
更直观的方式
smem -t -k -s rss
`smem`比`ps`更准确,因为它统计的是PSS(比例集大小),而不是简单的RSS。RSS会把共享库重复计算,PSS则按比例分摊。
如果`smem`没装:
# CentOS/RHEL
yum install smem
Ubuntu/Debian
apt install smem
看完了内存分布,一般会发现这几类"内存大户":
1. Java/Python等运行时环境——占大头 2. 数据库缓冲池——占次大头 3. 大量重复的进程——隐形浪费 4. Page Cache——正常占用,但可以被回收
第二步:优化Java服务内存
Java是内存浪费的重灾区。很多人的JVM参数直接用的默认值,或者网上抄了一段就用了。
堆内存不是越大越好
# 查看当前JVM参数
jcmd VM.flags
常见误区:给Java堆分配6GB,以为越大越好。实际上:
- 堆太大 → GC暂停时间越长 → STW(Stop The World)时间越长
- 堆太大 → 对象晋升到老年代更容易 → Full GC更慢
- 堆太大 → 操作系统给Page Cache留的空间少 → 文件IO性能下降
JDK版本的选择
java -version
如果你还在用JDK 8,强烈建议升级到JDK 17或21。G1GC和ZGC的内存利用率比JDK 8的默认GC高很多:
# JDK 17+ 推荐参数
java -Xms2g -Xmx2g \ -XX:+UseZGC \ -XX:+ZGenerational \ -XX:MaxGCPauseMillis=50 \ -jar myapp.jar
ZGC的内存利用率比G1GC高15%-20%,而且暂停时间控制在50ms以内。
一个容易忽略的参数:MaxDirectMemorySize
如果你的应用用了Netty、NIO等直接内存操作,堆外内存可能是隐形的大头:
# 查看进程的内存映射
pmap -x tail -5
限制直接内存大小:
-XX:MaxDirectMemorySize=512m
第三步:数据库内存优化
MySQL缓冲池
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
很多人知道要设置这个参数,但设置得不合理。原则:
- 单实例MySQL: 物理内存的70%-80%
- 多实例/混部: 物理内存的50%左右,给OS和其他进程留空间
- 绝对不要超过物理内存! 否则会触发swap
还有一个小技巧——多缓冲池实例:
-- 缓冲池大于1GB时,设置多个实例减少锁竞争
SET GLOBAL innodb_buffer_pool_instances = 8;
Redis内存优化
# 查看Redis内存使用
redis-cli info memory
Redis的内存优化有几个关键点:
1. 用Hash代替大量的Key-Value
# 不好:100万个用户信息,每个一个key
SET user:1:name "张三" SET user:1:age "28"
100万个key
好:用Hash结构
HSET user:1 name "张三" age "28"
50万个key
当Hash的field数量少于`hash-max-ziplist-entries`(默认512)且值小于`hash-max-ziplist-value`(默认64字节)时,Redis会用ziplist编码,内存占用能减少50%-70%。
2. 设置合理的过期策略
# 查看过期key比例
redis-cli info stats grep expired
3. 开启lazy-free
# redis.conf
lazyfree-lazy-eviction yes lazyfree-lazy-expire yes lazyfree-lazy-server-del yes
删除大key时异步释放内存,避免阻塞主线程。
第四步:减少进程重复
这个浪费很隐蔽。看个真实案例:
# 统计各类型进程的数量
ps -eo comm sort uniq -c sort -rn head -20
客户服务器上有17个Python进程、9个Node.js进程,每个都加载了完整的运行时环境。
Python进程优化: 用multiprocessing的spawn模式换成fork模式共享内存页;或者把多个功能合并到一个进程里用协程调度。 Node.js进程优化: 如果是Cluster模式,确认worker数量是否合理。很多人设成CPU核心数,但每个worker要加载完整的V8引擎,8核就8份V8内存。如果请求量不大,4个worker就够了。第五步:系统级优化
Swappiness
# 查看当前值
cat /proc/sys/vm/swappiness
默认60,太高了
改为10:尽量不用swap,但不完全禁用
sysctl vm.swappiness=10
完全禁用swap(设为0)不推荐——OOM Killer比swap更可怕。
Transparent Huge Pages
# 查看THP状态
cat /sys/kernel/mm/transparent_hugepage/enabled
对数据库来说,THP是个坑。它会导致内存碎片和延迟抖动。MySQL官方文档明确建议关闭:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
vm.overcommit_memory
# 对Redis服务器,设为1允许超额分配
sysctl vm.overcommit_memory=1
Redis在fork子进程做RDB持久化时,需要和父进程一样的内存空间。如果overcommit设为0(默认),fork可能因为"内存不够"而失败。
清理Page Cache(紧急情况)
# 只清理Page Cache(不影响dentries和inodes)
sync && echo 1 > /proc/sys/vm/drop_caches
清理Page Cache + dentries + inodes
sync && echo 3 > /proc/sys/vm/drop_caches
注意:这只是应急手段,不要写到定时任务里! Page Cache是操作系统的性能优化,频繁清理反而降低IO性能。
第六步:容器环境的特殊优化
如果你跑在K8s/Docker里,内存优化还有额外的坑:
合理设置requests和limits
resources:
requests: memory: "1Gi" # 保底值,K8s调度依据 limits: memory: "2Gi" # 上限,超过会被OOM Kill
关键原则: requests和limits的差距不要超过2倍。差距太大 → 节点超卖严重 → 突发流量时OOM Kill集群雪崩。
JVM要感知容器内存限制
JDK 8u191+ 和 JDK 11+ 默认支持容器内存感知,但老版本JDK需要手动开启:
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0 # 使用容器内存限制的75%
不要用`-Xmx`写死堆大小,用`MaxRAMPercentage`让JVM自适应。
优化效果
回到开头那个8G内存的服务器,优化前后对比:
| 项目 | 优化前 | 优化后 |
| Java堆 | 4GB(默认) | 2GB(合理配置) |
| JVM元空间 | 256MB(无上限) | 128MB(-XX:MaxMetaspaceSize) |
| 直接内存 | 1GB(无上限) | 512MB |
| MySQL缓冲池 | 3GB | 2GB |
| Redis | 1.5GB | 800MB(Hash优化) |
| 系统保留 | 200MB | 500MB |
| 剩余可用 | 0(经常swap) | 1.6GB |
从0可用到1.6GB可用,相当于白嫖了一台2G内存的机器。
做IT这么多年,见过太多"早知道就好了"的情况。
希望这篇文章能帮你少走弯路。如果真的遇到问题,别一个人扛着——易云城IT服务随时待命。
📞 服务热线:13708730161 💬 微信:eyc1689 📧 邮箱:service@eycit.com 🌐 https://www.eycit.com
您身边的IT专家。