PowerShell Memory Analysis: Identifying High-Memory Processes and System Cache Usage on 64-bit Windows


4 views

html

When diagnosing memory usage on 64-bit Windows systems, it's crucial to understand that physical RAM can be consumed by both active processes and system caches (like Standby List or Modified Page List). Traditional tools like Task Manager often don't provide the complete picture.

Here are powerful PowerShell commands to analyze memory usage comprehensively:

# Get top 5 processes by working set memory
Get-Process | Sort-Object -Property WS -Descending | Select-Object -First 5 -Property ID,ProcessName,WS,VM,PM

# Alternative: Using performance counters for more detailed analysis
Get-Counter '\Process(*)\Working Set - Private' | 
    Select-Object -ExpandProperty CounterSamples | 
    Sort-Object -Property CookedValue -Descending | 
    Select-Object -First 10 -Property InstanceName,CookedValue

To examine memory used by system caches that don't appear in process lists:

# Using Get-CimInstance for WMI queries (more efficient than Get-WmiObject)
$os = Get-CimInstance Win32_OperatingSystem
$totalPhysical = $os.TotalVisibleMemorySize
$freePhysical = $os.FreePhysicalMemory
$usedPhysical = $totalPhysical - $freePhysical
Write-Host "Physical memory used: $($usedPhysical/1MB) GB (Total: $($totalPhysical/1MB) GB)"

# Detailed cache analysis
Get-Counter '\Memory\Cache Bytes' | Select-Object -ExpandProperty CounterSamples

Here's a comprehensive script that combines multiple approaches:

function Get-MemoryUsageReport {
    $report = @()
    
    # Process memory
    $processes = Get-Process | Sort-Object -Property WS -Descending | Select-Object -First 10
    foreach ($proc in $processes) {
        $report += [PSCustomObject]@{
            Type        = "Process"
            Name        = $proc.ProcessName
            PID         = $proc.Id
            WorkingSet  = [math]::Round($proc.WS / 1MB, 2)
            PrivateMB   = [math]::Round($proc.PM / 1MB, 2)
        }
    }
    
    # System memory
    $os = Get-CimInstance Win32_OperatingSystem
    $totalPhysical = $os.TotalVisibleMemorySize / 1MB
    $freePhysical = $os.FreePhysicalMemory / 1MB
    
    $report += [PSCustomObject]@{
        Type        = "System"
        Name        = "Available Physical"
        PID         = "N/A"
        WorkingSet  = [math]::Round($freePhysical, 2)
        PrivateMB   = [math]::Round(($totalPhysical - $freePhysical), 2)
    }
    
    # Memory counters
    $cache = (Get-Counter '\Memory\Cache Bytes').CounterSamples[0].CookedValue / 1MB
    $report += [PSCustomObject]@{
        Type        = "System"
        Name        = "System Cache"
        PID         = "N/A"
        WorkingSet  = [math]::Round($cache, 2)
        PrivateMB   = 0
    }
    
    return $report | Format-Table -AutoSize
}

Get-MemoryUsageReport

Key metrics to focus on:

  • Working Set (WS): Total physical memory currently used by the process
  • Private Memory (PM): Memory exclusively used by the process
  • System Cache: Memory used by Windows for file caching

For deeper analysis of kernel memory pools (useful when you suspect driver leaks):

# Requires elevated privileges
Get-Counter '\Memory\Pool Paged Bytes', '\Memory\Pool Nonpaged Bytes' | 
    Select-Object -ExpandProperty CounterSamples

When your Windows 64-bit system shows high memory usage that Task Manager can't fully explain, PowerShell becomes an invaluable tool for deeper diagnostics. Unlike GUI tools, PowerShell can reveal both process memory and system cache usage that might otherwise remain hidden.

Start with this simple command to list processes by memory usage:

Get-Process | Sort-Object -Property WS -Descending | Select-Object -First 10 -Property ID,ProcessName,WS,VM

This shows the top 10 processes by Working Set (WS) memory, along with their Virtual Memory (VM) usage. The -Descending parameter sorts from highest to lowest.

For more detailed cache and system memory information, we can use performance counters:

Get-Counter -Counter "\Memory\Cache Bytes" -SampleInterval 2 -MaxSamples 5
Get-Counter -Counter "\Memory\System Cache Resident Bytes" -SampleInterval 2 -MaxSamples 5
Get-Counter -Counter "\Memory\Pool Paged Bytes" -SampleInterval 2 -MaxSamples 5
Get-Counter -Counter "\Memory\Pool Nonpaged Bytes" -SampleInterval 2 -MaxSamples 5

To detect potential memory leaks, track process memory over time:

$process = Get-Process -Name "your_process_name"
$startMemory = $process.WS
Start-Sleep -Seconds 60
$endMemory = (Get-Process -Name "your_process_name").WS
$memoryGrowth = ($endMemory - $startMemory)/1MB
Write-Host "Memory growth: $memoryGrowth MB"

System commit memory is often overlooked but crucial for understanding overall memory pressure:

$memoryStatus = Get-CimInstance -ClassName Win32_OperatingSystem
$totalVisibleMemory = $memoryStatus.TotalVisibleMemorySize/1MB
$freePhysicalMemory = $memoryStatus.FreePhysicalMemory/1MB
$totalVirtualMemory = $memoryStatus.TotalVirtualMemorySize/1MB
$freeVirtualMemory = $memoryStatus.FreeVirtualMemory/1MB

Write-Host "Memory Summary:"
Write-Host "Physical - Total: $totalVisibleMemory MB, Free: $freePhysicalMemory MB"
Write-Host "Virtual  - Total: $totalVirtualMemory MB, Free: $freeVirtualMemory MB"

For enterprise environments, WMI provides extensive memory information:

Get-WmiObject -Class Win32_PerfFormattedData_PerfOS_Memory | 
Select-Object -Property AvailableMBytes, CacheBytes, CacheFaultsPersec, 
CommitLimit, CommittedBytes, PagesPersec, PageFaultsPersec, PoolPagedBytes, 
PoolNonpagedBytes

For regular monitoring, create a reusable function:

function Get-MemorySnapshot {
    param(
        [int]$TopProcesses = 10,
        [switch]$IncludeSystem
    )
    
    $processes = Get-Process | 
        Sort-Object -Property WS -Descending | 
        Select-Object -First $TopProcesses -Property ID,ProcessName,@{Name='WS(MB)';Expression={[math]::Round($_.WS/1MB,2)}}
    
    if ($IncludeSystem) {
        $memory = Get-CimInstance -ClassName Win32_OperatingSystem
        $systemInfo = [PSCustomObject]@{
            TotalVisibleMemoryGB = [math]::Round($memory.TotalVisibleMemorySize/1MB,2)
            FreePhysicalMemoryGB = [math]::Round($memory.FreePhysicalMemory/1MB,2)
            CommitChargeGB = [math]::Round(($memory.TotalVirtualMemorySize - $memory.FreeVirtualMemory)/1MB,2)
        }
    }
    
    return [PSCustomObject]@{
        Processes = $processes
        SystemMemory = if ($IncludeSystem) { $systemInfo } else { $null }
    }
}

# Usage:
Get-MemorySnapshot -TopProcesses 5 -IncludeSystem