PowerShell can be pretty resource intensive especially if you use it to retrieve data from your directory. I thought it will be useful to share this issue and workaround which were recently discussed in the PowerShell newsgroup.
SYMPTOMS
When trying to retrieve all user objects for subsequent processing with the following code:
$users = Get-QADUser -IncludeAllProperties -SizeLimit 0
the computer throws the following exception:
Get-QADUser : Exception retrieving members: "Exception of type
'System.OutOfMemoryException' was thrown."
(Looks like a correct line, right? SizeLimit set to zero makes PwerShell retrieve all user objects, IncludeAllProperties makes it retrieve whole user objects (not just the default set of attributes), then the collection is assigned to a variable for subsequent use. So why the exception? Read on!)
CAUSE
In reality on a fairly big domain this single line quoted above consumed all the RAM the machine had by basically retrieving the whole AD database wrapped into PowerShell objects.
You should avoid retrieving and keeping in memory the data you don’t need in your scripts. See information on how this can be done below.
RESOLUTION
Here are a few important tips to keep in mind to optimize your PowerShell code when working with Active Directory:
Use Pipeline
Don’t save the whole collection of objects to a variable. This way you make PowerShell retrieve all the objects and keep them the whole session. Use pipeline instead – which makes PowerShell pass the retrieved objects one by one to the next cmdlet.
So instead of:
$users = Get-QADUser
ForEach ($user in $users) { here goes the code }
Use:
Get-QADUser |
ForEach { here goes the code }
Retrieve Only What You Need
-IncludeAllAttributes is a dangerous parameter because it, well, includes all attributes. Even the binary blobs you won’t have any idea on how to use. If you are ok with the attributes the cmdlets retrieves by default (to get the list just run Get-QADUser | Get-Member) simply use:
Get-QADUser -SizeLimit 0
If you need a couple additional parameter, use -IncludedProperties
switch to add them and just them (not all the attributes!)
Get-QADUser -SizeLimit 0 -IncludedProperties proxyAddresses
If you need to optimize even further, limit the retieval only to the attributes you need by using the DontUseDefaultIncludedProperties
switch:
Get-QADUser -DontUseDefaultIncludedProperties -IncludedProperties
SamAccountName,proxyAddresses
Filter Using cmdlet Parameters
Finally, if you need only a subset of objects use cmdlet parameters to do the filtering – and not the where clause.
So instead of:
Get-QADUser | where
{ $_.City -eq Amsterdam} | { here goes the code }
Use:
Get-QADUser -City Amsterdam | { here goes the code }
The difference is huge. In the former case, PowerShell retrieves all user objects and then does the filtering on the client. In the latter, the filtering becomes a part of the LDAP query and thus is made automatically on the domain controller during data retrieval.
SUMMARY
The ideal PowerShell script:
- Does not keep the whole set of objects but processes them one by one upon retrieval.
- Retrieves only the attributes it needs.
- Filters objects during retrieval.
Get-QADUser -City Amsterdam -DontUseDefaultIncludedProperties -IncludedProperties
SamAccountName,proxyAddresses | ForEach { here goes the code }
Tags: PowerShell, AD cmdlets, cmdlets, KB, Known Issues, Knowledge Base, Active Directory, AD
Thank you for posting this. I have been looking for ways to filter objects during the retrieval. I was hoping Import-Csv had something like this, but I have not found anything so far. I only need 4 columns out of 70 or so from my csv file (which has about 10,000 records). It looks like I may be stuck with grabbing all the data for a csv file. =(
And you don’t own the csv, do you? You could probably use regular expressions to parse the file and extract just the columns you need.
This was a tremendously helpful article and helped me fix a memory leak I had been contending with.
Glad that I could help!
Using Quest AD tools I often run in to memory consumption problems. I thought it was a question of memoryleaks, but its not, its the Garbage collection that doesn’t get collection until its to late.
So i’m using this when I use Quests AD Management Cmdlets in PowerShell, where $i is a simple counter
if (($i % 200) -eq 0)
{
[System.GC]::Collect()
}
This is awesome tip – thanks for sharing!
GC.Collect is only necessary when running in PowerGUI!!!
That’s interesting… I wonder if this is a side effect of PowerShell being in debugging mode (you mean the Script Editor, rigth?)
In that case, this should be applicable to debugging in ISE as well…
Aha – I was running i Debug Mode – I didn-t know – tried running it by pressing CTRL+F5 but it stil uses huge amount of data compared to running it in an External Powershell Window where it uses 65 -75 MB, and in PowerGUI it ends up using 1.4 GB and then chrashes (debug or not).
Ok, when I start an External Powershell through the menu of PowerGUI the memoryconsumption is even worse compared to running it in the PowerGUI (3.1.0.2058)!
But everything is fine when I start a Powershell command and Execute the script from in there.
PowerGUI launches powershell.exe with the -STA parameter. Thats why you see the difference.
This is so incredibly helpful. I wonder if you could also write something around how to avoid memory leaks. I have a powershell script which runs forever, processing information … sleeping.. repeat. The memory usage of the ISE just grows and grows. I’ve discovered and added “[gc]::collect()” into the loop, which seems to help. Also some articles speak about .Dispose when using a custom object. Should I be calling a function? An article to clarify this would also be extremely helpful. Articles out there suggesting to run from the task scheduler rather than a forever loop are giving a valid work around, but avoiding the issue.
I ran into similar issue using the Quest cmdlets…what I’ve started doing when processing items in a loop is using the RV (remove variable) to free up the space used by that variable (setting to null is not the same thing!). This helped quite a bit with the Quest cmdlets (including invoking the garbage collection).
I even try to do this within functions before the function ends on the variables used within that function (local variables) – I assume the values would be removed and cleaned up…I am just forcing it to happen
really basic example:
$list=import-csv somelist.csv
foreach ($i in $list) {
$var1=$i.value1
$var2= $i.value2
rv var1
rv var2
} # end foreach
rv i
rv list
Very nice article and very helpful indeed. It reduced my code length and memory usage. Thanks