C# IP扫描器大网段扫描后GC未释放内存致OutOfMemoryException问题
Hey there, let's work through this OutOfMemoryException issue you're facing with your multi-subnet IP scanner. I've dealt with similar problems when building network tools, so here's what I recommend to get this sorted:
First, Rule Out 32-bit Memory Limits
The 4GB virtual memory cap you mentioned is a dead giveaway that your project might be set to 32-bit (x86) in Visual Studio. Even on 64-bit systems, 32-bit apps can only access ~4GB of memory. Fix this first:
- Right-click your project in Solution Explorer → Properties → Build → Platform target → Select x64
- This lets your app leverage the full memory available in your deployment environment.
Stop Loading All IPs Into Memory At Once
The biggest culprit here is likely pre-generating every IP in the subnet and storing them in a single collection (like a List<IPAddress>). For a /16 subnet, that's 65,536 IPs—each IPAddress object adds up fast. Instead, generate IPs on-demand using an iterator:
public static IEnumerable<IPAddress> GenerateIpRange(IPAddress startIp, int subnetMask) { // Convert IP to uint for easier arithmetic byte[] startBytes = startIp.GetAddressBytes(); Array.Reverse(startBytes); // BitConverter expects big-endian for uint uint startIpUint = BitConverter.ToUInt32(startBytes, 0); // Calculate total number of IPs in the subnet uint totalIps = (uint)Math.Pow(2, 32 - subnetMask); uint endIpUint = startIpUint + totalIps - 1; // Yield one IP at a time instead of storing all in memory for (uint currentIpUint = startIpUint; currentIpUint <= endIpUint; currentIpUint++) { byte[] currentBytes = BitConverter.GetBytes(currentIpUint); Array.Reverse(currentBytes); // Convert back to little-endian for IPAddress yield return new IPAddress(currentBytes); } }
This way, you only have one IPAddress in memory at a time (per iteration) instead of thousands.
Optimize Object Allocation to Reduce GC Pressure
Every time you create a new object (like Ping, custom scan result classes), the GC has to clean it up later. Here's how to cut down on this:
- Use value types for small data: If you're storing scan metadata (like IP, status, response time), use a
structinstead of aclass. Structs are value types and don't add to heap memory pressure. - Reuse expensive objects with an Object Pool:
Pingobjects are costly to create/destroy. Use .NET's built-inObjectPoolto reuse them:using Microsoft.Extensions.ObjectPool; // Initialize the pool once private static readonly ObjectPool<Ping> _pingPool = new DefaultObjectPool<Ping>(new DefaultPooledPolicy<Ping>()); // Use the pool in your scan logic async Task ScanSingleIp(IPAddress ip) { var ping = _pingPool.Get(); try { var reply = await ping.SendPingAsync(ip, 1000); // Process the reply (only store results you actually need!) } finally { _pingPool.Return(ping); // Return to pool for reuse } } - Avoid storing unnecessary data: Don't keep every scan result in memory—write valid/interesting results to a file or database immediately, then discard them from memory.
Tune Garbage Collection for Server Environments
Since your deployment environment doesn't have memory limits, switch to Server GC which is optimized for multi-threaded, memory-heavy apps like your scanner:
- Add an
app.config(orappsettings.jsonfor .NET Core+) file to your project - Add this configuration:
<configuration> <runtime> <gcServer enabled="true"/> </runtime> </configuration>
Server GC uses dedicated threads for garbage collection and is more efficient at reclaiming memory in high-throughput scenarios.
Batch Scans & Trigger GC Strategically
Instead of scanning all IPs in parallel at once, split them into batches (e.g., 1000 IPs per batch). After each batch finishes, you can hint to the GC to clean up unused memory (use this sparingly—overcalling GC hurts performance):
// After finishing a batch GC.Collect(2, GCCollectionMode.Optimized); GC.WaitForPendingFinalizers();
This gives the GC a chance to reclaim memory before starting the next batch.
Bonus: Control Concurrency
If you're using async/await for scanning, use a SemaphoreSlim to limit the number of concurrent ping requests. Too many concurrent tasks can lead to excessive memory usage from pending operations:
private static readonly SemaphoreSlim _concurrencySemaphore = new SemaphoreSlim(50); // Limit to 50 concurrent pings async Task ScanSingleIp(IPAddress ip) { await _concurrencySemaphore.WaitAsync(); try { // Your ping logic here } finally { _concurrencySemaphore.Release(); } }
Start with the x64 platform switch first—it's the quickest win. Then work through the memory optimization steps, and you should see your memory usage drop dramatically even for large subnets.
内容的提问来源于stack exchange,提问作者Swannie

