The Complete Oracle Memory Map
Before tuning anything, you need to know exactly what lives where and why. Every performance problem in memory traces back to one of these structures being too small, too large, or fragmented.
Three Memory Management Modes
Most DBAs don't clearly know which mode their instance is running. This matters because it determines how you tune — and which tools are available.
SELECT name, value, description FROM v$parameter WHERE name IN ( 'memory_target', 'memory_max_target', 'sga_target', 'sga_max_size', 'pga_aggregate_target', 'db_cache_size', 'shared_pool_size', 'large_pool_size', 'streams_pool_size' ) ORDER BY name; -- Quick mode diagnosis: -- memory_target > 0 → AMM (Oracle controls SGA + PGA) -- sga_target > 0, memory = 0 → ASMM (Oracle controls SGA pools only) -- both = 0 → MSMM (fully manual)
Buffer Cache Advisor
Oracle continuously simulates what would happen to physical reads if you changed the buffer cache size. You don't need to guess — you just need to read the advisor table.
-- Enable advisor if off -- ALTER SYSTEM SET db_cache_advice = ON; SELECT size_for_estimate AS cache_mb, size_factor, estd_physical_read_factor AS read_factor, estd_physical_reads AS estd_reads, ROUND((1 - estd_physical_read_factor) * 100, 1) AS pct_io_reduction FROM v$db_cache_advice WHERE name = 'DEFAULT' AND block_size = (SELECT TO_NUMBER(value) FROM v$parameter WHERE name = 'db_block_size') ORDER BY size_for_estimate;
How to read the advisor output
The table below shows a real-world example. Find the "elbow" — the point where adding more memory gives diminishing returns.
| Cache MB | Size factor | Read factor | Est. phys reads | I/O reduction |
|---|---|---|---|---|
| 2048 | 0.25 | 3.82 | 76,400,000 | −282% (worse) |
| 4096 | 0.50 | 1.94 | 38,800,000 | −94% |
| 8192 | 1.00 | 1.00 | 20,000,000 | ← current |
| 12288 | 1.50 | 0.71 | 14,200,000 | 29% fewer reads |
| 16384 | 2.00 | 0.62 | 12,400,000 | 38% |
| 20480 | 2.50 | 0.60 | 12,000,000 | 40% |
| 24576 | 3.00 | 0.59 | 11,800,000 | ← diminishing return |
The elbow is at 12GB — going from 8GB to 12GB saves 29% of physical reads. Going from 12GB to 16GB saves only another 9%. Invest the 4GB, stop there.
-- Apply (dynamic with ASMM — no restart needed) ALTER SYSTEM SET db_cache_size = 12G SCOPE=BOTH; -- Verify the change landed SELECT component, current_size/1024/1024/1024 AS current_gb, min_size/1024/1024/1024 AS min_gb, last_oper_type FROM v$sga_dynamic_components ORDER BY current_size DESC; -- Which segments are consuming the most buffer cache? SELECT o.object_name, o.object_type, b.cnt * (SELECT TO_NUMBER(value) FROM v$parameter WHERE name = 'db_block_size') / 1024 / 1024 AS cached_mb FROM (SELECT obj, COUNT(*) cnt FROM v$bh WHERE dirty='N' GROUP BY obj) b JOIN dba_objects o ON o.data_object_id = b.obj WHERE b.cnt > 100 ORDER BY b.cnt DESC FETCH FIRST 20 ROWS ONLY;
Hit Ratio vs Physical Reads Per Second
The buffer cache hit ratio is a starting point — but it can mislead. A 99% hit ratio on a system doing 500,000 logical reads/sec still means 5,000 physical reads/sec. Always look at both.
-- Buffer cache hit ratio (target: > 95% on OLTP) SELECT ROUND( (1 - (p.value / (c.value + g.value))) * 100, 2 ) AS hit_ratio_pct FROM v$sysstat p, v$sysstat c, v$sysstat g WHERE p.name = 'physical reads' AND c.name = 'db block gets' AND g.name = 'consistent gets'; -- Physical reads per second since startup (the absolute measure) SELECT ROUND(p.value / GREATEST(((SYSDATE - s.startup_time) * 86400), 1)) AS phys_reads_per_sec FROM v$sysstat p, v$instance s WHERE p.name = 'physical reads';
ORA-04031 — Shared Pool Out of Memory
This error means Oracle cannot allocate a contiguous chunk in the shared pool. It crashes sessions mid-execution. Before just increasing SHARED_POOL_SIZE, diagnose the root cause.
-- Root cause 1: bind variable problem — library cache bloat -- Look for force_matching_signature with thousands of variants SELECT force_matching_signature, COUNT(DISTINCT sql_id) AS variants, SUM(sharable_mem)/1024/1024 AS total_mb, MIN(SUBSTR(sql_text,1,80)) AS sample_sql FROM v$sqlarea WHERE parsing_schema_name NOT IN ('SYS','SYSTEM') GROUP BY force_matching_signature HAVING COUNT(DISTINCT sql_id) > 100 ORDER BY total_mb DESC FETCH FIRST 10 ROWS ONLY; -- Root cause 2: large non-shareable objects eating shared pool SELECT name, namespace, sharable_mem/1024 AS kb, loads, executions, invalidations FROM v$db_object_cache WHERE sharable_mem > 102400 -- > 100KB objects AND type NOT IN ('CURSOR','PACKAGE','PROCEDURE','FUNCTION') ORDER BY sharable_mem DESC; -- Fix: pin critical packages to prevent aging out EXEC DBMS_SHARED_POOL.KEEP('APPS.ORDER_PROCESSING_PKG', 'P'); EXEC DBMS_SHARED_POOL.KEEP('APPS.PRICING_ENGINE_PKG', 'P'); -- Verify what is pinned SELECT owner, name, namespace, kept FROM v$db_object_cache WHERE kept = 'YES';
force_matching_signature and find 47,000 unique SQL variants — all identical except for WHERE clause literals. A Java developer used string concatenation instead of PreparedStatement. The library cache fills with 47,000 non-reusable cursors. Fix: enforce bind variables in the app. Immediately: set CURSOR_SHARING = FORCE as a temporary bandage — Oracle auto-creates bind variables. Long-term: fix the application code.
PGA Tuning
PGA tuning has the most dramatic performance impact per byte added. A sort that spills to disk can be 100× slower than one that stays in memory. The key metric is the work area execution mode breakdown.
Work area execution modes
-- Work area mode breakdown — the most important PGA metric SELECT name, value, CASE WHEN name = 'work areas executed in optimal mode' THEN 'No spill — all in memory' WHEN name = 'work areas executed in onepass mode' THEN 'One temp spill — tolerable' WHEN name = 'work areas executed in multipass mode' THEN 'ALARM — heavy disk spill' END AS meaning FROM v$pgastat WHERE name LIKE 'work areas%'; -- Current PGA allocation snapshot SELECT name, value/1024/1024 AS mb FROM v$pgastat WHERE name IN ( 'aggregate PGA target parameter', 'total PGA inuse', 'total PGA allocated', 'maximum PGA allocated' ); -- PGA target advisor — find the elbow SELECT pga_target_for_estimate/1024/1024 AS pga_target_mb, pga_target_factor, estd_pga_cache_hit_percentage AS estd_hit_pct, estd_overalloc_count FROM v$pga_target_advice ORDER BY pga_target_for_estimate;
Reading the PGA advisor output
| PGA target MB | PGA factor | Cache hit % | Overalloc count |
|---|---|---|---|
| 512 | 0.125 | 23% | 847 ← severe overallocation |
| 1024 | 0.25 | 41% | 312 |
| 2048 | 0.50 | 67% | 89 |
| 4096 | 1.00 | 81% | 0 ← current |
| 8192 | 2.00 | 91% | 0 ← elbow |
| 16384 | 4.00 | 94% | 0 |
| 32768 | 8.00 | 95% | 0 (minimal gain) |
The elbow is at 8GB — going from 4GB to 8GB buys 10% improvement in cache hit rate. Going to 16GB adds only 3% more. Invest the 8GB.
-- Apply (dynamic — no restart needed) ALTER SYSTEM SET pga_aggregate_target = 8G SCOPE=BOTH; -- 12c+: set a hard ceiling to prevent runaway PGA ALTER SYSTEM SET pga_aggregate_limit = 16G SCOPE=BOTH;
Finding and Fixing PGA Spills
-- Sessions currently spilling to temp SELECT s.sid, s.username, s.sql_id, t.tablespace, t.blocks * 8 / 1024 AS temp_mb_used FROM v$session s JOIN v$tempseg_usage t ON t.session_addr = s.saddr ORDER BY temp_mb_used DESC; -- Historical: SQL with the most disk sorts SELECT sql_id, sorts, disk_sorts, ROUND(disk_sorts * 100 / NULLIF(sorts, 0), 1) AS pct_disk_sorts, ROUND(elapsed_time/1e6/NULLIF(executions,0), 2) AS avg_ela_sec FROM v$sqlarea WHERE disk_sorts > 0 AND executions > 10 ORDER BY disk_sorts DESC FETCH FIRST 15 ROWS ONLY; -- Temp usage by user right now SELECT u.username, SUM(s.blocks) * 8 / 1024 AS temp_mb_used FROM v$sort_usage s JOIN dba_users u ON u.user_id = s.user# GROUP BY u.username ORDER BY temp_mb_used DESC;
v$sqlarea for disk_sorts and find one SQL with disk_sorts = 14,000 per night — a report sorting 50M rows. Fix in layers: (1) Immediate: raise pga_aggregate_target so the sort gets more RAM. (2) Better: add an index that delivers rows pre-sorted, eliminating the sort entirely. (3) Best: rewrite using analytic functions with PARTITION clause — processes smaller sorted subsets instead of sorting the entire 50M row result.
Memory Sizing Formula
This is the framework experienced DBAs use when sizing a new Oracle instance or reviewing an existing allocation.
Mixed OLTP + batch → SGA 60%, PGA 40%.
Analytics / DW → SGA 40%, PGA 60% (large sorts dominate).
SELECT component, current_size/1024/1024/1024 AS current_gb, user_specified_size/1024/1024/1024 AS specified_gb, last_oper_type, last_oper_time FROM v$sga_dynamic_components ORDER BY current_size DESC;
HugePages — The Hidden Performance Layer
This is something most junior DBAs don't know about. On Linux, default memory management uses 4KB pages. For a 40GB SGA, that's 10 million page table entries. The TLB (CPU's cache for page table entries) gets overwhelmed. HugePages uses 2MB pages — just 20,000 entries for the same SGA. The result: 5–15% performance improvement on large-memory OLTP systems.
/dev/shm (tmpfs), which does NOT support HugePages. This is the biggest practical reason to use ASMM (SGA_TARGET) instead of AMM (MEMORY_TARGET) on Linux production systems. If you're on AMM, you're giving up HugePages — and the performance that comes with it.
-- Step 1: Calculate required HugePages from SQL*Plus SELECT SUM(value)/1024/1024 AS sga_mb, CEIL(SUM(value) / 2097152) AS hugepages_needed FROM v$sga; -- Step 2: Check current HugePages on OS (run as root) -- grep -i hugepages /proc/meminfo -- HugePages_Total should be non-zero and >= hugepages_needed above -- Step 3: Set HugePages in /etc/sysctl.conf (OS level) -- vm.nr_hugepages = <hugepages_needed> -- sysctl -p (apply without reboot) -- Step 4: Disable AMM if currently using it ALTER SYSTEM SET memory_target = 0 SCOPE=SPFILE; ALTER SYSTEM SET memory_max_target = 0 SCOPE=SPFILE; ALTER SYSTEM SET sga_target = 40G SCOPE=SPFILE; ALTER SYSTEM SET sga_max_size = 48G SCOPE=SPFILE; -- Restart required for SGA_MAX_SIZE and to activate HugePages
Memory Health Dashboard
Run this single query on any Oracle instance to get an immediate health report of all four memory dimensions.
SELECT 'Buffer Cache Hit Ratio' AS metric, ROUND((1-(p.value/(c.value+g.value)))*100,2)||'%' AS value, CASE WHEN (1-(p.value/(c.value+g.value)))*100 < 90 THEN 'WARNING — increase DB_CACHE_SIZE' ELSE 'OK' END AS status FROM v$sysstat p, v$sysstat c, v$sysstat g WHERE p.name='physical reads' AND c.name='db block gets' AND g.name='consistent gets' UNION ALL SELECT 'Shared Pool Free %', ROUND(f.bytes*100/t.bytes,1)||'%', CASE WHEN f.bytes*100/t.bytes < 10 THEN 'WARNING — ORA-04031 risk' ELSE 'OK' END FROM (SELECT SUM(bytes) bytes FROM v$sgastat WHERE pool='shared pool' AND name='free memory') f, (SELECT SUM(bytes) bytes FROM v$sgastat WHERE pool='shared pool') t UNION ALL SELECT 'PGA Optimal Work Areas %', ROUND(opt.value*100/NULLIF(tot.value,0),1)||'%', CASE WHEN opt.value*100/NULLIF(tot.value,0) < 90 THEN 'WARNING — increase PGA_AGGREGATE_TARGET' ELSE 'OK' END FROM v$pgastat opt, v$pgastat tot WHERE opt.name = 'work areas executed in optimal mode' AND tot.name = 'work areas executions - total' UNION ALL SELECT 'Hard Parse Ratio %', ROUND(h.value*100/NULLIF(t.value,0),1)||'%', CASE WHEN h.value*100/NULLIF(t.value,0) > 10 THEN 'WARNING — check bind variables' ELSE 'OK' END FROM v$sysstat h, v$sysstat t WHERE h.name='parse count (hard)' AND t.name='parse count (total)';
Self-Check Questions
-
Q1
Your system is configured with
MEMORY_TARGET = 24Gon Linux. A colleague says performance is inexplicably worse than the old server despite identical SQL and more RAM. What Linux-level feature is likely disabled as a result of using AMM, and how do you fix it? -
Q2
The PGA advisor shows
estd_overalloc_count = 312at your currentPGA_AGGREGATE_TARGET. What does an overalloc_count greater than 0 mean, and why is it dangerous for system stability? -
Q3
You check
v$pgastatand seework areas executed in multipass mode = 847over the last hour. What exact problem is this telling you, and what are the two ways to fix it? -
Q4
Your shared pool
free memoryis consistently below 20MB on a 4GB shared pool. Before just increasingSHARED_POOL_SIZE, what two root causes should you investigate first — and what queries do you run for each? -
Q5
You increase
DB_CACHE_SIZEfrom 8GB to 16GB. The buffer cache hit ratio improves from 91% to 93% but physical reads per second barely changed. What does this tell you about the workload, and where should you look instead?