Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and "File in Use" from the State Right Now

· · Sysinternals, Process Explorer, VMMap, Handle Leak, Memory Leak, Failure Investigation, Windows, Long-Running

“Rebooting on Monday morning fixes it.” — I’ve heard that line over and over in maintenance consultations for equipment-integration applications. Right after it starts running everything is fine, but around Thursday switching screens gets sluggish, and by Friday evening a button press takes several seconds to come back. Occasionally a never-before-seen “invalid handle” error appears. And a reboot fixes it as though nothing had happened. Once things reach that point, operations settle into “reboot every week,” and years go by with nobody knowing the root cause.

You will never reach the cause of this kind of symptom by staring at logs. What you need is to look directly at what, and how much, that process is holding on to at this very moment. In the previous article, “A Practical Guide to Process Monitor (ProcMon),” we covered ProcMon, which records the timeline of operations. This time, as the sequel and part two of the practical Sysinternals series, we organise the tools on the state side — Process Explorer, Handle, and VMMap — around the three great symptoms of long-running applications: “it gets slower and slower,” “the file can’t be deleted,” and “it hung.”

1. The Bottom Line First

  • ProcMon is “history”; Process Explorer is “now.” Process Explorer lets you list and search the handles each process has open and the DLLs it has loaded, and Microsoft explicitly states that it’s suited to investigating DLL version problems and handle leaks.1
  • For “the file can’t be deleted or replaced,” Find Handle or DLL (Ctrl+F) is the fastest route. Search on part of the path and you can identify the process holding the file in seconds (Section 3).
  • For “it hung,” open the process properties → Threads tab and look at the thread stacks. But without symbol configuration (dbghelp.dll and a symbol path), all you get is a list of addresses (Section 4).12
  • For “it gets slower and slower,” add columns and watch the slope. Add the four columns Handle Count, USER Objects, GDI Objects, and Private Bytes, then look for the ones that keep climbing over time. GDI/USER objects have a per-process limit, and once it’s reached, drawing and window creation start breaking (Section 5).3
  • The CLI version, handle.exe, suits scripted periodic monitoring. handle -s -p <process> lets you record handle counts by type on a schedule. Force-closing handles with -c is officially warned about as destabilising, so we don’t normally use it.4
  • For “Private Bytes keeps growing,” break down the composition with VMMap. Heap means native, Managed Heap means .NET — decide which arena the culprit is in first, then move on to the dedicated tool (Section 6).5
  • When it’s OS-wide memory rather than a single process that looks suspect, use RAMMap.6 For crashes, switch to capturing a dump with ProcDump (“An Introduction to Collecting Windows Crash Dumps”).
  • None of these tools need installing, and they can be run directly from Sysinternals Live. That makes them easy to bring to an offline equipment PC — but symbols alone need advance preparation (Section 8).7

2. Process Explorer Basics — Running as Administrator, Replacing Task Manager, and Reading the Screen

Process Explorer needs no installation: extract the ZIP and run procexp.exe (on 64-bit, procexp64 runs automatically).1 Because it’s a tool for looking inside other users’ processes and services, when investigating you should always “Run as administrator.” Launch it with standard privileges and precisely the process you care about will come back with “Access is denied,” with neither stacks nor handles visible.

On an investigator’s machine, enabling Options → Replace Task Manager makes Ctrl+Shift+Esc and any Task Manager invocation from the taskbar open Process Explorer instead. The effect of “the tool you’re used to opens when you need it in a hurry” is quietly significant, and we’ve replaced it on every development machine here (the same menu item reverts it).

The screen is a two-pane layout: the process tree on top, and details of the selected process below. Use View → Lower Pane View to switch the lower pane between the Handles view (a list of open handles) and the DLLs view (a list of loaded DLLs and memory-mapped files).1 The first thing to learn is what the row colours mean (you can check and change them under Options → Configure Colors).

Colour (default) Meaning
Green Newly started process (about one second by default)
Red Process in the middle of exiting
Light blue Process running as your own user account
Pink Process hosting a service
Purple Suspected packed (compressed/obfuscated) executable
Dark grey Suspended process

Anomalies such as “a process is born green, immediately turns red and disappears, over and over” can be spotted from the tree view and colour coding alone. Because parent-child process relationships are visible in the tree, it’s also immediately obvious “whose child is this conhost.exe” and “is the application being launched by a service or by Task Scheduler.”

3. “The File Is in Use and Can’t Be Deleted” — Identifying the Culprit with Find Handle or DLL

“The log file can’t be deleted.” “I tried to update and the exe is in use.” “The USB stick can’t be safely ejected.” — Investigating this symptom ends at Find → Find Handle or DLL (Ctrl+F). Enter part of a file or folder name (for example report.csv or D:\Data) and search, and you get a list of processes that have a handle open containing that name, or that have loaded that DLL.1 Click a result row and the corresponding process is selected in the upper pane with the corresponding handle highlighted in the lower pane.

Once you’ve found it, the proper response is “shut that process down cleanly.” You can also right-click the handle in Process Explorer and force-close it with Close Handle, but for the same reason as handle -c below, we don’t recommend that in production. Also note that only named objects can be searched — unnamed events and thread handles won’t be hits.

The DLLs view is also used to confirm “which version of a DLL, from which location, was actually loaded.” Checking DLL version problems such as “it was holding an old DLL” or “the fixed version I deployed isn’t being read” pairs this screen with ProcMon’s load-order investigation (Section 5.2 of part one).

How to safely replace an exe or DLL that’s in use in the first place is a design question, not an investigation one. Our sister article published the same day, “How to Replace an exe or DLL That Is In Use,” covers replacement design including Restart Manager, so if you’re the one building the update mechanism, head there.

4. “It Hung” — Reading Thread Stacks in the Threads Tab

The window has gone white and stopped responding. Before you force-quit it, double-click the process to open its properties and look at the Threads tab. It lists CPU usage, cycle count, and start address per thread, and selecting a thread and pressing the Stack button shows which function call that thread is currently stopped inside. Most hangs are “the UI thread is waiting for something,” so if you see WaitForSingleObject, EnterCriticalSection, or a synchronous network or serial I/O wait near the top of the stack, that’s the direct cause.

However, without symbols configured, the stack is just a list of addresses and modulename+0x1234 entries and is essentially unreadable. Configure the following two items under Options → Configure Symbols.

Dbghelp.dll path:
  C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\dbghelp.dll
  * Point at the one included with WinDbg (Debugging Tools for Windows)

Symbols path:
  srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
  * C:\Symbols is the local cache. From the second time onward it's read from here

There are two key points. First, the default dbghelp.dll in System32 doesn’t support downloading from a symbol server, so you need to point at the one shipped with WinDbg. Second, as the official documentation states, when using a symbol server, symsrv.dll must be present in the same location as the dbghelp.dll you specified (WinDbg’s installation folder has both).1 The srv*cache*serverURL symbol path format and Microsoft’s public symbol server https://msdl.microsoft.com/download/symbols are the same mechanism WinDbg uses.2 To read your own application’s stacks you also need your own build’s PDBs, so add the folder holding them to the symbol path, separated by a semicolon.

For .NET applications, the stacks in the Threads tab are native-frame-centric and managed method names may not come out accurately. If you’re seriously chasing a managed hang or deadlock, capturing a dump while it’s hung and looking at it with WinDbg + SOS is the reliable route (“Reading Crash Dumps with WinDbg + SOS”). The division of labour is: the Threads tab is the “get a rough idea in three minutes, on the spot” tool; a dump is the “take it home and settle it” tool. And please don’t press the Kill/Suspend buttons in the Threads tab against a production process — what you intended as observation becomes intervention.

5. “It Gets Slower and Slower” — Monitoring Leaks with Handle Count, USER/GDI Columns, and handle.exe

The classic causes of applications that degrade under long-running operation are leaks of handles, GDI/USER objects, and memory. Process Explorer is excellent as a monitoring tool too — add the following columns from View → Select Columns.

  • Process Performance tab → Handle Count (the number of kernel object handles)
  • Process Memory tab → Private Bytes, Virtual Size, USER Objects, GDI Objects

What you’re looking at is not the absolute value but the slope. A healthy application’s handle count rises and falls with activity but stays within a fixed range; when there’s a leak, it climbs steadily — “up with every operation, and never back down.” Even just leaving screenshots every hour, or morning and evening, will show you the trend by the next day. GDI objects and USER objects have a per-process limit (10,000 by default, changeable via the registry value GDIProcessHandleQuota and similar) and a theoretical session-wide limit of 65,536; once the limit is hit, creating pens, brushes, and windows starts failing.3 “The screen rendering goes haywire on Fridays” is usually exactly this.

For environments where you can’t open a GUI, or for scripted periodic monitoring, use the CLI version, handle.exe. It’s a command-line tool that lists and searches handles, and it requires administrator privileges.4

:: Who is holding this file/folder (partial name match search)
handle.exe /accepteula report.csv
handle.exe D:\Data

:: Dump all handle types, not just files, narrowed by process name
handle.exe -a -p MyEquipApp

:: Aggregate handle counts by type -- log this for periodic leak monitoring
handle.exe -s -p MyEquipApp.exe >> C:\Logs\handles_%date:~0,4%%date:~5,2%%date:~8,2%.log

Append the output of -s (the per-type totals such as Event: 1523, File: 88, …) hourly via Task Scheduler and you can get hard numbers for “how many handles of which type grow per day” even on an equipment PC you can only maintain remotely. Once you know the type, the list of suspects narrows dramatically. Event means events not being released, File means forgetting to close, Thread means handles left behind after threads finish, and so on.

Note that the -c option can force-close a specified handle, but the official documentation itself warns that closing handles can cause application or system instability.4 Our policy is not to use it in production even as a stopgap for releasing a lock. Once you know “which type is leaking,” the procedure for identifying “which code allocated it” continues in “Investigating Long-Run Crashes of an Industrial Camera App - The Handle Leak (Part 1)” and “Building a Windows Failure-Path Test Foundation with Application Verifier.”

6. VMMap — Breaking Down “Private Bytes Keeps Growing”

Handle counts are flat but Private Bytes alone keeps climbing — that’s when you open VMMap. It’s a tool that analyses a process’s virtual and physical memory (working set), and it breaks down committed virtual memory by type and shows it to you.5 Launch it, select the target process, and you get a colour-coded summary at the top and a detailed memory map below. Here’s how to read the main categories.

Category What it contains Typical culprit when it keeps growing
Image The EXE/DLL bodies themselves Plugin DLLs left loaded
Heap Native heap (new/malloc/HeapAlloc) C/C++ code, equipment SDKs, interop code failing to free
Managed Heap The .NET GC heap Managed reference leaks (event handlers and the like)
Private Data Direct allocation via VirtualAlloc, etc. Image frame buffers, SDK-internal buffers
Stack Thread stacks Threads created and never cleaned up
Shareable / Mapped File Shared memory, mapped files Inter-process sections not being released

The usage pattern is: “refresh the snapshot with F5 while repeating the problematic operation, and watch which category grows.” VMMap supports snapshot comparison, a timeline view, and exporting results,5 so you can collect evidence on the spot such as “over 100 measurements, Heap grew 40 MB and Managed Heap stayed flat.” Once the arena is decided, it’s over to the dedicated tools: Managed Heap means investigating on the .NET side (“Pinpointing "Slow" with PerfView and dotnet-trace”), Heap/Private Data means investigating on the native side (Application Verifier, dump analysis). Skipping this triage and going straight to blaming the GC, then burning several days, is the single most common detour in equipment applications built from .NET plus a native SDK.

In 32-bit processes, fragmentation is also worth observing. Fragmentation View shows how the free space in the address space is scattered, visualising states like “Free totals several hundred MB, but the largest contiguous free block (Largest) is only a few tens of MB.” “There should be memory available, yet we get OutOfMemory” and “only the allocation of a large image buffer fails” usually come down to this, and it makes solid supporting evidence for countermeasures (going 64-bit, redesigning to reuse buffers).

When it’s OS-wide memory rather than a single process that looks suspect (none of the processes are fat, yet you run out of memory), switch to RAMMap, which shows what all physical memory is being used for, broken down by tab. It exposes file cache, drivers, and kernel usage too, so this is where you go looking for “a culprit other than the application.”6

7. Case Study — Triaging “the Equipment PC That Gets Slow Every Friday”

Here’s how the equipment PC from the opening is actually triaged with the tools so far. Because operations reboot it on Monday morning, Friday is “day five of operation.” In other words, the initial hypothesis is that something grows in proportion to time.

  1. Around Wednesday, install Process Explorer and set up the columns. Add Handle Count, USER Objects, GDI Objects, and Private Bytes, and record the target process’s values. At the same time, register an hourly handle -s -p TargetApp.exe log with Task Scheduler (Section 5).
  2. The next day, look at the slope. In this case Handle Count grew by roughly 20,000 in a day, and the per-type log showed Event handles increasing monotonically. Private Bytes and GDI/USER were essentially flat. At this point it narrows to “kernel objects (events) not being released.”
  3. Look at the real thing in the Handles view. Sort the lower pane’s Handles view by Type and there are tens of thousands of unnamed Events. Because they’re unnamed you can’t trace them with Find Handle, but knowing the type and the rate of growth is enough. From the correspondence with the measurement cycle (in this case two per equipment polling round), the hypothesis of the communications library failing to release its wait events emerged, and a code review confirmed it. If you want to pin down the allocation site with a tool, this is where Application Verifier and !htrace come in.
  4. If Private Bytes had been growing, then instead of step 3 you’d break down the composition in VMMap (Section 6) and branch to native investigation for Heap or .NET investigation for Managed Heap.
  5. If none of the numbers were growing and it only hangs, look at the stacks in the Threads tab (Section 4) and identify what it’s waiting on.

The key point is securing the evidence of “the slope of the graph” before you fix anything. Take the same measurement after the fix and show that the slope is zero, and you can report “it’s fixed” with numbers. For long-running problems where waiting for a reproduction takes days, setting up this measurement is the investigation.

8. Bringing Tools onto Production PCs and Operational Cautions — Sysinternals Live, EULAs, and Offline Symbols

All the Sysinternals tools are standalone executables that need no installation, so bringing the ZIP on a USB stick is enough to run them. A EULA acceptance dialog appears on first launch, so for unattended runs or from a script, accept it explicitly with the /accepteula switch. If the network is available, the Sysinternals Live service lets you run them directly without downloading, via a URL such as https://live.sysinternals.com/procexp.exe or a UNC path such as \\live.sysinternals.com\tools\<toolname>.7 That’s the fastest route when you want to look at this one machine right now.

What trips you up on an offline equipment PC is symbols (the stack display from Section 4 isn’t usable). There are two answers: (1) have a development machine with internet access resolve symbols against the same OS build, copy the whole local cache (C:\Symbols) to the equipment PC, and point the symbol path at that local folder only; or (2) give up on stack analysis, restrict yourself on site to capturing dumps (ProcDump) and recording numbers, and take them back to a development machine for analysis. Option (2) is the reliable one, and how to capture dumps is covered in “An Introduction to Collecting Windows Crash Dumps.”

Finally, on the operational side. Observation with Process Explorer or VMMap is low-load and low-risk, but intervention operations — killing or suspending a process, killing a thread, Close Handle, handle -c — should be prohibited in production as a rule. As with ProcMon in part one, run it through the same approval process as any other change, and decide up front “who observes what, when, and what will not be operated on” before you begin.

9. Standard Practice (Decision Table)

Symptom Tool to use Where to look
Gets slower and slower / becomes unstable after a few days Process Explorer The slope of the Handle Count / USER & GDI Objects / Private Bytes columns (Section 5)
A file can’t be deleted or replaced Process Explorer / handle.exe Search by path with Find Handle or DLL (Ctrl+F) → the process holding it (Section 3)
Hang / not responding Process Explorer Properties → Threads tab → thread stacks (symbols required, Section 4)
Private Bytes keeps growing VMMap Which of Heap / Managed Heap / Private Data is growing (Section 6)
Out of memory even though no process is fat RAMMap Physical memory usage via Use Counts and File Summary6
It crashes / dies with an exception ProcDump + WinDbg Dump collectionWinDbg + SOS
Where CPU time goes / .NET GC PerfView / dotnet-trace Quantitative investigation of “it’s slow”
Timeline of operations (which file, when, in what order) Process Monitor Part one

10. Summary

  • Whereas ProcMon records “the history of operations,” Process Explorer / Handle / VMMap are tools for seeing “the state at this very moment.” Investigating long-running applications needs both.
  • “The file can’t be deleted” takes seconds with Find Handle or DLL (Ctrl+F); “it hung” gets a first read from the stacks in the Threads tab. Reading stacks presupposes configuring dbghelp.dll (from a location that also has symsrv.dll) and a symbol path.
  • For “it gets slower and slower,” add the Handle Count, USER/GDI Objects, and Private Bytes columns and watch the slope. A scheduled handle -s log gets you numbers even on an equipment PC where you can’t open a GUI.
  • Growth in Private Bytes should be triaged in VMMap into Heap (native) / Managed Heap (.NET) / Private Data before moving on to the dedicated tool. For 32-bit processes, suspect fragmentation too.
  • Force-closing with handle -c or Close Handle is a source of instability, exactly as Microsoft warns. In production, stick to observation — don’t intervene, or if you must, get approval first.
  • The tools are standalone and easy to bring along, and Sysinternals Live even lets you run them directly. In offline environments, compensate by pre-caching symbols or taking a dump home with you.

KomuraSoft LLC handles leak investigations for business and equipment-integration applications that degrade under long-running operation, root-cause analysis of on-site failures such as hangs and “file in use,” and the setup of evidence-collection procedures together with the fixes that prevent recurrence.

References

  1. Microsoft Learn, Process Explorer - Sysinternals. On Process Explorer displaying a process’s open handles and its loaded DLLs and memory-mapped files in two panes (handle mode / DLL mode); on being able to search for the process holding a particular handle or DLL; on it being useful for tracking down DLL version problems and handle leaks; on symsrv.dll needing to be present in the same location as dbghelp.dll when using a symbol server; and on being able to run it directly from Sysinternals Live.  2 3 4 5 6

  2. Microsoft Learn, Microsoft Public Symbol Server. On the Microsoft public symbol server’s symbol path being specifiable in the form srvlocal cachehttps://msdl.microsoft.com/download/symbols, and on the downstream store (local cache) holding only the symbols that have been accessed once, so that subsequent reads come from the local copy.  2

  3. Microsoft Learn, GDI Objects. On GDI handles having a theoretical limit of 65,536 per session, and on there being a default per-process limit that can be changed via the registry value GDIProcessHandleQuota (in the range 256 to 65,536).  2

  4. Microsoft Learn, Handle - Sysinternals. On handle.exe being a command-line tool that displays information about open handles and requiring administrator privileges; on partial name matching; on -a covering all handle types; on -p narrowing by process; on -s aggregating handle counts by type; and on -c being able to close handles while carrying the warning that doing so “can cause application or system instability.”  2 3

  5. Microsoft Learn, VMMap - Sysinternals. On VMMap being a process virtual and physical memory analysis tool that shows a breakdown of committed virtual memory by type along with the physical memory (working set) assigned to each type; on its filtering and refresh (snapshot) features; and on its support for exporting data and for scripting via command-line options.  2 3

  6. Microsoft Learn, RAMMap - Sysinternals. On RAMMap being a tool for analysing OS-wide physical memory usage, showing what it’s used for across tabs such as Use Counts (totals by type), Processes (process working sets), and File Summary (file data resident in RAM); and on its support for saving and loading snapshots.  2 3

  7. Microsoft Learn, Sysinternals. On Sysinternals Live being a service that lets you run tools without downloading them, directly via a live.sysinternals.com/ URL or a \\live.sysinternals.com\tools\ path, and on being able to browse the list of tools at live.sysinternals.com.  2

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

Task Manager already exists — why go to the trouble of using Process Explorer?
Because Task Manager doesn't show you which process has which file or object open, or where each thread is currently stopped in code. Process Explorer lists the handles and DLLs per process, searches handles by name (Ctrl+F), and displays thread stacks, which puts investigations of "the file can't be deleted" and "it hung" on a completely different level from Task Manager. Add columns for handle counts and USER/GDI object counts and it doubles as a leak-monitoring tool. If you enable Replace Task Manager from the Options menu, any call to Task Manager is replaced by Process Explorer, so on an investigator's machine it's practical to just swap it out.
If I close a handle with handle.exe's -c option, can I release a file lock immediately?
Technically yes, but as a rule don't do it in production. The official documentation itself warns that closing handles can cause application or system instability. It seizes a handle from outside while ignoring the process's internal state, so the worst-case accident is entirely possible: the moment that process uses the same handle later, it points at a different object and misbehaves (handle value reuse). The proper approach is to identify the process holding it and shut it down cleanly. If the context is replacing an exe or DLL that's in use, consider a design-level solution such as Restart Manager.
Private Bytes keeps growing. What should I do first?
The first move is to open the target process in VMMap and see which category is growing. If Heap is growing, a native leak via malloc/new/HeapAlloc is the prime suspect, and equipment vendor SDKs or C++/CLI and interop code become the usual culprits. If Managed Heap is growing, it's a .NET managed heap problem, so move on to investigating references the GC can't reclaim (event handlers that were never unsubscribed, and so on). If Private Data (direct allocation via VirtualAlloc) is growing, suspect code or an SDK that allocates large blocks such as image buffers. Once you've narrowed it down, moving on to the dedicated tool for each case (WinDbg, PerfView, and so forth) keeps the investigation from wandering.
On an offline equipment PC, the stacks in the Threads tab are just a list of addresses and unreadable. What can I do?
That's because symbols (PDBs) can't be downloaded, and you have two options: bring the symbols in, or take a dump back with you. To bring them in, have a development machine with internet access resolve symbols once against the same OS build and the same application configuration, copy the whole local cache folder (for example C:\Symbols) to the equipment PC, and point the symbol path at that local folder only. To take one back, capturing a dump while it's hung and analysing it in WinDbg on a development machine is more reliable, and it's also the better route when you want to read a managed application's stacks accurately. All of this assumes you've archived your own application's PDBs for every build.
Is it a problem to install Process Explorer or VMMap on a production or equipment PC?
Both are standalone executables that need no installation and run without registry entries or resident services, so the barrier to bringing them in is low. You have to accept the EULA on first launch, and when driving them from a script you can accept it explicitly with the /accepteula switch. If the network is available, you can also run them directly from Sysinternals Live (https://live.sysinternals.com) without downloading anything. But while observation is low risk, "intervention" operations such as killing or suspending a process, killing a thread, or force-closing a handle lead directly to accidents — in production, stick to observing and recording, and run it through your normal change approval process.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog