内存模型是随着越来越丰富和复杂的对象生命周期要求的发展而发展起来的。
最初的内存模型完全是线性的,静态的,一个程序运行时所有需要的对象都是在运行前完全准备好了的,运行完了时释放掉。典型的代表就是Fortran语言。这种语言的运行性能非常高(当然了,没有任何别的消耗嘛),但是表达能力受到限制(毕竟,要求静态的确定一切对象和内存的绑定关系)。最明显的一个限制就是没办法支持递归。这种内存模型支持的对象的生命周期跟应用程序的生命周期完全一致。同生共死,天下大同。
Alogal的出现引入了一个强大的概念: lexical scope,内存模型也相应的出现了细分概念:栈。栈就是那种先进后出的容器,它完美的切合了lexical scope。同时,栈上的对象的生命周期也很清晰,进栈是出生,出栈时死亡。栈这个概念是如此的清晰、容易理解,而且如此的强大,导致现代各种语言的内存模型中,栈都占有非常显著的地位。栈自然地解决了递归问题。栈上对象的生命周期的管理完全可以自动化而高效的完成。但是,栈仍然不足以满足某些对象生命周期的要求。
我先说说这是那种样子的生命周期。简单的说,栈对于共享的对象的生命周期无能为力。也就是说,某个对象,在多个平级的或者嵌套的scope之间共享,而栈却没办法解决。非得用栈来满足这种需求的话,会导致大量的memcpy,导致性能的低下。而且还是模拟的解决这个问题。其实,对于参数和返回值,基于栈架构的内存模型和对象就是采用复制和反向复制的方式来完成的。
面对这个挑战(共享对象生命周期问题),第一个反应就是避免共享,其实就是我前面说的完全副本的方式。这种方式在一定程度上有效,不过,对于大对象是非常不划算的。
C 和Pascal语言来了,这类语言提供了另一个概念叫做堆(其实,堆这个概念并不是它们最先引入的,但是是由它们发扬光大的)。从此内存模型分裂为两个界限明显的类型:栈和堆。堆就是那种随机进随机出的对象的栖息之地。也就是说,这种对象的生命周期不再可以向栈对象那样自动高效的管理了。有了这样的基础设施,共享对象就变得简单了。创建一个堆对象,然后A使用之,只要不销毁,B就可以使用之,这样,该对象就可以由A和B共享了。当然,牵扯到共享,必然会涉及到同步和互斥的问题。也就是A和B究竟怎样访问该对象的问题。一般来说,采取的策略是是把并发访问串行化的技术。由此导出很多互斥的技术。我们就不在这上面纠缠了。我们关注的是对象本身的生命周期,也就是说。这个可以共享的对象的生、死问题。由谁负责生(创建)似乎大家都没有什么抱怨的,由第一个要用该对象的负责,这也不会导致麻烦。原因是:如果你不负责生,那么该对象就不存在,你也就没办法使用了。:)由谁负责死(销毁)呢。这个看起来也很简单:最后一个用完了就销毁。而事实上也确实就这么简单。但是这儿有一点麻烦。跟创建不一样,创建不会被忘记,而销毁会。忘记销毁对象对自己没有伤害,所以,就选择忘记吧。:)这不是拉屎不擦屁股的问题。这个不难受。
这样说吧。现代的语言按照由谁负责堆对象的销毁问题大致可以分成两大类,一类叫做有垃圾回收的,一类没有。有垃圾回收的那一类是由运行环境(运行时)负责销毁共享对象,没有垃圾回收的那一类由程序自己负责销毁共享对象。对程序员来说,当然有垃圾回收的语言跟友好了。但问题是垃圾回收需要首先搞定哪一些共享对象不再需要了。这是一个比较困难的问题。而对于没有垃圾回收的那种语言来说,程序自己逻辑上应该很清楚哪一些对象不再需要了,可以销毁了。
一般情况下,我们把那些由运行时负责销毁共享对象的那些堆叫做托管堆,负责销毁共享对象的运行时的一部分叫做垃圾回收器(GC),也就是说,托管堆就使那种委托给GC管理的堆。
相应的,没有委托给GC管理的堆就是普通的堆了。普通堆有应用程序自己负责销毁共享对象。
上面的描述还没有提到的一个问题是:对象究竟在什么时候销毁?由于一个对象不立即销毁一般不会导致什么重大的问题,所以销毁的时机相对来说可以很灵活。当然,肯定是在最后一个使用了以后。但是以后是多久呢?是立即销毁?还是延迟一段时间?如果是延迟,那么究竟延迟多长时间?还是更进一步的说延迟不定长的时间?
由于一般情况下,我们认为批量销毁比一个一个销毁要快一些,所以一个指导性的方案就是等到垃圾(生命周期应该已经结束了的共享对象)积累到一定的量以后(只要不影响程序的正常运行)批量销毁。这一般会导致销毁的不定长延迟。
上面说了:“由于一个对象不立即销毁一般不会导致什么重大的问题,所以销毁的时机相对来说可以很灵活。”。注意这个句子里面的“一般”这个字眼,这也就是说:在特殊情况下,不立即销毁会导致重大问题。那种情况是特殊情况呢?就是那种销毁对象的动作还带有别的副作用,而这个副作用会影响以后程序的运作行为的情况。这种情况在使用C++的RAII的时候是基本情况。
现在错略的说说垃圾回收的基本方法。
我们把托管堆中的对象以及对象的互相引用关系看作是一个“图”(数学上定点和边的集合),应用程序的栈上有一些引用引用到这个图的某些顶点。从GC的角度来看,应用程序的作用就是不断地改变图的连通性,故此把应用程序叫做Mutator。GC就是通过查看图的连通性把那些孤立的顶点(就是那种生命周期结束的共享对象)回收的。
那么,我们怎么知道图的连通性的呢?
1、可以通过从根集跟踪。所谓根集,就是应用程序栈上的引用集合。我们知道,这种跟踪肯定可以搞定图的连通性,但是,这要求我们能够区分什么是引用,而什么不是。
2、每次应用程序修改图的连通性的时候,记录下来。这样GC就可以直接销毁那些孤立的顶点了。
第一种方式是最常用的方式,或者甚至可以这样说,第二种方式根本没人用。
不过,第二种方式的某种变形方式用的人却很多。那就是RefCount。第二种方式把集中存放的连通图信息分散的放置到各个共享对象身上,让他们记住自己被多少个别的对象引用就行了。这样当它发觉自己被0个对象引用的时候,就自裁。
这个方式看起来非常好,也非常干净,但是有两个缺点。一是性能。每一次引用一个对象,都需要增加这个引用计数,放弃引用的时候得减少之。影响了性能。另一个是对于环形图的无能为力。
分享到:
相关推荐
领域对象的生命周期是指从创建到销毁的过程中,对象经历的各种状态及其变化。这个主题通常与面向对象编程(OOP)和领域驱动设计(DDD)紧密相关。下面我们将深入探讨领域对象的生命周期及其相关知识点。 首先,我们...
堆内存可以分为Old Generation和New Generation两部分,Old Generation存放生命周期长久的实例对象,而新的对象实例一般放在New Generation。New Generation还可以再分为Eden区和Survivor区,新的对象实例总是首先...
UIView的生命周期对于理解iOS应用中视图的加载和管理至关重要。在开发iOS应用时,了解UIView及其子类的生命周期方法,可以让开发者合理地安排资源的分配和释放,优化应用的性能,以及提供更好的用户体验。 首先,...
JAVA内存模型与垃圾回收是Java开发中至关重要的概念,它们直接影响到程序的性能和稳定性。首先,我们来看看Java内存模型。 Java内存模型,通常被称为JVM内存模型,它定义了程序中不同部分如何访问和共享数据。在...
JVM内存模型与垃圾回收是Java性能优化的关键部分。JVM(Java Virtual Machine)内存模型分为多个区域,包括新生代(New Generation)、老年代(Old Generation)和永久代(Permanent Generation)。新生代又细分为...
- 对于final字段,一旦初始化完成,其值在整个生命周期内都不可改变,这确保了线程安全。 - 对于final引用的对象,引用本身不可变,但对象内的状态仍可能改变,需额外注意。 6. **Happens-Before原则** - 这是...
这本书的目标是帮助读者理解C++对象模型背后的细节,包括内存管理、类型系统、对象生命周期、继承、多态等核心概念。 C++对象模型是C++编程的基础,它描述了如何在内存中表示类和对象,以及它们之间的关系。首先,...
构造函数用于初始化对象,而析构函数在对象生命周期结束时被调用,通常用于释放资源。 **5. 继承** 继承允许一个类继承另一个类的特性和行为,从而支持代码重用和层次化设计。 **6. 多态** 多态性使得基类...
再者,C++的对象生命周期管理,包括构造与析构、拷贝与移动语义,也是本书的重要内容。书中将详细解释何时及如何调用构造函数和析构函数,以及如何避免常见的资源管理陷阱,如“悬挂指针”和“双重释放”。理解这些...
栈内存生命周期短,随着方法调用的结束而销毁,速度快但容量有限。 4. 防止内存泄漏: - 内存泄漏是指程序中已分配的内存无法释放回系统,导致资源浪费。在Java中,垃圾收集器通常能有效防止内存泄漏,但过度依赖...
不当的对象生命周期处理可能导致内存溢出或者性能下降。开发者需要从需求出发,识别具有自然边界的业务对象,并将它们映射到内存中的In-memory Domain Model,以此来优化内存使用。缓存作为内存模型的一部分,需要有...
理解这两个过程对理解对象生命周期至关重要,尤其在涉及资源管理(如智能指针)和异常安全时。 4. **继承与多态**:C++的继承机制使得类之间可以形成层次结构,子类可以扩展或修改父类的功能。多态是面向对象编程的...
这些区域各自有不同的功能和生命周期。 1. 堆:这是Java对象的主要存储区域,所有实例对象和数组都在堆中分配内存。堆是线程共享的,因此在多线程环境下需要特别关注内存的同步和可见性问题。 2. 栈:每个线程都有...
2. **构造与析构**:在C++中,构造函数用于初始化新创建的对象,而析构函数则在对象生命周期结束时执行,通常用于释放资源。理解这两者对于有效管理内存至关重要。 3. **内存管理**:C++提供了两种主要的内存区域...
栈内存主要存放方法的局部变量、方法参数、运算中间结果等,生命周期随方法的调用和结束而变化。 3. **本机内存** - 除了JVM内部的堆和栈,Java程序还会与操作系统交互,使用到本机内存,例如Native方法的调用可能...
2. **对象生命周期**:讨论Java对象从创建到销毁的全过程,包括对象的实例化、垃圾收集机制以及内存分配策略,如对象池和分代垃圾回收。 3. **线程与内存**:重点讲解Java中的线程共享和线程局部变量,以及并发编程...
理解对象生命周期和内存分配策略有助于实现这些目标。 3. **垃圾回收** - **垃圾收集算法**:包括标记-清除、复制、标记-整理和分代收集等,每种算法都有其适用场景和优缺点。 - **新生代与老年代**:Java内存...
3. **构造与析构**:构造函数用于初始化新创建的对象,而析构函数在对象生命周期结束时负责清理资源。在C++中,构造函数可以是默认的、带有参数的或拷贝构造函数,它们的执行顺序和作用都有特定的规定。 4. **继承...
这些内存区域共同构成了Java虚拟机(JVM)的内存模型。 总结来说,Java的面向对象特性提供了封装、继承和多态,使代码更易理解和维护。同时,理解内存解析,特别是堆和栈内存的工作原理,对于编写高效、无内存泄漏...