行动力决定了一个人的成败,有想法,就去做! C#的内存管理原理解析+标准Dispose模式的实现

尽管.NET运行库负责处理大部分内存管理工作,但C#程序员仍然必须理解内存管理的工作原理,了解如何高效地处理非托管的资源,才能在非常注重性能的系统中高效地处理内存。
C#编程的一个优点就是程序员不必担心具体的内存管理,垃圾回收器会自动处理所有的内存清理工作。用户可以得到近乎像C++语言那样的效率,而不必考虑像C++中复杂的内存管理工作。但我们仍需要理解程序在后台如何处理内存,才有助于提高应用程序的速度和性能。

先了解一下Windows系统中的虚拟寻址系统:

该系统把程序可用的内存地址映射到硬件内存中的实际地址上,在32位处理器上的每个进程都可以使用4GB的硬件内存(64位处理器更大),这个4GB的内存包含了程序的所有部分(包括可执行代码、代码加载的所有DLL、程序运行时使用的所有变量的内容)
这个4GB的内存称为虚拟地址空间,或虚拟内存。其中的每个存储单元都是从0开始排序的。要访问存储在内存的某个空间中的一个值,就需要提供表示该存储单元的数字。编译器负责把变量名转换为处理器可以理解的内存地址。

值类型和引用类型在C#中的数据类型分为值类型和引用类型,对他们使用了不同但又相似的内存管理机制。

1.值数据类型的内存管理

在进程的虚拟内存中,有一个区域称为栈。C#的值类型数据、传递给方法的参数副本都存储在这个栈中。在栈中存储数据时,是从高内存地址向低内存地址填充的。
操作系统维护一个变量,称为栈指针。栈指针为当前变量所占内存的最后一个字节地址,栈指针会根据需要随时调整,它总是会调整为指向栈中下一个空闲存储单元的地址。当有新的内存需求时,就根据当前栈指针的值开始往下来为该需求分配足够的内存单元,分配完后,栈指针更新为当前变量所占内存的最后一个字节地址,它将在下一次分配内存时调整为指向下一个空闲单元。
如:int a= 10;
声明一个整型的变量需要32位,也就是4个字节内存,假设当前栈指针为89999,则系统就会为变量a分配4个内存单元,分别为89996~89999,之后,栈指针更新为89995
double d = 20.13; //需要64位,也就是8个字节内存,存储在89988~89995

栈的工作方式是先进后出(FIFO):在释放变量时,总是先释放后面声明的变量(后面分配内存)。

2.引用数据类型的内存管理

引用类型对象的引用存储在栈中(占4个字节的空间),而它的实际数据存储在主托管堆或大对象堆上,托管堆是可用的4GB虚拟内存中的另一个内存区域。
大对象堆:在.NET下,因为压缩较大对象(大于85000个字节)很影响性能,所以为它们分配了自己的托管堆。.NET垃圾回收器不对大对象堆执行压缩过程。
如:Person arabel= new Person();
声明变量arabel时,在栈上为该变量分配4个字节的空间以存储一个引用,new运算符为对象Person对象在堆上分配空间,然后把该空间的地址赋给变量arabel,而构造函数则用来初始化。

.NET运行库为了给对象arabel分配空间,需要搜索堆,选取第一个未使用的且足够容纳对象所有数据的连续块。但垃圾回收器程序在回收堆中所有无引用的对象后,会执行压缩操作,即:把剩下的有用对象移动到堆的端部,挨在一起形成一个连续的内存块,并更新所有对象的引用为新地址,同时更新堆指针,方便为下一个新对象分配堆空间。

一般情况下,垃圾回收器在.NET运行库认为需要它时运行。
System.GC类是一个表示垃圾回收器的.NET类,可以调用System.GC.Collect()方法,强迫垃圾回收器在代码的某个地方运行。
当代码中有大量的对象刚刚取消引用,就比较适合调用垃圾回收器,但不能保证所有未引用的对象都能从堆中删除。
垃圾回收器运行时,它实际上会降低程序的性能,因为在它执行期间,将会暂停应用程序的其它所有线程。

但.NET垃圾回收器使用了"世代垃圾回收器(generational)":

托管堆分为几个部分:第0代,第1代,第2代,第3代,....
所有新对象都被分配在第0代部分,在给新对象分配堆空间时,如果超出了第0代对应的部分的容量(),或者调用了GC.Collect()方法,就会开始进行垃圾回收。
每当垃圾回收器执行压缩时,第0代部分留下来的对象将会被移动到第1代上,此时第0代部分就变成空,用来放置下一个新对象。
类似的,当第一代满时,也会进行压缩,剩下对象移到下一代。
托管堆有一个堆指针,功能和栈指针类似。

3.总结:

使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切。C#中栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;堆是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小

C#程序在CLR上运行的时候,内存从逻辑上划分两大块:栈,堆。这俩基本元素组成我们C#程序的运行环境
栈通常保存着我们代码执行的步骤,如 AddFive()方法,int pValue变量,int result变量等。而堆上存放的则多是对象,数据等。我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子。栈也是如此,当一个方法(或类型)被调用完成的时候,就从栈顶取走(called a Frame:调用帧),接着下一个。
堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉(等待垃圾回收器来清理)。
栈内存无需我们管理,也不受GC管理。当栈顶元素使用完毕,立马释放。而堆则需要GC(Garbage collection:垃圾收集器)清理。
当我们的程序执行的时候,在栈和堆中分配有四种主要的类型:值类型,引用类型,指针,指令。

  • 值类型:在C#中,继承自System.ValueType的类型被称为值类型,bool byte char decimal double enum float int long sbyte short struct uint ulong ushort`

  • 引用类型:继承自System.Object, class  interface  delegate  object  string

  • 指针:在内存区中,指向一个类型的引用,通常被称为“指针”,它是受CLR( Common Language Runtime:公共语言运行时)管理,我们不能显式使用。指针在内存中占一块内存区,它本身只代表一个内存地址(或者null),它所指向的另一块内存区才是我们真正的数据或者类型。

    值类型、引用类型的内存分配:
  • 引用类型总是被分配在堆上

  • 值类型和指针总是分配在被定义的地方,他们不一定被分配到栈上,如果一个值类型被声明在一个方法体外并且在一个引用类型中,那它就会在堆上进行分配。

栈(Stack),在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。
当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。

当程序需要更多的堆空间时,GC需要进行垃圾清理工作,暂停所有线程,找出所有不可达到对象,即无被引用的对象,进行清理、压缩。并通知栈中的指针重新指向地址排序后的对象。

4.释放非托管的资源

有了垃圾回收器,意味着我们只要让不再需要的对象的所有引用都超出作用域,并允许垃圾回收器在需要时释放内存即可。
原则:在.net中,没有必要调用Dispose的时候,你就不要调用它(垃圾回收器运行时会占用/阻塞主线程)。
但是,垃圾回收器不知道如何释放非托管的资源(如文件句柄、网络连接、数据库连接)。
在定义一个类时,有两种机制来自动释放非托管的资源:(更保险的做法是同时使用两种机制,防止忘记调用Dispose()方法)

  1. 声明一个析构函数(终结器);

  2. 为类实现System.IDiposable接口,实现Dispose()方法;

5.析构函数:

C#编译器在编译析构函数时,它会隐式地把析构函数编译为等价于Finalize()方法,从而确保执行父类的Finalize()方法。
定义方式如下:析构函数无返回值、无参数、无访问修饰符

class MyClass{~MyClass(){}
}//以下版本是编译析构函数实际调用的等价代码:protected override void Finalize(){    try{ //释放自身资源 }finally{ base.Finalize(); }
}
析构函数的缺点:

由于C#使用垃圾回收器的工作方式,无法确定C#对象的析构函数何时执行。
定义了析构函数的对象需要经过两次垃圾回收处理才能被销毁(第二次调用析构函数时才真正删除对象),而没有定义析构函数的对象反而只需要一次处理即可删除。
如果频繁使用析构函数,而且执行长时间的清理任务,会严重影响性能。

6.IDiposable接口:

所以,推荐通过为类实现System.IDisposable接口,实现Dispose()方法,来替代析构函数。IDisposable接口定义的模式为释放非托管资源提供了确定的机制,并避免了对垃圾回收器依赖的问题。
IDisposable接口声明了Dispose()方法,无参数,无返回值。可以为Dispose()方法实现代码来显式地释放由对象直接使用的所有非托管资源,并在所有也实现IDisposable接口的封装对象中调用Dispose()方法。这样,该方法可以可以精确地控制非托管资源的释放。
注意:如果在Dispose()方法调用之前的运行代码抛出了异常,则该方法就执行不到了,所以应该使用try...finally,并把Dispose()方法放在finally块内,以确保它的执行。如下:

Person person = null;  //假设Person类实现了IDisposable接口try{person = new Person();
}finally{    if(person != null){person.Dispose();}
}

C#提供了using关键字语法,可以确保在实现了IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法,如下:

using ( Person person = new Person() )
{ ..... }

using语句后面是一对"()",其中是引用变量的声明和实例化,该语句是其中的变量放在随后的语句块中,并且在变量超出作用域时,即使抛出异常,也会自动调用Dispose()方法。
然后,在需要捕获其它异常时,使用try...finally的方式就会比较清晰。而常常为Dispose()方法定义一个包装方法Close(),这样显得更清晰明了(Close()方法内仅调用Dispose()方法)

为了防止忘记调用Dispose()方法,更保险的做法是同时实现两种机制:即实现IDisposable接口的Dispose()方法,也定义析构函数。

7.C#中标准Dispose模式的实现

摘要:C#程序中的Dispose方法,一旦被调用了该方法的对象,虽然还没有垃圾回收,但实际上已经不能再使用了。
先了解一下C#程序(或者说.NET)中的资源分类。简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类:

  • 托管资源:由CLR管理分配和释放的资源,即由CLR里new出来的对象;

  • 非托管资源:不受CLR管理的对象,windows内核对象,如文件、数据库连接、套接字、COM对象等;
      毫无例外地,如果我们的类型使用到了非托管资源,或者需要显式释放的托管资源,那么,就需要让类型继承接口IDisposable。这相当于是告诉调用者,该类型是需要显式释放资源的,你需要调用我的Dispose方法。
      不过,这一切并不这么简单,一个标准的继承了IDisposable接口的类型应该像下面这样去实现。这种实现我们称之为Dispose模式:

    public class SampleClass : IDisposable{    //演示创建一个非托管资源private IntPtr nativeResource = Marshal.AllocHGlobal(100);    //演示创建一个托管资源private AnotherResource managedResource = new AnotherResource();    private bool disposed = false;    /// <summary>/// 实现IDisposable中的Dispose方法,用于手动调用/// </summary>public void Dispose()    {        //必须为trueDispose(true);        //通知垃圾回收机制不再调用终结器(析构器)因为我们已经自己清理了,没必要继续浪费系统资源//即:从等待终结的Finalize队列中移除thisGC.SuppressFinalize(this);}    /// <summary>/// 不是必要的,提供一个Close方法仅仅是为了更符合其他语言(如C++)的规范/// </summary>public void Close()    {Dispose();}    /// <summary>/// 必须,以备程序员忘记了显式调用Dispose方法/// </summary>~SampleClass(){        //必须为false,跳过托管资源的清理,只手动清理非托管的资源,垃圾回收器会自动清理托管资源Dispose(false);}    /// <summary>/// 非密封类修饰用protected virtual/// 密封类修饰用private/// </summary>/// <param name="disposing"></param>protected virtual void Dispose(bool disposing)    {        if (disposed){            return;}        if (disposing){            // 清理托管资源if (managedResource != null){managedResource.Dispose();managedResource = null;}}        // 清理非托管资源if (nativeResource != IntPtr.Zero){Marshal.FreeHGlobal(nativeResource);nativeResource = IntPtr.Zero;}        //让类型知道自己已经被释放disposed = true;}    public void SamplePublicMethod()    {        //确保在执行对象的任何方法之前,该对象可用(未被释放)if (disposed) {            throw new ObjectDisposedException("SampleClass", "SampleClass is disposed");}        //在这里可以使用对象}
    }

      在Dispose模式中,几乎每一行都有特殊的含义。
      在标准的Dispose模式中,我们注意到一个以~开头的方法:

        /// <summary>/// 必须,以备程序员忘记了显式调用Dispose方法/// </summary>~SampleClass(){        //必须为falseDispose(false);}

      这个方法叫做类型的终结器。提供终结器的全部意义在于:我们不能奢望类型的调用者肯定会主动调用Dispose方法,基于终结器会被垃圾回收器调用这个特点,终结器被用做资源释放的补救措施。
      一个类型的Dispose方法应该允许被多次调用而不抛异常。鉴于这个原因,类型内部维护了一个私有的布尔型变量disposed:
    private bool disposed = false;
      在实际处理代码清理的方法中,加入了如下的判断语句:

            if (disposed){            return;}        //省略清理部分的代码,并在方法的最后为disposed赋值为truedisposed = true;

      这意味着类型如果被清理过一次,则清理工作将不再进行。
      应该注意到:在标准的Dispose模式中,真正实现IDisposable接口的Dispose方法,并没有实际的清理工作,它实际调用的是下面这个带布尔参数的受保护的虚方法:

        /// <summary>/// 非密封类修饰用protected virtual/// 密封类修饰用private/// </summary>/// <param name="disposing"></param>protected virtual void Dispose(bool disposing)    {      
         //省略代码}

      之所以提供这样一个受保护的虚方法,是为了考虑到这个类型会被其他类继承的情况。如果类型存在一个子类,子类也许会实现自己的Dispose模式。受保护的虚方法用来提醒子类必须在实现自己的清理方法的时候注意到父类的清理工作,即子类需要在自己的释放方法中调用base.Dispose方法。
      还有,我们应该已经注意到了真正撰写资源释放代码的那个虚方法是带有一个布尔参数的。之所以提供这个参数,是因为我们在资源释放时要区别对待托管资源和非托管资源。
      在供调用者调用的显式释放资源的无参Dispose方法中,调用参数是true:

        public void Dispose()    {        //必须为trueDispose(true);        //其他省略}

      这表明,这个时候代码要同时处理托管资源和非托管资源。
      在供垃圾回收器调用的隐式清理资源的终结器中,调用参数是false:

        ~SampleClass(){        //必须为falseDispose(false);}

      这表明,隐式清理时,只要处理非托管资源就可以了。
      那么,为什么要区别对待托管资源和非托管资源。在认真阐述这个问题之前,我们需要首先弄明白:托管资源需要手动清理吗?不妨先将C#中的类型分为两类,一类继承了IDisposable接口,一类则没有继承。前者,我们暂时称之为非普通类型,后者我们称之为普通类型。
      非普通类型因为包含非托管资源,所以它需要继承IDisposable接口,但是,这个包含非托管资源的类型本身,它是一个托管资源。所以说,托管资源需要手动清理吗?这个问题的答案是:托管资源中的普通类型,不需要手动清理,而非普通类型,是需要手动清理的(即调用Dispose方法)。
      Dispose模式设计的思路基于:如果调用者显式调用了Dispose方法,那么类型就该按部就班为自己的所以资源全部释放掉。如果调用者忘记调用Dispose方法,那么类型就假定自己的所有托管资源(哪怕是那些上段中阐述的非普通类型)全部交给垃圾回收器去回收,而不进行手工清理。理解了这一点,我们就理解了为什么Dispose方法中,虚方法传入的参数是true,而终结器中,虚方法传入的参数是false。

8.及时让不再需要的静态字段的引用等于null:

在CLR托管应用程序中,存在一个根的概念,类型的静态字段、方法参数以及局部变量都可以作为根存在(值类型不能作为根,只有引用类型的指针才能作为根)。垃圾回收器会沿着线程栈上行检查根,如果发现该根的引用为空,则标记该根为可被释放。
而JIT编译器是一个经过优化的编译器,无论我们是否为变量赋值为null,该语句都会被忽略掉,在我们将项目设置为Release模式下,该语句将根本不会被编译进运行时内。
但是,在另外一种情况下,却要注意及时为变量赋值为null。那就是类型的静态字段。而且,为类型对象赋值为null,并不意味着同时为该类型的静态字段赋值为null:当执行垃圾回收时,当类型的对象被回收的时候,该类型的静态字段并没有被回收(因为静态字段是属于类的,它日后可能会被该类型的其它实例继续使用)。
实际工作中,一旦我们感觉到自己的静态引用类型参数占用内存空间比较大,并且使用完毕后不再使用,则可以立刻将其赋值为null。这也许并不必要,但这绝对是一个好习惯。

相关文章:

  • 深入理解C#:编程技巧总结(一)

  • 深入理解C#:编程技巧总结(二)

原文地址:http://www.cnblogs.com/susufufu/p/6878974.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/325467.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

让面试官颤抖的 HTTP 2.0 协议面试题

转载自 让面试官颤抖的 HTTP 2.0 协议面试题 Http协议&#xff0c;对于拥有丰富开发经验的程序员来说简直是信手拈来&#xff0c;家常便饭。虽然天天见&#xff0c;但是对于http协议的问题&#xff0c;可能很多人在没有积极准备的情况下&#xff0c;不一定能很好的回答出来。…

一步步学习EF Core(3.EF Core2.0路线图)

前言 这几天一直在研究EF Core的官方文档,暂时没有发现什么比较新的和EF6.x差距比较大的东西.不过我倒是发现了EF Core的路线图更新了,下面我们就来看看 今天我们来看看最新的EF Core 2.0路线图 E文好的移步:https://github.com/aspnet/EntityFramework/wiki/Roadmap#ef-core…

Docker 核心概念、安装、端口映射及常用操作命令,详细到令人发指。

转载自 Docker 核心概念、安装、端口映射及常用操作命令&#xff0c;详细到令人发指。 Docker简介 Docker是开源应用容器引擎&#xff0c;轻量级容器技术。 基于Go语言&#xff0c;并遵循Apache2.0协议开源 Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移…

Build Tour 2017 中国站北京、上海报名了

微软于 5 月 10 日在总部西雅图举办的 Build 2017 大会上&#xff0c;发布了针对云计算、人工智能、Windows 以及混合现实平台等技术的一系列重要更新&#xff0c;这令众多来自企业、ISV、初创企业的开发者&#xff0c;学生开发者&#xff0c;以及技术爱好者兴奋不已。 为了帮助…

getOrDefault()和subList()

返回 key 相映射的的 value&#xff0c;如果给定的 key 在映射关系中找不到&#xff0c;则返回指定的默认值。

.NET Core类库项目中如何读取appsettings.json中的配置

这是一位朋友问我的问题&#xff0c;写篇随笔回答一下。有2种方法&#xff0c;一种叫丑陋的方法 —— IConfiguration &#xff0c;一种叫优雅的方法 —— IOptions 。 1&#xff09;先看丑陋的方法 比如在 RedisClient 中需要读取 appsettings.json 中的 redis 连接字符串&a…

js引擎执行代码的基本流程

js引擎执行代码的基本流程 先执行初始化代码: 包含一些特别的代码设置定时器绑定监听发送ajax请求后面在某个时刻才会执行回调代码

微服务框架下的思维变化-OSS.Core基础思路

如今框架两字已经烂大街了&#xff0c;xx公司架构设计随处可见&#xff0c;不过大多看个热闹&#xff0c;这些框架如何来的&#xff0c;细节又是如何思考的&#xff0c;相互之间的隔离依据又是什么...相信很多朋友应该依然存在自己的疑惑&#xff0c;特别是越来越火热的微服务以…

Spring Boot 2.x 启动全过程源码分析(全)

转载自 Spring Boot 2.x 启动全过程源码分析&#xff08;全&#xff09; 上篇《Spring Boot 2.x 启动全过程源码分析&#xff08;一&#xff09;入口类剖析》我们分析了 Spring Boot 入口类 SpringApplication 的源码&#xff0c;并知道了其构造原理&#xff0c;这篇我们继…

Vue 2017 现状与展望 | 视频+PPT+速记快速回顾

微软Typescript团队和VS Code团队亲自给Vue开发插件&#xff0c;下一个版本的Vue 2.4将由微软提供支持Vue使用Typescript&#xff0c;之前为VS Code写vue扩展插件的人已入职微软VS Code团队 讲师 | 尤雨溪 速记 | kalasoo 5 月 20 日&#xff0c;在全球首届 VueConf 上&#xf…

6 道 BATJ 必考的 Java 面试题

转载自 6 道 BATJ 必考的 Java 面试题 题目一 请对比 Exception 和 Error&#xff0c;另外&#xff0c;运行时异常与一般异常有什么区别&#xff1f; 考点分析&#xff1a; 分析 Exception 和 Error 的区别&#xff0c;是从概念角度考察了 Java 处理机制。总的来说&#…

终于知道什么情况下需要实现.NET Core中的IOptions接口

自从接触 IOptions 之后&#xff0c;一直纠结这样的问题&#xff1a;自己定义的 Options 要不要实现 IOptions 接口。 微软有的项目中实现了&#xff0c;比如 Caching 中的 MemoryCacheOptions &#xff1a; public class MemoryCacheOptions : IOptions<MemoryCacheOptio…

Amazing ASP.NET Core 2.0

前言 ASP.NET Core 的变化和发展速度是飞快的&#xff0c;当你发现你还没有掌握 ASP.NET Core 1.0 的时候&#xff0c; 2.0 已经快要发布了&#xff0c;目前 2.0 处于 Preview 1 版本&#xff0c;意味着功能已经基本确定&#xff0c;还没有学习过 ASP.NET Core 的同学可以直接…

Java面试常问计算机网络问题

转载自 Java面试常问计算机网络问题 一、GET 和 POST 的区别 GET请注意&#xff0c;查询字符串&#xff08;名称/值对&#xff09;是在 GET 请求的 URL 中发送的&#xff1a;/test/demo_form.asp?name1value1&name2value2 GET 请求可被缓存 GET 请求保留在浏览器历史…

使用DocFX生成文档

文档生成工具DocFX&#xff0c; 类似JSDoc或Sphinx&#xff0c;可以从源代码中提取注释生成文档之外&#xff0c;而且还有语法支持你加入其他的文件链接到API添加额外的说明&#xff0c;DocFX会扫描你的源代码和附加的文件为你生成一个完整的HTML模版网站&#xff0c;你可以自己…

如何在一分钟内搞定面试官

转载自 如何在一分钟内搞定面试官 很多人的求职面试的过程中都会遇到这个问题&#xff1a; “请做个自我介绍。” 有的人&#xff0c;可以口若悬河、妙语连珠讲3分钟&#xff0c;有的人&#xff0c;可能磕磕巴巴&#xff0c;讲了30秒&#xff0c;前者一定能胜过后者&#x…

基于ZKWeb + Angular 4.0的开源管理后台Demo

这是一套基于ZKWeb网页框架和Angular 4.0编写的开源管理后台Demo&#xff0c;实现了前后端分离和模块化开发&#xff0c; 地址是: https://github.com/zkweb-framework/ZKWeb.MVVMDemo &#xff0c;开源协议是MIT&#xff0c;你可以随意的修改并用于个人或商业用途 我之前已经…

祝我们的所有女孩子,女生节快乐~

公历3月7日是女生节。起源于20世纪90年代初&#xff0c;由山东大学发起&#xff0c;后发展于中国各高校&#xff0c;是一个关爱女生、展现高校女生风采的节日。女神节快乐祝我们18级青鸟1班的女孩儿们&#xff0c;女生节快乐&#xff01;愿快乐与你作陪&#xff0c;美丽将你跟随…

从 0 开始手写一个 Spring MVC 框架,向高手进阶

转载自 从 0 开始手写一个 Spring MVC 框架&#xff0c;向高手进阶 Spring框架对于Java后端程序员来说再熟悉不过了&#xff0c;以前只知道它用的反射实现的&#xff0c;但了解之后才知道有很多巧妙的设计在里面。如果不看Spring的源码&#xff0c;你将会失去一次和大师学习…