繁忙的工作总容易让我们忽视最基础的知识,手里的活停一停,下楼呼吸下新鲜空气(北京的朋友抱歉了),让大脑切换下进程。
回想工作中我们所遇到的难点,嗯,好多都是我们对基础知识了解得不够透彻,或者说只知道了表层的东西。而往往我们总是被这些表层东西所欺骗了,最后等待我们的就是bug量增多,性能低下,运行不稳定,维护成本剧增,客户满意度下降,更严重的便可能导致项目进入恶性循环。可想而知,我们所面临的挑战是多么艰巨。
言归正传,本节我们主要讲的内容是.Net中变量内存分配,是的,就讲这么简单的知识。
一、我们先看下类的成员变量:
public class Customer
{
int customerId;
string customerName;
}
public class Customer
{
int customerId = 0;
string customerName = null;
}
乍看之下,我们会觉得第二种写法更加合情合理,但我们的编译器没有那么智能。他总会首先帮我们将成员变量赋予一个初始值,当我们人为赋予初始值的时候,编译器会在调用默认构造函数之前用我们赋予的初始化值替换他默认初始化的值。也就是除非我们初始化的数据是业务相关数据,否则我们赋默认值则给JIT增加了负担。
二、静态变量:
当我们增加个CustomerCount int类型静态变量,并赋予初始值0时,这个时候我们会发现,编译器默认给我们创建了一个静态构造函数,并在静态构造函数中替换掉编译器默认初始化的值。如果我们不人为初始化呢,其实编译器还会自动为我们提供一个静态的构造函数,除非是我们的类没有静态变量。静态构造函数只会执行一次。
三、常量:
1、我们知道静态常量无需分配内存的,而且必须定义的时候就得赋初始化值,这样的优点我们也不难发现他会带来潜伏的bug,当他的引用(其实是那个常量值)分布到不同的程序集中时,如果后期由于业务变化需要改变常量值时,项目必须整体重新编译一次,否则其他引用的程序集常量还保持原来值。可以看到,这给我们维护带来了不便。
2、由于静态常量带来的不便,所以这个时候动态常量便应运而生了,动态常量需要分配内存的,其必须在声明或在构造函数中赋初始化值,如果不人为赋值,编译器会初始化默认值的,但这样的常量是毫无意义的。
四、局部变量:
我们都很清楚,C#中访问局部变量的之前,我们必须人为为他赋初值,如果没有赋值,则编译就不会通过。其实这个时候,编译器是很聪明的,我们声明了一个局部变量,但不给他赋值,我们要这个局部变量干嘛呢(闭包情况不在这个范围)?他并不是类成员变量,可以共享。反过来想一想,如果编译器也为局部变量赋初值,那么JIT的负担得多么重。所以默认情况下,C#是不会初始化局部变量的。
下面我们来看看局部变量内存分配的情况:
public class Customer
{
public Customer GetCustomer()
{
int customerId = 4;
Customer customer = GetCustomerById(customerId);
return customer;
}
private Customer GetCustomerById(int id)
{
Customer customer = new Customer();
return customer;
}
}
好,具体情况我们来看图:
说明:①②给局部变量customerId赋值。
③④将this和customerId值压入堆栈,准备调用GetCustomerById方法。
说明:⑤⑥调用GetCustomerById方法
⑦⑧在托管堆上创建Customer对象,并给局部变量customer赋值
说明:⑨⑩给返回值returnObj赋值,returnObj是编译器创建的临时存放返回值得局部变量
⑪将返回值的内容压入堆栈
说明:⑫GetCustomerById方法执行完毕,将返回值传递到调用方维护的堆栈里。此时GetCustomerById局部变量都被堆栈弹出,这部分内存自动收回。
⑬给局部变量customer赋值
说明:⑭⑮给局部变量returnObj赋值
⑯将返回值压入堆栈中
上图,只是为了形象说明局部变量内存分配情况,并不是最终本地代码执行时内存情况,JIT还会为我们做很多优化,而且局部变量表没有体现到堆栈中。
从上图中,我们可以知道,局部变量中值类型的值存放到堆栈中(并不是所有值类型都存放在堆栈区,有例外情况,比如闭包和yield的情况就比较特殊,编译器会提升局部变量的),而引用类型则是在托管堆中开辟内存存放引用类型对象,其对象引用则是存放到堆栈中。
——Aaron.Pan