Preface: For many C # programmers, often very little attention to the release of their memory, they think C # with a powerful garbage collection mechanism, all unwilling to consider this aspect of things, in fact, many times we need to consider C # memory management problems, otherwise it will be easy to cause memory leak problem.
Although. The NET runtime is responsible for most of the memory management work, but C # programmers still have to understand how memory management works and how to efficiently handle unmanaged resources in order to efficiently process memory in a very performance-focused system.
One advantage of C # programming is that programmers don't have to worry about specific memory management, and the garbage collector automatically handles all the memory cleanup work. Users can get almost as efficient as the C + + language without having to consider complex memory management tasks like C + +. But we still need to understand how the program handles memory in the background to help improve the speed and performance of the application.
First look at the virtual addressing system in the Windows system:
The system maps the memory address available to the program to the actual address in the hardware memory, each process on a 32-bit processor can use 4GB of hardware memory (64-bit processor larger), this 4GB of memory contains all parts of the program (including executable code, code loaded by all DLLs, The contents of all variables used when the program runs)
This 4GB of memory is called the virtual address space, or virtual memory. Each of these storage units is sorted starting at 0. To access a value stored in a space in memory, you need to provide a number that represents that storage unit. The compiler is responsible for translating the variable names into memory addresses that the processor can understand.
Value types and reference types are classified as value types and reference types in C #, and they use a different but similar memory management mechanism.
1. Memory management for Value data types
In the virtual memory of the process, there is a region called the stack. C # 's value type data, and the parameter copies passed to the method, are stored in this stack. When data is stored in the stack, it is populated from a high memory address to a low memory address.
The operating system maintains a variable, called a stack pointer. The stack pointer is the last byte address of the memory that the current variable occupies, and the stack pointer is adjusted as needed, and it is always adjusted to point to the address of the next free storage unit on the stack. When there is a new memory requirement, the value of the current stack pointer is started down to allocate enough memory units for that requirement, and after allocation, the stack pointer is updated to the last byte address of the memory occupied by the current variable, which will be adjusted to point to the next idle cell the next time the memory is allocated.
Such as:int a= 10;
Declaring an integer variable requires 32 bits, which is 4 bytes of memory, assuming that the current stack pointer is 89999, then the system allocates 4 memory units for variable A, respectively 89996~89999, after which the stack pointer is updated to 89995
double d = 20.13;
Requires 64 bits, or 8 bytes of memory, to be stored in the 89988~89995
The stack works in an advanced post-out (FIFO): When a variable is released, the variable declared later is always released (memory is allocated later).
2. Memory management for reference data types
A reference to a reference type object is stored in the stack (4 bytes of space), and its actual data is stored on the primary or large object heap, which is another area of memory in available 4GB virtual memory.
Large Object heap: in. NET, because compressing large objects (greater than 85,000 bytes) affects performance, they are assigned their own managed heap: NET garbage collector does not perform a compression process on a large object heap.
Such as:Person arabel= new Person();
When declaring a variable Arabel, the variable is allocated 4 bytes of space on the stack to store a reference, the new operator allocates space on the heap for the object person object, and then assigns the address of the space to the variable Arabel, and the constructor is used to initialize it.
. NET runtime to allocate space for an object Arabel, you need to search the heap and pick the first unused contiguous block that holds all of the object's data. However, after the garbage collector program reclaims all unreferenced objects in the heap, it performs a compression operation, i.e., moving the remaining useful objects to the end of the heap, forming a contiguous block of memory together, and updating the reference of all objects to the new address, and updating the heap pointers to facilitate allocating heap space for the next new object.
In general, the garbage collector is in. The NET runtime considers it necessary to run it.
System.GC
Class is a representation of the garbage collector. NET class, you can invoke System.GC.Collect()
a method that forces the garbage collector to run somewhere in the code.
When you have a large number of objects in your code that have just been dereferenced, it is more appropriate to call the garbage collector, but there is no guarantee that all unreferenced objects can be removed from the heap.
When the garbage collector runs, it actually degrades the performance of the program because it pauses all other threads of the application during its execution.
However, the. NET garbage collector uses the generation garbage collector (generational):
The managed heap is divided into several parts: No. 0 generation, 1th generation, 2nd generation
All new objects are assigned in the No. 0 generation, and garbage collection begins when allocating heap space to new objects, if the capacity of the corresponding part of the No. 0 generation () is exceeded, or if the Gc.collect () method is called.
Whenever the garbage collector performs compression, the objects left in the No. 0 generation will be moved to the 1th generation, and the No. 0 part becomes empty to place the next new object.
Similarly, when the first generation is full, compression is also performed, leaving the objects to be moved to the next generation.
The managed heap has a heap pointer that functions like a stack pointer.
3. Summary:
Use. NET Framework Development program, we do not have to worry about memory allocation problems, because there is a GC this big butler gives us everything. C # Stacks are allocated memory space during compilation, so your code must have a clear definition of the size of the stack, which is a dynamically allocated memory space during the program's run, and you can determine the size of the heap memory to allocate depending on how the program is running
When the
C # program runs on the CLR, the memory is logically divided into two chunks: stack, heap. These basic elements make up the running environment of our C # program
addfive () method, int PValue
variable, int result
variable, and so on. And the heap is mostly objects, data and so on. We can think of stacks as a box that is stacked together. When we use it, each time we take a box from the top of the way. The same is true of stacks, when a method (or type) is called to complete, take away from the top of the stack (called a frame: Call frame), and then next.
stack memory is not managed by us and is not managed by GC. When the top element of the stack is finished, it is released immediately. The heap requires a GC (garbage collection: garbage collector) cleanup.
when our program executes, there are four main types allocated in stacks and heaps: value types, reference types, pointers, directives.
- Value type: In C #, a
System.ValueType
type that inherits from is called a value type, bool byte char decimal double enum float int long sbyte short struct uint ulong ushort '
- Reference type: inherited from
System.Object
,class interface delegate object string
Pointer: In the memory area, a reference to a type, often referred to as a "pointer", is managed by the CLR (Common Language Runtime: Common language Runtime) and we cannot use it explicitly. The pointer occupies a chunk of memory in memory, which itself represents only one memory address (or null), and another chunk of memory that it points to is our real data or type.
Memory allocations for value types, reference types:
- Reference types are always allocated on the heap
Value types and pointers are always allocated where they are defined, they are not necessarily assigned to the stack, and if a value type is declared outside a method body and in a reference type, it is allocated on the heap.
Stack, each thread maintains its own dedicated thread stack while the program is running.
When a method is called, the main thread starts in the metadata of the owning assembly, finds the called method, and then compiles the result (typically the local CPU instruction) on top of the stack by JIT-on-demand. The CPU takes instructions from the top of the stack via the bus, and the driver executes.
When the program needs more heap space, the GC will need to do garbage cleanup, suspend all threads, find all unreachable objects, i.e. no referenced objects, to clean up, compress. and notifies the stack that the pointer is re-pointing to the object after the address is sorted.
4. Releasing unmanaged Resources
With the garbage collector, it means that all references to objects that are no longer needed are out of scope and allow the garbage collector to free up memory as needed.
Principle: in. NET, there is no need to call Dispose when you do not call it (the garbage collector runtime consumes/blocks the main thread).
However, the garbage collector does not know how to release unmanaged resources (such as file handles, network connections, database connections).
When defining a class, there are two mechanisms to automatically release unmanaged resources: (It is safer to use both mechanisms to prevent forgetting to invoke the Dispose()
method)
- Declares a destructor (terminator);
- Implements the interface for the class
System.IDiposable
, implements the Dispose()
method;
5. Destructors:
When the C # compiler compiles a destructor, it implicitly compiles the destructor to an equivalent Finalize()
method, ensuring that the method of the parent class is executed Finalize()
.
The definition is as follows: no return value, no parameter, no access modifier for destructor
class MyClass{ ~MyClass() { }}//以下版本是编译析构函数实际调用的等价代码:protected override void Finalize(){ try { //释放自身资源 } finally { base.Finalize(); }}
Disadvantages of destructors:
Because C # uses the garbage collector to work, it is not possible to determine when the destructor of a C # object executes.
Objects that define destructors require two garbage collection to be destroyed (the object is actually deleted when the destructor is called the second time), and objects that do not have a destructor defined can be deleted with only one processing.
If destructors are used frequently and long cleanup tasks are performed, performance can be severely impacted.
6.IDiposable Interface:
Therefore, it is recommended to override the destructor by implementing an interface for the class System.IDisposable
, implementing the Dispose()
method. IDisposable
the schema defined by the interface provides a deterministic mechanism for releasing unmanaged resources and avoids the problem of relying on the garbage collector.
IDisposable
The interface declares a Dispose()
method with no parameters and no return value. You can implement code for the Dispose () method to explicitly release all unmanaged resources that are used directly by the object and to invoke methods in all encapsulated objects that also implement IDisposable
the interface Dispose()
. This way, the method can precisely control the release of unmanaged resources.
Note: If the Dispose()
run code before the method call throws an exception, the method does not execute, so it should be used and put the try...finally
Dispose()
method finally
inside the block to ensure its execution. As follows:
null; //假设Person类实现了IDisposable接口try{ person = new Person();}finally{ if(person != null) { person.Dispose(); }}
C # provides using
keyword syntax to ensure that a method is automatically called on an object that implements an IDisposable
interface when the reference is out of scope, Dispose()
as follows:
using ( Person person = new Person() ){ ..... }
The using statement is followed by a pair of "()", which is the declaration and instantiation of a reference variable, where the variable is placed in a subsequent statement block, and the method is called automatically even if an exception is thrown when the variable goes out of scope Dispose()
.
Then, when you need to catch other exceptions, try...finally
the way you use them is clearer. Dispose()
A wrapper method is often defined for the method, which makes Close()
it clearer (the method is called only within the Close () method Dispose()
)
To prevent forgetting to invoke the Dispose()
method, it is more safe to implement both mechanisms: IDisposable
the method of implementing the interface, and also the definition of a Dispose()
destructor.
Implementation of standard Dispose mode in 7.c#
Summary: The Dispose method in a C # program, once the object of the method has been called, has not yet been garbage collected, but is no longer in use.
Let's look at the C # program (or. NET) in the resource category. In a nutshell, each of the types in C # represents a resource, and resources fall into two categories:
- Managed resources: Resources allocated and freed by the CLR, which are objects that are made by new in the CLR;
Unmanaged resources: Objects that are not managed by the CLR, Windows kernel objects such as files, database connections, sockets, COM objects, and so on;
Without exception, if our type uses unmanaged resources, or if we need to explicitly dispose of managed resources, then you need to have the type inherit the interface IDisposable
. This is equivalent to telling the caller that the type is required to explicitly dispose of the resource, and you need to call my Dispose
method.
However, this is not so simple, a standard inherited IDisposable
interface type should be implemented as follows. This implementation we call the Dispose pattern:
PublicClassSampleClass:idisposable{Demo Creating an unmanaged resourcePrivate IntPtr Nativeresource = Marshal.allochglobal (100);Demo Creating a managed resourcePrivate Anotherresource Managedresource =New Anotherresource ();PrivateBOOL disposed =False///<summary>Implements the Dispose method in IDisposable, which is used to manually invoke///</summary>PublicvoidDispose () {Must be true for Dispose (true);Notifies the garbage collection mechanism that the finalizer is no longer called (destructor) because we've already cleaned up, there's no need to continue wasting system resources.That is, remove the this GC from the finalize queue awaiting finalization. SuppressFinalize (this); }///<summary>Not necessary, provide a close method just to be more compliant with other languages (such as C + +) specification///</summary>PublicvoidClose () {Dispose ();}///<summary>Must, in case the programmer forgets to explicitly call the Dispose method///</summary> ~sampleclass () {Must be false to skip cleanup of managed resources, and to manually clean up unmanaged resources, the garbage collector automatically cleans up managed resources Dispose (FALSE); }///<summary>Protected virtual for non-sealed class modificationSeal class decoration with private///</summary>///<param name= "disposing" ></param>ProtectedVirtualvoidDispose (bool disposing) {if (disposed) {Return }if (disposing) {Clean up Managed resourcesif (managedresource! =null) {managedresource.dispose (); managedresource = null;}} //clean unmanaged Resources if (nativeresource! = IntPtr.Zero) { Marshal.freehglobal (Nativeresource); Nativeresource = IntPtr.Zero; } //let the type know that they have been released disposed = true;} public void samplepublicmethod (// Ensure that the object is available (not disposed) before any methods of the object are executed if (disposed) {throw new objectdisposedexception (" SampleClass ", //can use object here}}
In Dispose mode, almost every line has a special meaning.
In the standard dispose mode, we notice a method that begins with a ~:
/// <summary> /// 必须,以备程序员忘记了显式调用Dispose方法 /// </summary> ~SampleClass() { //必须为false Dispose(false); }
This method is called a type terminator. The whole point of providing a finalizer is that we cannot expect the caller of the type to be actively invoking the Dispose method, based on the fact that the finalizer is called by the garbage collector, and the finalizer is used as a remedy for resource release.
A type of Dispose method should allow multiple calls without throwing exceptions. For this reason, the type internally maintains a private Boolean variable disposed:
private bool disposed = false;
In the actual method of handling code cleanup, add the following judgment statement:
if (disposed) { return; } //省略清理部分的代码,并在方法的最后为disposed赋值为true disposed = true;
This means that if a type is cleaned up once, the cleanup will no longer work.
It should be noted that in the standard dispose mode, the IDisposable
Dispose method that actually implements the interface has no actual cleanup work, and it actually calls the following protected virtual method with a Boolean parameter:
///< summary> ///non-sealed class decoration with protected virtual ///seal class decoration with private / </summary> ///<param name= "disposing" ></param> protected virtual void dispose (bool disposing) {//Omit code}
The reason for providing such a protected virtual method is to take into account the fact that this type is inherited by other classes. If a type has a subclass, the subclass might implement its own dispose pattern. A protected virtual method is used to remind subclasses that they must notice the cleanup of the parent class when implementing their own cleanup method, that is, the subclass needs to call base in its own release method. Dispose method.
Also, we should have noticed that the virtual method that actually writes the resource release code is with a Boolean parameter. This parameter is provided because we treat managed and unmanaged resources differently when the resource is released.
The call parameter is true in the parameterless Dispose method for the explicitly freed resource that is called by the caller:
public void Dispose() { //必须为true Dispose(true); //其他省略 }
This indicates that the code should handle both the managed and unmanaged resources at this time.
In the finalizer for an implicit cleanup resource that is called by the garbage collector, the calling parameter is false:
~SampleClass() { //必须为false Dispose(false); }
This indicates that when an implicit cleanup is available, the unmanaged resources are processed as long as they are handled.
So why treat managed and unmanaged resources differently. Before we elaborate on this, we need to understand first: does the managed resource need to be cleaned up manually? You might want to first divide the types in C # into two categories, one inheriting the IDisposable interface, and one without inheritance. The former, which we call a non-trivial type for the time being, is called the normal type.
Non-trivial type because it contains an unmanaged resource, it needs to inherit the IDisposable interface, but this contains the unmanaged resource's type itself, which is a managed resource. So does the managed resource need to be cleaned up manually? The answer to this question is that the normal type in the managed resource does not need to be cleaned manually, rather than the normal type, which needs to be cleaned manually (that is, the Dispose method is called).
The idea of the Dispose pattern design is based on: if the caller explicitly calls the Dispose method, then the type should be programmed to release all resources. If the caller forgets to call the Dispose method, then the type assumes that all of its managed resources (even those of the non-trivial type described in the previous paragraph) are all handed over to the garbage collector for recycling without manual cleanup. With this in mind, we understand why the parameter passed by the virtual method in the Dispose method is true, and the parameter passed in by the virtual method is false in the finalizer.
8. Make a reference to a static field that is no longer needed in a timely manner equals null:
In a CLR managed application, there is a concept of a root, where static fields, method parameters, and local variables can exist as roots (value types cannot be roots and only pointers of reference types can be used as roots). The garbage collector checks the root upstream along the thread stack, and if the reference to the root is found to be empty, then the root is marked for release.
The JIT compiler is an optimized compiler, regardless of whether we assign a variable to NULL, the statement is ignored, and when we set the project to release mode, the statement will not be compiled into the runtime at all.
However, in another case, be careful to assign a value of NULL to a variable in a timely manner. That is the static field of the type. Also, assigning a value of NULL to a type object does not imply that a static field of that type is assigned null at the same time: When a garbage collection is performed, a static field of that type is not recycled when the object of the type is recycled (because the static field belongs to the class, it may later be used by other instances of that type).
In actual work, once we feel that our static reference type parameters occupy a large amount of memory space and are no longer used after use, you can assign them to null immediately. This may not be necessary, but it's definitely a good habit.
C # Memory Management parsing