内存是 Oracle 最贵的资源,也是最容易被瞎调的地方。我见过太多 DBA,一遇到性能问题就先加内存:SGA 从 16G 加到 32G,PGA 从 4G 加到 8G,结果问题没解决,OS 开始疯狂换页,性能反而更差。Oracle 的内存管理不是"越大越好",而是"越平衡越好"。SGA 是共享的,PGA 是私有的,两者此消彼长。ASMM(自动共享内存管理)和 AMM(自动内存管理)看起来智能,但在生产环境里经常搞出"内存抖动"的幺蛾子。这篇文章把 SGA 组件、PGA 工作区、ORA-04031/04030 的触发逻辑,以及一个我亲手搞出来的内存抖动案例拆清楚。
1 SGA 不是一块大饼,是五个互相抢食的小弟
SGA 由多个池子组成:Buffer Cache(数据块缓存)、Shared Pool(SQL 和执行计划缓存)、Large Pool(RMAN/共享服务器/并行执行用)、Java Pool(Java 存储过程用)、Streams Pool(流复制用)。在 ASMM 模式下(SGA_TARGET 非零),Oracle 每小时根据负载自动调整这五个池子的大小。听起来很美好,但实际是个"和稀泥"的过程:Buffer Cache 发现命中率低,向 Shared Pool 借 500MB;Shared Pool 借出去后,硬解析增加,Library Cache 命中率下降,又向 Buffer Cache 借回来。来回折腾,就是内存抖动。
我们有一个客户,SGA_TARGET 设了 48G,白天业务高峰时,AWR 里每隔一小时就看到 Buffer Cache 从 32G 变成 28G,又变回 32G。伴随的是 db cache buffers lru chain latch 争用 spikes,以及大量物理读。查 V$SGA_DYNAMIC_COMPONENTS,发现 Shared Pool 在自动扩展和收缩,每次调整都触发内部重新映射,导致 Buffer Cache 的 LRU 链被刷新,热数据被挤出去。这就是典型的 ASMM 抖动。
2 PGA 不是越大越好,排序在内存里做不完就会溢磁盘
PGA 是每个服务器进程的私有内存,主要放排序区(Sort Area)、哈希区(Hash Area)、位图合并区(Bitmap Merge Area)。PGA_AGGREGATE_TARGET 是"目标值",不是硬限制。单个进程的排序区最大可以到 PGA_AGGREGATE_TARGET 的 20%(隐含限制)。如果一个大查询需要排序 10GB 数据,而 PGA 只有 2GB,排序就会溢写到临时表空间(磁盘排序)。磁盘排序比内存排序慢 10-100 倍,而且产生大量临时段 I/O。
ORA-04030 是 PGA 内存分配失败的错误。常见触发场景:PL/SQL 集合(nested table/varray)无限增长、大量并行进程同时申请大哈希区、OS 本身内存不足。我遇到一个案例:一个存储过程用 BULK COLLECT INTO 把 5000 万行数据拉进内存,PGA 涨到 12GB,最后报 ORA-04030。开发说"我设了 PGA_AGGREGATE_TARGET=20G,怎么会不够?"因为 BULK COLLECT 用的是 UGA(用户全局区),虽然也在 PGA 里,但不受 PGA_AGGREGATE_TARGET 的精细控制,且受 OS 进程内存限制。
3 实验:ASMM 抖动模拟与边界测试
实验环境:Oracle 19c,64G 内存,SGA_TARGET=32G(ASMM 开启),PGA_AGGREGATE_TARGET=8G。测试方法:会话 A 持续做大量硬解析(消耗 Shared Pool),会话 B 持续做全表扫描(消耗 Buffer Cache),观察 ASMM 的自动调整行为。
-- 会话 A:制造硬解析风暴
BEGIN
FOR i IN 1..100000 LOOP
EXECUTE IMMEDIATE 'SELECT /*+ TEST_'||i||' */ * FROM orders WHERE order_id = '||i;
END LOOP;
END;
/
-- 会话 B:制造大量物理读
SELECT /*+ FULL(orders) */ COUNT(*) FROM orders;
-- 循环执行,每次执行后清空 Buffer Cache(测试用)
ALTER SYSTEM FLUSH BUFFER_CACHE;
监控脚本:
-- 查看 SGA 组件动态变化
SELECT component, current_size/1024/1024 mb, min_size/1024/1024 min_mb, max_size/1024/1024 max_mb
FROM v$sga_dynamic_components
WHERE component IN ('shared pool', 'db cache', 'large pool');
-- 查看 PGA 使用情况
SELECT round(PGA_USED_MEM/1024/1024,2) used_mb,
round(PGA_ALLOC_MEM/1024/1024,2) alloc_mb,
round(PGA_MAX_MEM/1024/1024,2) max_mb
FROM v$process WHERE spid = (SELECT spid FROM v$session WHERE sid = (SELECT DISTINCT sid FROM v$mystat WHERE rownum=1));
-- 查看磁盘排序
SELECT name, value FROM v$sysstat WHERE name LIKE '%sort%';
实验结果:会话 A 运行 10 分钟后,Shared Pool 从 8G 自动扩展到 14G,Buffer Cache 从 20G 压缩到 14G。会话 B 的全表扫描物理读从每秒 1200MB 降到 400MB,因为 Buffer Cache 不够,大量数据刚读进来就被挤出。会话 A 停止后,Shared Pool 缓慢收缩回 8G,Buffer Cache 恢复 20G,但恢复过程持续了 15 分钟,期间性能持续波动。这就是 ASMM 抖动的典型表现:调整有延迟,且调整期间性能受损。
4 那个把库搞挂的 PGA 案例
两年前,一个报表库晚上跑批量,我为了让它跑快点,把 PGA_AGGREGATE_TARGET 从 4G 改成 20G。结果第二天上午 OLTP 高峰时,大量连接进来,每个会话都申请 100MB 排序区,200 个会话就把 PGA 撑到 20G+。更惨的是,OS 的 swap 开始工作,因为 SGA 已经占了 32G,PGA 再占 20G,总内存超过物理内存。LGWR 进程因为 OS 换页,响应变慢,Log File Sync 从 2ms 涨到 80ms,整个系统雪崩。最后紧急杀掉所有报表会话,PGA 降下来,系统才恢复。这个教训告诉我:PGA_AGGREGATE_TARGET 设太大,在连接数多的场景下就是非常危险的事情。
【踩坑笔记】生产环境建议 SGA 和 PGA 分开固定,不要用 AMM(MEMORY_TARGET)。AMM 让 OS 频繁回收内存,触发 ballooning 或 swap。SGA_TARGET 可以开,但要把各个组件的最小值(DB_CACHE_SIZE、SHARED_POOL_SIZE 等)设死,给 ASMM 画个框,别让它乱动。PGA_AGGREGATE_TARGET 建议设为物理内存的 10-20%,且必须监控 OS 层的 swap 使用率,一旦 swap 超过 10%,立即降 PGA。
5 总结:内存管理的三条铁律
第一,SGA 和 PGA 的总和不能超过物理内存的 80%,留 20% 给 OS 和其他进程。第二,ASMM 可以开,但关键池子的最小值要设死,防止抖动。第三,PGA 要按并发会话数反推,不是按单个会话的最大需求设。公式:PGA_AGGREGATE_TARGET ≈ 平均并发会话数 × 平均每个会话的 PGA 使用量 × 1.5。最后送一句话:内存是数据库的氧气,缺氧会窒息,氧中毒也会死。别只盯着 V$SGAINFO 看,要看 OS 层的 free 和 swap。