精华内容
下载资源
问答
  • 多到技术中的内存管理
    千次阅读
    2018-09-05 10:49:32

    一、堆与栈

    1. 栈区(stack):由编译器自动分配释放,函数的参数值,局部变量等值。

    2. 堆区(heap):一般由开发人员分配释放,若不释放,则可能会引起内存泄漏。

    NSString* string = @"sdfsdf";//常量string->栈
    
    NSInteger index = 0; //index->栈
    
    NSMutableString* mString = [[NSMutableString alloc] initWithString:@"sdfsdf"];//mString->堆

    二、内存管理

    Objective-C提供了两种种内存管理方式:manual reference counting(MRC,手动引用计数器),automatic reference counting(ARC,自动引用计数)。ARC作为苹果新提供的技术,苹果推荐开发者使用ARC技术来管理内存;

    1、引用计数器

    采用引用计数进行管理

    1. 每个对象都有一个关联的整数,称为引用计数器
    2. 当代码需要使用该对象时,则将对象的引用计数加1
    3. 当代码结束使用该对象时,则将对象的引用计数减1
    4. 当引用计数的值变为0时,此时对象将被释放。

    与之对应的消息发送方法

    1. 当对象被创建(alloc、new或copy等方法)时,其引用计数初始值为1
    2. 给对象发送retain消息,其引用计数加
    3. 给对象发送release消息,其引用计数减1
    4. 当对象引用计数归0时,ObjC給对象发送dealloc消息销毁对象

    下面通过一个简单的例子来说明

    新建RetainCountObject类,重写其创建和销毁的方法

    @implementation RetainCountObject
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            NSLog(@"初始引用计数为 %ld",self.retainCount);
        };
        return self;
    }
    
    - (void)dealloc {
        NSLog(@"对象被释放");
        NSLog(@"release后的引用计数为 %ld", self.retainCount);
    
        [super dealloc];
    }
    
    @end

    在ViewDidLoad方法中创建RetainCountObject对象,给object发送消息    

    RetainCountObject * object = [[RetainCountObject alloc]init];
    
    [object retain];
    
    NSLog(@"object引用计数为 %ld", object.retainCount);
    
    [object release];
    
    NSLog(@"objec引用计数为 %ld", object.retainCount);
    
    [object release];      //将指针置nil,否则变为野指针
    
    object = nil;

    MRC_Project[40145:1469636] 初始引用计数为 1

    MRC_Project[40145:1469636] object引用计数为 2

    MRC_Project[40145:1469636] objec引用计数为 1

    MRC_Project[40145:1469636] 对象被释放

    注意一些特殊的情况:

    NSString引用计数问题

    如果我们尝试查看一个string的引用计数

    NSString * str = @" hello";   NSLog(@"hello guys :%ld", str.retainCount); 

    MRC_Project[40189:1472607] hello :-1

    NSString实际上是一个字符串常量,由栈管理,是没有引用计数的。

    赋值不会拥有某个对象

    NSString* title = object.title;

    这里仅仅是指针赋值操作,并不会增加name的引用计数,需要持有对象必须要发送retain消息。

    Dealloc 析构函数

    由于释放对象是会调用dealloc方法,因此重写dealloc方法来查看对象释放的情况,如果没有调用则会造成内存泄露。在上面的例子中我们通过重写dealloc让对象被释放的时候打印日志来告诉我们已经完成释放。

    在上面例子中,如果我们增加这样一个操作

    //最后release object时
    [object release];     
    
    NSLog(@"release后的引用计数为 %ld", self.retainCount);
    
    [super dealloc];

    MRC_Project[40314:1477373] release后的引用计数为 1

    会发现获取到的引用计数为1,为什么不是0呢?

    这是因为对引用计数为1的对象release时,系统知道该对象将被回收,就不会再对该对象的引用计数进行减1操作,这样可以增加对象回收的效率。

    2、自动释放池

    AutoreleasePool的原理

    自动释放池,系统有一个现成的自动内存管理池,他会随着每一个mainRunloop的结束而释放其中的对像;自动释放池也可以手动创建,他可以让pool中的对象在执行完代码后马上被释放,可以起到优化内存,防止内存溢出的效果(如视频针图片的切换时、创建大量临时对象时等)。

    autorelease:自动释放,使对象在超出指定的生存范围时能够自动并正确地释放 release 即是立即释放)。

    自动释放池的创建

    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc]init];
    
    // do something
    id obj = [[NSMutableArray alloc] init];
    [obj autorelease];
    
    [pool release];
    
    // [pool drain];//GC(垃圾回收机制)环境没影响

     对于所有调用过autorelease实例方法的对象,在pool release时,将调用release释放对象

    @autoreleasepool {
      // do something
    }

    MRC方法放回对象时需要autorelease

    - (NSArray *)getArray{
        NSArray* array = [NSArray array];
        return array;
    
        /*
        NSArray* array = [[[NSArray alloc] init] autorelease];
        return array;
         */
    }

    自动释放池的触发 

    - (void)autoRelease_Test {
        @autoreleasepool {
            TestModel *model = [[TestModel alloc] init];
            [model autorelease];
    
            //model can dongSomething you want
            NSLog(@"自动释放:end");
        }
    }
    
    

    MRC_Project[2678:287011] 自动释放:end

    MRC_Project[2678:287011] TestModel dealloc

    可以看到,当自动释放调用后,model对象才被释放,因此在池子释放之前,model可以正常调用。

    3、IOS内存管理规则

    基本原则

    1. 当你通过new、alloc或copy方法创建一个对象时,它的引用计数为1,当不再使用该对象时,应该向对象发送release或者autorelease消息释放对象。
    2. 当你通过其他方法获得一个对象时,如果对象引用计数为1且被设置为autorelease,则不需要执行任何释放对象的操作;
    3. 如果你打算取得对象所有权,就需要保留对象并在操作完成之后释放,且必须保证retain和release的次数对等。

    ARC的修饰变量

    strong,  weak,  autoreleasing,  unsafe_unretained

    __strong //强引用,持有所指向对象的所有权,无修饰符情况下的默认值。如需强制释放,可置nil

    比如我们常用的定时器:

    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:nil userInfo:nil repeats:NO];

    相当于:

    NSTimer* __strong timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:nil userInfo:nil repeats:NO];

    当不需要使用时,强制销毁定时器:

    [timer invalidate];
    timer = nil;

    __weak //弱引用,不持有所指向对象的所有权,引用指向的对象内存被回收之后,引用本身会置nil,避免野指针。

    比如避免循环引用的弱引用声明:

    __weak typeof(self) weakSelf = self;

    __autoreleasing //自动释放对象的引用,一般用于传递参数

    比如一个读取数据的方法:

    - (void)doSomething:(NSError **)error { ... }

    当你调用时会发现这样的提示:

    [self doSomething: (NSError *__autoreleasing *)]

    这是编译器自动帮我们插入以下代码:

    NSError* __autoreleasing error = nil;
    [self doSomething:&error];
    
    __unsafe_unretained //弱引用,内存被释放后有僵尸对象,会产生野指针,不建议使用

    ARC及MRC的属性 

    @property (nonatomic, assign) int value; //简单的赋值
    
    @property (nonatomic, retain) NSArray* array; //引用计数器加一,指针复制
    
    @property (nonatomic, copy) NSString* string; //生成新的内存区域,内容复制
    
    @property (nonatomic, strong) NSNumber* number;//强引用,当所有指向同一块内存的强指针都赋空时,内存将被释放
    
    @property (nonatomic, weak) NSData* data; //弱引用, 当所有指向同一块内存的强指针都赋空时,弱引用失效,为nil
    
    @property (nonatomic, unsafe_unretained) NSData* data1; //不安全性弱引用,当所有指向同一块内存的强指针都赋空时,该对象将成为野指针,再次调用会导致程序崩溃
    
    - (NSString *)string {
        return _string;
    }
    
    - (void)setString:(NSString*)string {
        _string = string;
        //weak,strong,assign 的set方法
    }
    
    - (void)setString:(NSString*)string {
        if(_string != string) {
            [_string release];
            _string = [string retain];
        }
        //retain 的set方法
    }
    
    - (void)setString:(NSString*)string {
        if(_string != string) {
            [_string release];
            _string = [string copy];
        }
        //copy 的set方法
    }
    
    //释放属性对象
    - (void)dealloc {
        self.string = nil;
        [super dealloc];
    }

    Block内存管理 

    Block本身是像对象一样可以retain,和release。但是,block在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。

    // MRC
    @property(nonatomic, copy) void(^block)(void);

    MRC中 copy会把block从栈上移动到堆上。

    // ARC
    @property(nonatomic, strong) void(^block1)(void);
    
    @property(nonatomic, copy) void(^block2)(void);

    ARC即时由强引用strong将其从栈复制到堆(copy仍旧可用)

    block 的内存管理:

    当程序运行到这里时,stack 空间中有 shared 变量和 captured 变量。

    这里可以看出,__block 变量开始是处于stack上的。

    当程序运行到这里时,stack 空间中有 shared 变量,captured 变量和block1。

    这里可以看出,block 类型的变量开始时也是处在stack上的。

    当程序运行到这里时,stack 空间中有 shared 变量,captured 变量和block1。

    这里值得注意的就是当我们直接修改stack 上的captured变量时,block1中的captured变量仍然是原来的数值10。事实上,从const 我们就可以看出,block1中的captured变量是不能被修改的而且是从stack原有变量的一个const 拷贝。在block1中访问的captured变量是const拷贝的,也就是说block1中captured = 10,而不是原有的stack上的值 20。当然,在block1中,我们也不能修改captured变量。

    Copy Block

    block在一开始是处在stack上的,这是为了考虑到效率的原因,但是,有时候是需要block的生命周期长于一开始的stack,这时,我们就通过copy block 来将block复制到heap。

    当程序执行完 block2 = [block1 copy];时,__block 类型变量shared,被复制到了heap中,很显然,shared变量需要被block和block2共享(当然还有stack也要共享),而block2被移动到heap中,很可能生命周期会长于stack,所以,shared也被复制到了heap中。而block2中的captured 也被复制到了heap中。

    当程序执行完 block3 = [block2 copy];时, 我们看到的是,block2 和block3 其实指向的是同一片内存空间。事实上,block的数据结构中,保存了引用计数,而对于copy到heap中的block 再copy时,行为同普通对象retain一样,会使引用计数+1。那么如果我们对[block retain]会如何呢? 实际上什么都没有发生,至少在现在的runtime版本下。因为retain中,不仅有引用计数+1在,而且retain的返回值,必须同返回调用对象的地址一样,而block的地址是可能变化的(stack or heap),所以,这里retain的行为几乎是被忽略掉的。

    当heap中的block变量先于stack被销毁时,如调用 [block2 release]; [block3 release];,heap中的block2,block3 由于引用计数为0 而被销毁,而 __block 变量shared则还在heap中,因为stack还要使用,block1 也要使用。

    heap中的block变量晚于stack时,显然,stack 被清除,function中也啥都没了

    最后,当block2 和block3 都被release之后,则恢复到最初状态。

     从MRC到ARC的转变 

    项目 -> Build Phases -> Compile Sources 找到要修改的文件

    ARC工程添加MRC文件则输入:-fno-objc-arc

    MRC工程添加ARC文件则输入:-fobjc-arc

    引用方式

    retain:原对象引用计数加1(ARC中使用strong)

    copy:拷贝对象为栈内存时,将栈内存拷贝至堆内存,生成新的内存地址 ,原对象引用计数不变;堆内存拷贝,非可变类型的对象拷贝-NSArray(原对象引用计数加1,与retain类似),可变类型的对象拷贝-NSMutableArray(生成新的非可变类型的对象-NSArray,原对象引用计数不变)

    assign:不涉及引用计数的变化(ARC中对象不使用assign,但数据类型仍然可以使用)

    weak:比assign多了一个功能,对象释放后把指针置为nil,避免了野指针。

    strong:等同于retain + 栈内存拷贝

    在你打开ARC时,你是不能使用retain、release、autorelease 操作的,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了,但是你需要在对象属性上使用weak 、strong和copy,其中strong就相当于retain + 栈内存拷贝,而weak相当于assign,数据类型仍旧使用assign声明。

    差异性

    在MRC中以下代码是行不通的,因为从Array中移除一个对象时,这个对象会发送一条release消息,随后仍使用该对象就可能导致crash。

    id obj = [array objectAtIndex:0];  
    [array removeObjectAtIndex:0];  
    
    NSLog(@"%@", obj);  

    在ARC中这段代码是完全合法的,因为obj变量是一个strong指针,它成为了对象的拥有者,从Array中移除该对象也不会导致对象被释放。

    4、经典内存泄漏

    僵尸对象和野指针

    僵尸对象:内存已经被回收的对象。

    野指针:指向僵尸对象的指针,向野指针发送消息会导致崩溃。

    EXC_BAD_ACCESS

    循环引用

    1)delegate循环引用

    ARC中默认的对象声明都是strong性质的,在两个或两个以上的类相互引用时,会导致循环引用,其中一方需要用weak修饰,才不会造成retainCycle,如:delegate 属性用weak声明;MRC中即用assign修饰 。

    @interface ClassA : NSObject
    @property (nonatomic, weak) id <ClassADelegate> delegate;
    @end
    
    @interface ClassB : NSObject <ClassADelegate>
    @property (nonatomic, strong) ClassA *objectA;
    @end
    
    @implementation ClassB
    - (void)doSomething {
       self.objectA = [[ClassA alloc] init];
       _objectA.delegate = self;
    }
    @end

    2)Block循环引用

    在block中引用block所属的类、实例变量或类的属性也会导致循环引用

    self.block = ^{
       [self doSomething];
     };

    block是会对内部的对象进行一次retain。也就是说,self会被retain一次。当self释放的时候,需要block释放后才会对self进行释放,但是block的释放又需要等self的dealloc中才会释放。如此一来变形成了循环引用,导致内存泄露。

    arc中用__weak修饰self、mrc中用__block修饰,如下代码:

    __weak ViewController* weakSelf = self;//arc
    //__block ViewController* weakSelf = self;//mrc
    
    self.block = ^{
       [weakSelf doSomething];
    };

    3)NSTimer循环引用

    NSTimer在使用期间会强引用target,结束定时器需要手动销毁。

    - (void)createTimer {
       self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeCount) userInfo:nil repeats:YES];
    }
    
    - (void)destroyTimer {
       if (_timer) {
          [_timer invalidate];
          self.timer = nil;
       }
    }

    delegate与block都是引用了对象指针,NSTimer引用的是对象内容,在定时器没有销毁前,使用__weak并不能解决循环引用问题,可以通过NSProxy将NSTimer对target的强引用转为弱引用。

    - (void)createTimer {
       // YYKit-YYWeakProxy
       YYWeakProxy* weakProxy = [[YYWeakProxy alloc] initWithTarget:self];
       self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakProxy selector:@selector(timeCount) userInfo:nil repeats:YES];
    }

    __bridge导致的内存泄漏

    1)__bridge,OC对象转成CF对象,内存的所有权还在ocStr手中,由ARC负责释放。

    NSString *ocStr = [[NSString alloc] initWithFormat:@"hello"];
    CFStringRef cfStr = (__bridge CFStringRef)ocStr;

    2)__bridge,CF对象转成OC对象,内存的所有权还在cfStr手中,所以必须手动释放cfStr,否则会有内存泄漏。 

    CFStringRef cfStr = CFStringCreateWithCString(NULL, "hello", kCFStringEncodingUTF8);
    NSString *ocStr = (__bridge NSString *)cfStr;
    CFRelease(cfStr);

    3)__bridge_transfer,等价于CFBridgingRelease(),CF对象转成OC对象,内存的所有权从cfStr移交到ocStr手上,由ARC负责释放。

    CFStringRef cfStr = CFStringCreateWithCString(NULL, "hello", kCFStringEncodingUTF8);
    NSString *ocStr = (__bridge_transfer NSString *)cfStr; // 内存由cf转移至oc
    
    /* 等价方法
     NSString *ocStr = (NSString *)CFBridgingRelease(cfStr);
    */

    4)__bridge_retained,等价于CFBridgingRetain(),OC对象转成CF对象,内存的所有权从ocStr移交到cfStr手上,所以必须手动释放cfStr,否则会有内存泄漏。 

    NSString *ocStr = [[NSString alloc] initWithFormat:@"hello"];  
    CFStringRef cfStr = (__bridge_retained CFStringRef)ocStr; // 内存由oc转移至cf
    
    /* 等价方法
     CFStringRef cfStr = (CFStringRef)CFBridgingRetain(ocStr);
    */
    
    CFRelease(cfStr);

    循环中大量的临时对象

    示例代码:

    for (int i = 0; i < 10000; i ++) {
      Person * soldier = [[Person alloc]init]; 
      [soldier fight];     
    }

    该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存急剧上涨。解决方法和上文中提到的自动释放池常见问题类似:在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

    for (int i = 0; i < 10000; i ++) {
      @autoreleasepool {
        Person* soldier = [[Person alloc]init];
        [soldier fight];          
      }
    }

    然而有时候autoReleasePool也不是万能的:

    for (int i = 0; i < 2000; i ++) {
      CGSize size = [UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg",i]].size;          
    }

    用imageNamed方法加载图片占用Cache的内存,autoReleasePool也不能释放。

    for (int i = 0; i < 2000; i ++) {
      @autoreleasepool {
        CGSize size = [UIImage imageWithContentsOfFile:filePath].size;              
       }
    }

    系统内存告警

    系统内存瓶颈发出的内存告警-MemoryWarning

    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
    
        self.view = nil;
        self.data = nil;
    }

    5、IOS内存管理相关补充

    1)retain操作对stackblock无效,只能用copy

    2)__strong强引用__weak,防止self可能被释放

    __weak typeof(self) weakSelf = self;
    ReachabilityStatusBlock callback = ^(ReachabilityStatus status) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
    };

    3)GCD的block中无需__weak,GCD的block里面只引用了self ,self并没有引用GCD的block所以不需要用弱引用

    dispatch_async(dispatch_get_main_queue(), ^{
        //_Block_object_assingn(&self); self  retain
        [self doSomething];
        //_Block_object_dispose(&self); self release
     });
    

    4)声明copy和strong,NSMutable在copy后变成NS(新的内存空间),strong只是引用计数加一。

    @property (nonatomic, strong) NSMutableArray* s_arr;
    @property (nonatomic, copy) NSMutableArray* c_arr;

    5)NSMutableDictionary生成的allKeys和allValues对象,不受key:value的删除而删除

    NSMutableDictionary* dic = [NSMutableDictionary dictionaryWithDictionary:@{@"ss":@"bb", @"AA": @"BB"}];
        
    NSArray* ABCallValue = [dic allValues];
    NSArray* ABCallKey = [dic allKeys];
        
    [dic removeObjectForKey:@"AA"];
        
    NSLog(@"[ABCallValue description]:%@\n[ABCallKey description]:%@", [ABCallValue description], [ABCallKey description]);
    
    /*
    [ABCallValue description]:(bb,BB)
    [ABCallKey description]:(ss,AA)
    */

    6)super在block中也会导致循环引用,需要注意调用方式

    - (void)createTabBarItem {
        @weakify(self);
        [tabBarItem setSelectBlock:^(NSInteger index) {
            @strongify(self);
            [self superSetSelectedIndex:index];
        }];
    
        [self.tabBar addSubview:tabBarItem];
    }
    
    
    // 单独出来,在setSelectBlock使用super会导致循环引用
    - (void)superSetSelectedIndex:(NSUInteger)index {
        [super setSelectedIndex:index];
    }
    更多相关内容
  • 内存管理:程序是如何被优雅的装载内存

    千次阅读 多人点赞 2021-11-04 09:26:35
    内存作为计算机中一项比较重要的资源,它的主要作用就是解决CPU和磁盘之间速度的鸿沟,但是由于内存条是需要插入主板上的,因此对于一台计算机...带着这些疑问我们来看看计算系统内存管理那些事。 内存的交换技术..

    内存作为计算机中一项比较重要的资源,它的主要作用就是解决CPU和磁盘之间速度的鸿沟,但是由于内存条是需要插入到主板上的,因此对于一台计算机来说,由于物理限制,它的内存不可能无限大的。我们知道我们写的代码最终是要从磁盘被加载到内存中的,然后再被CPU执行,不知道你有没有想过,为什么一些大型游戏大到10几G,却可以在只有8G内存的电脑上运行?甚至在玩游戏期间,我们还可以聊微信、听音乐...,这么多进程看着同时在运行,它们在内存中是如何被管理的?带着这些疑问我们来看看计算系统内存管理那些事。

    内存的交换技术

    如果我们的内存可以无限大,那么我们担忧的问题就不会存在,但是实际情况是往往我们的机器上会同时运行多个进程,这些进程小到需要几十兆内存,大到可能需要上百兆内存,当许许多多这些进程想要同时加载到内存的时候是不可能的,但是从我们用户的角度来看,似乎这些进程确实都在运行呀,这是怎么回事?

    这就引入要说的交换技术了,从字面的意思来看,我想你应该猜到了,它会把某个内存中的进程交换出去。当我们的进程空闲的时候,其他的进程又需要被运行,然而很不幸,此时没有足够的内存空间了,这时候怎么办呢?似乎刚刚那个空闲的进程有种占着茅坑不拉屎的感觉,于是可以把这个空闲的进程从内存中交换到磁盘上去,这时候就会空出多余的空间来让这个新的进程运行,当这个换出去的空闲进程又需要被运行的时候,那么它就会被再次交换进内存中。通过这种技术,可以让有限的内存空间运行更多的进程,进程之间不停来回交换,看着好像都可以运行。 

     如图所示,一开始进程A被换入内存中,所幸还剩余的内存空间比较多,然后进程B也被换入内存中,但是剩余的空间比较少了,这时候进程C想要被换入到内存中,但是发现空间不够了,这时候会把已经运行一段时间的进程A换到磁盘中去,然后调入进程C。 

    内存碎片

    通过这种交换技术,交替的换入和换出进程可以达到小内存可以运行更多的进程,但是这似乎也产生了一些问题,不知道你发现了没有,在进程C换入进来之后,在进程B和进程C之间有段较小的内存空间,并且进程B之上也有段较小的内存空间,说实话,这些小空间可能永远没法装载对应大小的程序,那么它们就浪费了,在某些情况下,可能会产生更多这种内存碎片。

     如果想要节约内存,那么就得用到内存紧凑的技术了,即把所有的进程都向下移动,这样所有的碎片就会连接在一起变成一段更大的连续内存空间了。 

     但是这个移动的开销基本和当前内存中的活跃进程成正比,据统计,一台16G内存的计算机可以每8ns复制8个字节,它紧凑全部的内存大概需要16s,所以通常不会进行紧凑这个操作,因为它耗费的CPU时间还是比较大的。

    动态增长

    其实上面说的进程装载算是比较理想的了,正常来说,一个进程被创建或者被换入的时候,它占用多大的空间就分配多大的内存,但是如果我们的进程需要的空间是动态增长的,那就麻烦了,比如我们的程序在运行期间的for循环可能会利用到某个临时变量来存放目标数据(例如以下变量a,随着程序的运行是会越来越大的):

    var a []int64
    for i:= 0;i <= 1000000;i++{
      if i%2 == 0{
       a = append(a,i) //a是不断增大的
      }
    }
    

    当需要增长的时候:

    1. 如果进程的邻居是空闲区那还好,可以把该空闲区分配给进程
    2. 如果进程的邻居是另一个进程,那么解决的办法只能把增长的进程移动到一个更大的空闲内存中,但是万一没有更大的内存空间,那么就要触发换出,把一个或者多个进程换出去来提供更多的内存空间,很明显这个开销不小。

    为了解决进程空间动态增长的问题,我们可以提前多给一些空间,比如进程本身需要10M,我们多给2M,这样如果进程发生增长的时候,可以利用这2M空间,当然前提是这2M空间够用,如果不够用还是得触发同样的移动、换出逻辑。

    空闲的内存如何管理

    前面我们说到内存的交换技术,交换技术的目的是腾出空闲内存来,那么我们是如何知道一块内存是被使用了,还是空闲的?因此需要一套机制来区分出空闲内存和已使用内存,一般操作系统对内存管理的方式有两种:位图法链表法

    位图法

    先说位图法,没错,位图法采用比特位的方式来管理我们的内存,每块内存都有位置,我们用一个比特位来表示:

    1. 如果某块内存被使用了,那么比特位为1
    2. 如果某块内存是空闲的,那么比特位为0

    这里的某块内存具体是多大得看操作系统是如何管理的,它可能是一个字节、几个字节甚至几千个字节,但是这些不是重点,重点是我们要知道内存被这样分割了。

     位图法的优点就是清晰明确,某个内存块的状态可以通过位图快速的知道,因为它的时间复杂度是O(1),当然它的缺点也很明显,就是需要占用太多的空间,尤其是管理的内存块越小的时候。更糟糕的是,进程分配的空间不一定是内存块的整数倍,那么最后一个内存块中一定是有浪费的。

    如图,进程A和进程B都占用的最后一个内存块的一部分,那么对于最后一个内存块,它的另一部分一定是浪费的。

    链表法

    相比位图法,链表法对空间的利用更加合理,我相信你应该已经猜到了,链表法简单理解就是把使用的和空闲的内存用链表的方式连接起来,那么对于每个链表的元素节点来说,他应该具备以下特点:

    1. 应该知道每个节点是空闲的还是被使用的
    2. 每个节点都应该知道当前节点的内存的开始地址和结束地址

    针对这些特点,最终内存对应的链表节点大概是这样的:

     p代表这个节点对应的内存空间是被使用的,H代表这个节点对应的内存空间是空闲的,start代表这块内存空间的开始地址,length代表的是这块内存的长度,最后还有指向邻居节点的pre和next指针

    因此对于一个进程来说,它与邻居的组合有四种:

    1. 它的前后节点都不是空闲的
    2. 它的前一个节点是空闲的,它的后一个节点也不是空闲的
    3. 它的前一个节点不是空闲的,它的后一个节点是空闲的
    4. 它的前后节点都是空闲的

    当一个内存节点被换出或者说进程结束后,那么它对应的内存就是空闲的,此时如果它的邻居也是空闲的,就会发生合并,即两块空闲的内存块合并成一个大的空闲内存块。

    ok,通过链表的方式把我们的内存给管理起来了,接下来就是当创建一个进程或者从磁盘换入一个进程的时候,如何从链表中找到一块合适的内存空间?

    首次适应算法

    其实想要找到空闲内存空间最简单的办法就是顺着链表找到第一个满足需要内存大小的节点,如果找到的第一个空闲内存块和我们需要的内存空间是一样大小的,那么就直接利用,但是这太理想了,现实情况大部分可能是找到的第一个目标内存块要比我们的需要的内存空间要大一些,这时候呢,会把这个空闲内存空间分成两块,一块正好使用,一块继续充当空闲内存块。

     一个需要3M内存的进程,会把4M的空间拆分成3M和1M。

    下次适配算法

    和首次适应算法很相似,在找到目标内存块后,会记录下位置,这样下次需要再次查找内存块的时候,会从这个位置开始找,而不用从链表的头节点开始寻找,这个算法存在的问题就是,如果标记的位置之前有合适的内存块,那么就会被跳过。

     一个需要2M内存的进程,在5这个位置找到了合适的空间,下次如果需要这1M的内存会从5这个位置开始,然后会在7这个位置找到合适的空间,但是会跳过1这个位置。

    最佳适配算法

    相比首次适应算法,最佳适配算法的区别就是:不是找到第一个合适的内存块就停止,而是会继续向后找,并且每次都可能要检索到链表的尾部,因为它要找到最合适那个内存块,什么是最合适的内存块呢?如果刚好大小一致,则一定是最合适的,如果没有大小一致的,那么能容得下进程的那个最小的内存块就是最合适的,可以看出最佳适配算法的平均检索时间相对是要慢的,同时可能会造成很多小的碎片。

     假设现在进程需要2M的内存,那么最佳适配算法会在检索到3号位置(3M)后,继续向后检索,最终会选择5号位置的空闲内存块。

    最差适配算法

    我们知道最佳适配算法中最佳的意思是找到一个最贴近真实大小的空闲内存块,但是这会造成很多细小的碎片,这些细小的碎片一般情况下,如果没有进行内存紧凑,那么大概率是浪费的,为了避免这种情况,就出现了这个最差适配算法,这个算法它和最佳适配算法是反着来的,它每次尝试分配最大的可用空闲区,因为这样的话,理论上剩余的空闲区也是比较大的,内存碎片不会那么小,还能得到重复利用。

     一个需要1.5M的进程,在最差适配算法情况下,不会选择3号(2M)内存空闲块,而是会选择更大的5号(3M)内存空闲块。

    快速适配算法

    上面的几种算法都有一个共同的特点:空闲内存块和已使用内存块是共用的一个链表,这会有什么问题呢?正常来说,我要查找一个空闲块,我并不需要检索已经被使用的内存块,所以如果能把已使用的和未使用的分开,然后用两个链表分别维护,那么上面的算法无论哪种,速度都将得到提升,并且节点也不需要P和M来标记状态了。但是分开也有缺点,如果进程终止或者被换出,那么对应的内存块需要从已使用的链表中删掉然后加入到未使用的链表中,这个开销是要稍微大点的。当然对于未使用的链表如果是排序的,那么首次适应算法和最佳适应算法是一样快的。

    快速适配算法就是利用了这个特点,这个算法会为那些常用大小的空闲块维护单独的链表,比如有4K的空闲链表、8K的空闲链表...,如果要分配一个7K的内存空间,那么既可以选择两个4K的,也可以选择一个8K的。

     它的优点很明显,在查找一个指定大小的空闲区会很快速,但是一个进程终止或被换出时,会寻找它的相邻块查看是否可以合并,这个过程相对较慢,如果不合并的话,那么同样也会产生很多的小空闲区,它们可能无法被利用,造成浪费。

    虚拟内存:小内存运行大程序

    可能你看到小内存运行大程序比较诧异,因为上面不是说到了吗?只要把空闲的进程换出去,把需要运行的进程再换进来不就行了吗?内存交换技术似乎解决了,这里需要注意的是,首先内存交换技术在空间不够的情况下需要把进程换出到磁盘上,然后从磁盘上换入新进程,看到磁盘你可能明白了,很慢。其次,你发现没,换入换出的是整个进程,我们知道进程也是由一块一块代码组成的,也就是许许多多的机器指令,对于内存交换技术来说,一个进程下的所有指令要么全部进内存,要么全部不进内存。看到这里你可能觉得这不是正常吗?好的,别急,我们接着往下看。

    后来出现了更牛逼的技术:虚拟内存。它的基本思想就是,每个程序拥有自己的地址空间,尤其注意后面的自己的地址空间,然后这个空间可以被分割成多个块,每一个块我们称之为(page)或者叫页面,对于这些页来说,它们的地址是连续的,同时它们的地址是虚拟的,并不是真正的物理内存地址,那怎么办?程序运行需要读到真正的物理内存地址,别跟我玩虚的,这就需要一套映射机制,然后MMU出现了,MMU全称叫做:Memory Managment Unit,即内存管理单元,正常来说,CPU读某个内存地址数据的时候,会把对应的地址发到内存总线上,但是在虚拟内存的情况下,直接发到内存总线上肯定是找不到对应的内存地址的,这时候CPU会把虚拟地址告诉MMU,让MMU帮我们找到对应的内存地址,没错,MMU就是一个地址转换的中转站。

    程序地址分页的好处是:

    1. 对于程序来说,不需要像内存交换那样把所有的指令都加载到内存中才能运行,可以单独运行某一页的指令
    2. 当进程的某一页不在内存中的时候,CPU会在这个页加载到内存的过程中去执行其他的进程。

    当然虚拟内存会分页,那么对应的物理内存其实也会分页,只不过物理内存对应的单元我们叫页框。页面和页框通常是一样大的。我们来看个例子,假设此时页面和页框的大小都是4K,那么对于64K的虚拟地址空间可以得到64/4=16个虚拟页面,而对于32K的物理地址空间可以得到32/4=8个页框,很明显此时的页框是不够的,总有些虚拟页面找不到对应的页框。

    我们先来看看虚拟地址为20500对应物理地址如何被找到的:

    1. 首先虚拟地址20500对应5号页面(20480-24575)
    2. 5号页面的起始地址20480向后查找20个字节,就是虚拟地址的位置
    3. 5号页面对应3号物理页框
    4. 3号物理页框的起始地址是12288,12288+20=12308,即12308就是我们实际的目标物理地址。

    但是对于虚拟地址而言,图中还有红色的区域,上面我们也说到了,总有些虚拟地址没有对应的页框,也就是这部分虚拟地址是没有对应的物理地址,当程序访问到一个未被映射的虚拟地址(红色区域)的时候,那么就会发生缺页中断,然后操作系统会找到一个最近很少使用的页框把它的内容换到磁盘上去,再把刚刚发生缺页中断的页面从磁盘读到刚刚回收的页框中去,最后修改虚拟地址到页框的映射,然后重启引起中断的指令。

    最后可以发现分页机制使我们的程序更加细腻了,运行的粒度是页而不是整个进程,大大提高了效率。

    页表

    上面说到虚拟内存到物理内存有个映射,这个映射我们知道是MMU做的,但是它是如何实现的?最简单的办法就是需要有一张类似hash表的结构来查看,比如页面1对应的页框是10,那么就记录成hash[1]=10,但是这仅仅是定位到了页框,具体的位置还没定位到,也就是类似偏移量的数据没有。不猜了,我们直接来看看MMU是如何做到的,以一个16位的虚拟地址,并且页面和页框都是4K的情况来说,MMU会把前4位当作是索引,也就是定位到页框的序号,后12位作为偏移量,这里为什么是12位,很巧妙,因为2^12=4K,正好给每个页框里的数据上了个标号。因此我们只需要根据前4位找到对应的页框即可,然后偏移量就是后12位。找页框就是去我们即将要说的页表里去找,页表除了有页面对应的页框后,还有个标志位来表示对应的页面是否有映射到对应的页框,缺页中断就是根据这个标志位来的。

    可以看出页表非常关键,不仅仅要知道页框、以及是否缺页,其实页表还有保护位修改位访问位高速缓存禁止位

    • 保护位:指的是一个页允许什么类型的访问,常见的是用三个比特位分别表示执行
    • 修改位:有时候也称为脏位,由硬件自动设置,当一个页被修改后,也就是和磁盘的数据不一致了,那么这个位就会被标记为1,下次在页框置换的时候,需要把脏页刷回磁盘,如果这个页的标记为0,说明没有被修改,那么不需要刷回磁盘,直接把数据丢弃就行了。
    • 访问位:当一个页面不论是发生读还是发生写,该页面的访问位都会设置成1,表示正在被访问,它的作用就是在发生缺页中断时,根据这个标志位优先在那些没有被访问的页面中选择淘汰其中的一个或者多个页框。
    • 高速缓存禁止位:对于那些映射到设备寄存器而不是常规内存的页面而言,这个特性很重要,加入操作系统正在紧张的循环等待某个IO设备对它刚发出的指令做出响应,保证这个设备读的不是被高速缓存的副本非常重要。

    TLB快表加速访问

    通过页表我们可以很好的实现虚拟地址到物理地址的转换,然而现代计算机至少是32位的虚拟地址,以4K为一页来说,那么对于32位的虚拟地址,它的页表项就有2^20=1048576个,无论是页表本身的大小还是检索速度,这个数字其实算是有点大了。如果是64位虚拟的地址,按照这种方式的话,页表项将大到超乎想象,更何况最重要的是每个进程都会有一个这样的页表

    我们知道如果每次都要在庞大的页表里面检索页框的话,效率一定不是很高。而且计算机的设计者们观察到这样一种现象:大多数程序总是对少量的页进行多次访问,如果能为这些经常被访问的页单独建立一个查询页表,那么速度就会大大提升,这就是快表,快表只会包含少量的页表项,通常不会超过256个,当我们要查找一个虚拟地址的时候。首先会在快表中查找,如果能找到那么就可以直接返回对应的页框,如果找不到才会去页表中查找,然后从快表中淘汰一个表项,用新找到的页替代它。

    总体来说,TLB类似一个体积更小的页表缓存,它存放的都是最近被访问的页,而不是所有的页。

    多级页表

    TLB虽然一定程度上可以解决转换速度的问题,但是没有解决页表本身占用太大空间的问题。其实我们可以想想,大部分程序会使用到所有的页面吗?其实不会。一个进程在内存中的地址空间一般分为程序段、数据段和堆栈段,堆栈段在内存的结构上是从高地址向低地址增长的,其他两个是从低地址向高地址增长的。

    可以发现中间部分是空的,也就是这部分地址是用不到的,那我们完全不需要把中间没有被使用的内存地址也引入页表呀,这就是多级页表的思想。以32位地址为例,后12位是偏移量,前20位可以拆成两个10位,我们暂且叫做顶级页表和二级页表,每10位可以表示2^10=1024个表项,因此它的结构大致如下:

     对于顶级页表来说,中间灰色的部分就是没有被使用的内存空间。顶级页表就像我们身份证号前面几个数字,可以定位到我们是哪个城市或者县的,二级页表就像身份证中间的数字,可以定位到我们是哪个街道或者哪个村的,最后的偏移量就像我们的门牌号和姓名,通过这样的分段可以大大减少空间,我们来看个简单的例子:

    如果我们不拆出顶级页表和二级页表,那么所需要的页表项就是2^20个,如果我们拆分,那么就是1个顶级页表+2^10个二级页表,两者的存储差距明显可以看出拆分后更加节省空间,这就是多级页表的好处。

    当然我们的二级也可以拆成三级、四级甚至更多级,级数越多灵活性越大,但是级数越多,检索越慢,这一点是需要注意的。


    最后

    为了便于大家理解,本文画了20张图,肝了将近7000多字,创作不易,各位的三连就是对作者最大的支持,也是作者最大的创作动力。

    微信搜一搜【假装懂编程】,加入我们,与作者共同学习,共同进步。

     

    往期精彩:

    展开全文
  • C / C++ 内存管理

    千次阅读 多人点赞 2022-05-27 09:35:36
    2、C语言动态内存管理方式 malloc / calloc / realloc / free 3、C++内存管理方式 new / delete 操作内置类型 new / delete 操作自定义类型 4、operator new与operator delete函数(重要点进行讲解) ...

    目录

    1、C / C++内存分布

    2、C语言中动态内存管理方式

            malloc / calloc / realloc / free

    3、C++内存管理方式

            new / delete 操作内置类型

            new / delete 操作自定义类型

    4、operator new与operator delete函数(重要点进行讲解)

            operator new与operator delete函数(重点)

            operator new与operator delete的类专属重载(了解)

    5、new和delete的实现原理

            内置类型

            自定义类型

    6、定位new表达式(placement-new) (了解)

    7、常见面试题

            7.1、malloc/free和new/delete的区别

            7.2、内存泄漏

                    什么是内存泄漏,内存泄漏的危害

                    内存泄漏分类(了解)

                    如何检测内存泄漏(了解)

                    如何避免内存泄漏

            7.3、如何一次在堆上申请4G的内存?


    1、C / C++内存分布

    我们先来看下面的一段代码和相关问题

    int globalVar = 1;
    static int staticGlobalVar = 1;
    void Test()
    {
    	static int staticVar = 1;
    	int localVar = 1;
    	int num1[10] = { 1, 2, 3, 4 };
    	char char2[] = "abcd";
    	const char* pChar3 = "abcd";
    	int* ptr1 = (int*)malloc(sizeof(int) * 4);
    	int* ptr2 = (int*)calloc(4, sizeof(int));
    	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
    	free(ptr1);
    	free(ptr3);
    }

    来看看如下的几个问题:

    1. 选择题:
    选项: A.栈 B.堆 C.数据段 D.代码段
    globalVar在哪里?__C__ staticGlobalVar在哪里?__C__
    staticVar在哪里?__C__ localVar在哪里?__A__
    num1 在哪里?__A__
    char2在哪里?__A__ *char2在哪里?__A__
    pChar3在哪里?__A__ *pChar3在哪里?__D__
    ptr1在哪里?__A__ *ptr1在哪里?__B__
    
    2. 填空题:
    sizeof(num1) = __40__;
    sizeof(char2) = __5__; strlen(char2) = __4__;
    sizeof(pChar3) = __4/8__; strlen(pChar3) = __4__;
    sizeof(ptr1) = __4/8__;

    其实这部分内容在C语言的时候我已经讲解过,这里给出博客链接C/C++内存分配

    这里给出一幅图:

     C/C++程序内存分配的几个区域:

    • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
    • 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
    • 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
    • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

    想看更多这方面练习的小伙伴可以点击这个链接:sizeof 、strlen 内存分配练习


    2、C语言中动态内存管理方式

    malloc / calloc / realloc / free

    这部分内容我在C语言的博客中有详细全面的讲解,可以点击这块链接查看:C语言动态内存管理

    这边给出代码演示:

    void Test()
    {
    	int* p1 = (int*)malloc(sizeof(int));
    	free(p1);
    	int* p2 = (int*)calloc(4, sizeof(int));
    	free(p2);
    	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
    	free(p3);
    }
    • malloc:

    在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址

    • calloc:

    与malloc相似,不过函数calloc() 会将所分配的内存空间中的每一位都初始化为零

    • realloc:

     给一个已经分配了地址的指针重新分配空间,可以做到对动态开辟内存大小的调整。

    • 【面试题】:malloc/calloc/realloc的区别?

    1. 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题.
    2. 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;
    3. 函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void类型.void表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针.
    4. realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址.相反,realloc返回的指针很可能指向一个新的地址.
    5. realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,此时即原地扩;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动,即异地扩

    3、C++内存管理方式

    C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理

    new / delete 操作内置类型

    void Test()
    {
    	// new一个int类型的空间
    	int* ptr4 = new int;
    	// new一个int类型的空间并初始化为10
    	int* ptr5 = new int(10);
    	// new10个int类型的空间
    	int* ptr6 = new int[10];
    	// new10个int类型的空间并初始化
    	int* ptr7 = new int[10]{ 10,9,8,7,6,5 }; //跟数组的初始化很像,大括号有几个,初始化几个,其余为0。不过C++11才支持的语法
    	delete ptr4;
    	delete ptr5;
    	delete[] ptr6;
    	delete[] ptr7;
    }
    

    注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ]

    总结:对于内置类型而言,用malloc和new,除了用法不同,没有什么区别。它们的区别在于自定义类型

    new / delete 操作自定义类型

    先给出结论:

    • 申请空间时:malloc只开空间,new既开空间又调用构造函数初始化。
    • 释放空间时:delete会调用析构函数,free不会

    先看下malloc和free:

    很明显,malloc的对象只是开辟了空间,并没有初始化,free后也只是普通的释放。

    再看下new和delete:

    当我们运行程序时,结果如下:

    很明显,使用new,既可以开辟空间,又调用了构造函数从而完成初始化,而delete时调用了析构函数,以此释放空间。

    在我们先前学习的链表中,C语言为了创建一个节点并将其初始化,需要单独封装一个函数进行初始化,我C++只需要用new即可开空间+初始化:

    struct ListNode
    {
    	struct ListNode* _next;
    	int _val;
        //构造函数
    	ListNode(int val = 0) 
    		:_next(nullptr)
    		,_val(val)
    	{}
    };
    int main()
    {
    	ListNode* n2 = new ListNode(10); //C++的new相当于我之前的BuyListNode函数
    	return 0;
    }

    如若只是单纯的区分malloc和new,那么malloc纯粹只开空间不初始化,而new既开空间又初始化。

    总结:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

    • new和malloc还有一个区别就是在申请内存失败时的处理情况不同。

    malloc如若开辟内存失败,会返回空指针这个我们都晓得的,但是new失败抛异常

    仔细观察下面这段代码:

    int main()
    {
        //malloc失败,返回空指针
    	int* p1 = (int*)malloc(sizeof(int) * 10);
    	assert(p1); //malloc出来的p1需要检查合法性
        //new失败,抛异常
    	int* p2 = new int;
    	//new出来的p2不需要检查合法性
    }

    为了演示malloc和new在开辟内存时失败的场景,这里给出一份测试:

    int main()
    {
    	void* p3 = malloc(1024 * 1024 * 1024); //1G
    	cout << p3 << endl;
    	void* p4 = new char[1024 * 1024 * 1024];
    	cout << p4 << endl;
    }

    换个顺序看看:

    此段测试充分说明了我先开辟1G的大小是没有问题的,但是再开辟1个G的大小就会报错了,为了能够看出malloc和new均报错的场景,我们再定义一个指针占据这1G:

    此段测试更能够清楚的看出mallloc失败会返回空指针,而new失败会抛异常。 对于抛异常,我们理应进行捕获,不过这块内容我后续会讲到,这里先给个演示:


    4、operator new与operator delete函数(重要点进行讲解)

    operator new与operator delete函数(重点)

    newdelete是用户进行动态内存申请和释放的操作符operator newoperator delete是系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

    • 注意:operator new和operator delete不是对new和delete的重载,这是俩库函数。

    源码链接:operator new、operator delete

    operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。operator new本质是封装了malloc。operator delete本质是封装了free。

    • 具体使用operator new和operator delete的操作如下:
    int main()
    {
    	Stack* ps2 = (Stack*)operator new(sizeof(Stack));
    	operator delete(ps2);
    
    	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
        assert(ps1);
    	free(ps1);
    }

    operator new和operator delete的功能和malloc、free一样。也不会去调用构造函数和析构函数,不过还是有区别的,1、operator new不需要检查开辟空间的合法性。2、operator new开辟空间失败就抛异常

    • operator new和operator delete的意义体现在new和delete的底层原理
    Stack* ps3 = new Stack;
    new的底层原理:转换成调用operator new + 构造函数
    delete ps3;
    delete的底层原理:转换成调用operator delete + 析构函数

    new的底层原理就是转换成调用operator new + 构造函数,我们可以通过查看反汇编来验证:

    delete也是转换成调用operator delete + 析构函数,这里画图演示总结:

    operator new与operator delete的类专属重载(了解)

    为了避免有些情况下我们反复的向堆申请释放空间,于是产生池化技术(内存池),直接找内存池申请释放空间,此时效率更高更快。以后会详细讲解到池化技术,这里简要了解。而上述这俩的类专属重载就是在new调用operator new的时候就可以走内存池的机制从而提高效率。


    5、new和delete的实现原理

    内置类型

    如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间而且new在申请空间失败时会抛异常,malloc会返回NULL

    自定义类型

    new的原理

    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的构造

    delete的原理

    1. 在空间上执行析构函数,完成对象中资源的清理工作
    2. 调用operator delete函数释放对象的空间

    new T[N]的原理

    1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
    2. 在申请的空间上执行N次构造函数

    delete[ ]的原理

    1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

    6、定位new表达式(placement-new) (了解)

    定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

    使用格式:

    new (place_address) type或者new (place_address) type(initializer-list)

    place_address必须是一个指针,initializer-list是类型的初始化列表

    使用场景:

    定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化

    class Test
    {
    public:
    	Test(int date = 2)
    		: _data(date)
    	{
    		cout << "Test():" << this << endl;
    	}
    	~Test()
    	{
    		cout << "~Test():" << this << endl;
    	}
    private:
    	int _data;
    };
    int main()
    {
    	// pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    	Test* pt1 = (Test*)malloc(sizeof(Test));
        //new (place_address) type
    	new(pt1)Test; // 注意:如果Test类的构造函数有参数时,此处需要传参
    
        //new(place_address) type(initializer - list)
    	Test* pt2 = (Test*)malloc(sizeof(Test));
    	new(pt2)Test(10);
    }
    

    7、常见面试题

    7.1、malloc/free和new/delete的区别

    共同点:

    • 都是从堆上申请空间,并且需要用户手动释放。

    不同点:

    1. malloc和free是函数,new和delete是操作符
    2. malloc申请的空间不会初始化,new可以初始化
    3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
    4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
    5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常(底层区别)
    6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理(底层区别)

    7.2、内存泄漏

    什么是内存泄漏,内存泄漏的危害

    什么是内存泄漏:

    • 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。(内存泄漏是指针丢了)

    内存泄漏的危害:

    • 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
    void MemoryLeaks()
    {
    	// 1.内存申请了忘记释放
    	int* p1 = (int*)malloc(sizeof(int));
    	int* p2 = new int;
    	// 2.异常安全问题
    	int* p3 = new int[10];
    	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
    	delete[] p3;
    }

    内存泄漏分类(了解)

    C/C++程序中一般我们关心两种方面的内存泄漏:

    堆内存泄漏(Heap leak)

    • 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

    系统资源泄漏

    • 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

    如何检测内存泄漏(了解)

    如何避免内存泄漏

    1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
    2. 采用RAII思想或者智能指针来管理资源。
    3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
    4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

    总结一下:

    • 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

    7.3、如何一次在堆上申请4G的内存?

    // 将程序编译成x64的进程,运行下面的程序试试?
    #include <iostream>
    using namespace std;
    int main()
    {
    	void* p = new char[0xfffffffful];
    	cout << "new:" << p << endl;
    	return 0;
    }

    展开全文
  • 操作系统之内存管理详解

    千次阅读 2022-04-23 13:44:15
    道程序环境下,程序的逻辑地址与内存中的物理地址不可能一致因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 内存空间的扩充 利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存 ...

    1 内存管理概念

    1.1 基本原理和要求

    请添加图片描述

    内存管理:

    操作系统对内存的划分和动态分配

    内存管理的功能有:

    内存空间的分配与回收由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。
    地址转换在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致
    因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。
    内存空间的扩充利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存
    存储保护保证各道作业在各自的存储空间内运行,互不干扰

    程序的装入和链接:

    源程序 -> 可执行程序:

    • 编译。由编译程序将用户源代码编译成若干目标模块。
    • 链接。由链接程序将编译后形成的一组目标模块及所需的库函数链接在一起,形成一个完整的装入模块。
    • 装入。由装入程序将装入模块装入内存运行。

    请添加图片描述

    链接方式:

    • 静态链接

      在程序运行之前,先将各目标模块及它们所需的库涵数链接成一个完整的可执行程序,以后不再拆开。

    • 装入时动态链接

      将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的方式。

    • 运行时动态链接

      对某些目标模块的链接,是在程序执行中需要该目标模块时才进行的。其优点是便于修改和更新,便于实现对目标模块的共享。

    装入方式:

    在这里插入图片描述

    1. 绝对装入。

      在编译时,若知道程序将骁留在内存的某个位置,则编译程序将产生绝对地址的目标代码。绝对装入程序按照装入模块中的地址,将程序和数据装入内存。由于程序中的逻辑地址与实际内存地址完全相同,因此不需对程序和数据的地址进行修改。绝对装入方式只适用于单道程序环境。

      另外,程序中所用的绝对地址,可在编译或汇编时给出,也可由程序员直接赋予。而通常情况下在程序中采用的是符号地址,编译或汇编时再转换为绝对地址。

    2. 可重定位装入。

      在多道程序环境下.多个目标模块的起始地址(简称始址)通常都从0开始,程序中的其他地址都是相对于始址的,此时应采用可重定位装入方式。根据内存的当前情况,将装入模块装入内存的适当位置。装入时对目标程序中指令和数据的修改过程称为重定位,地址变换通常是在装入时一次完成的,所以又称静态重定位。

      静态重定位的特点是,一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存,则不能装入该作业。

      此外,作业一旦进入内存,整个运行期间就不能在内存中移动,也不能再屮请内存空间。

    3. 动态运行时装入,也称动态重定位。

      程序在内存中若发生移动,则需要采用动态的装入方式。装入程序把装入模块装入内存后,并不立即把装入模块中的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时才进行。因此,装入内存后的所有地址均为相对地址。这种方式需要一个重定位寄存器的支持。

      动态重定位的特点如下:可以将程序分配到不连续的存储区中:在程序运行之前可以只装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存;便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间。

    在这里插入图片描述

    逻辑地址空间和物理地址空间:

    编译后,每个目标模块都从0号单元开始编址,这称为该目标模块的相对地址(或逻辑地址)。当链接程序将各个模块链接成一个完整的可执行目标程序时,链接程序顺序依次按各个模块的相对地址构成统一的从0号单元开始编址的逻辑地垃空问。

    用户程序和程序员只需知道逻辑地址,而内存管理的具体机制则是完全透明的,只有系统编程人员才会涉及内存管理的具体机制。不同进程可以有相同的逻辑地址,因为这些相同的逻辑地址可以映射到主存的不同位置。

    物理地址空间是指内存中物理单元的集合,它是地址转换的最终地址,进程在运行时执行指令和访问数据,最后都要通过物理地址从主存中存取。

    当装入程序将可执行代码装入内存时,必须通过地址转换将逻辑地址转换成物理地址,这个过程称为地址重定位;

    内存保护:

    两种方法:

    1. 在CPU中设置一对上、下限寄存器,存放进程的上、下限地址。进程的指令要访问某个地址时,CPU检查是否越界。

    在这里插入图片描述

    1. 采用重定位寄存器(又称基址寄存器)和界地址寄存器(又称限长寄存器)进行越界检查。重定位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址

    在这里插入图片描述

    1.2 覆盖与交换

    在这里插入图片描述

    覆盖技术:

    引入了覆盖技术,用来解决 “程序大小超过物理内存总和” 的问题‘

    覆盖技术的思想:将程序分为多个段(多个模块)。常用的段常驻内存,不常用的段在需要时调入内存。内存中分为一个“固定区”和若干个“覆盖区”。需要常驻内存的段放在“固定区”中,调入后就不再调出(除非运行结束)不常用的段放在“覆盖区”,需要用到时调入内存,用不到时调出内存必须由程序员声明覆盖结构,操作系统完成自动覆盖。

    在这里插入图片描述

    缺点:对用户不透明,增加了用户编程负担。覆盖技术只用于早期的操作系统中,现在已成为历史。

    交换技术:

    交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)

    中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新调入内存。

    暂时换出外存等待的进程状态为挂起状态(挂起态,suspend)

    挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态
    在这里插入图片描述

    何时换:

    1. 具有对换功能的操作系统中,通常把磁盘空间分为文件区对换区两部分。

      文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式;

      对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入换出速度,因此通常对换区采用连续分配方式(学过文件管理章节后即可理解)。

      总之,对换区的I/O速度比文件区的更快。

    2. 交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。

      例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程; 如果缺页率明显下降,就可以暂停换出。

    3. 可优先换出阻塞进程;可换出优先级低的进程;为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间…

    (注意:PCB会常驻内存,不会被换出外存)

    1.3 连续分配管理方式

    在这里插入图片描述

    单一连续分配:

    在单一连续分配方式中,内存被分为系统区和用户区。系统区通常位于内存的低地址部分,用于存放操作系统相关数据;用户区用于存放用户进程相关数据。

    内存中只能有一道用户程序,用户程序独占整个用户区空间。

    优点:实现简单;无外部碎片;可以采用覆盖技术扩充内存;不一定需要采取内存保护(eg:早期的PC操作系统MS-DOS)。

    缺点:只能用于单用户、单任务的操作系统中;有内部碎片;存储器利用率极低。

    固定分区分配:

    整个用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业,

    划分方式

    1. 分区大小相等:缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场合

      (比如:钢铁厂有n个相同的炼钢炉,就可把内存分为n个大小相等的区域存放n个炼钢炉控制程序)

    2. 分区大小不等:增加了灵活性,可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分

      (比如:划分多个小分区、适量中等分区、少量大分区)

    操作系统需要建立一个数据结构——分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态(是否已分配)。

    在这里插入图片描述

    优点:实现简单,无外部碎片。

    缺点

    1. 当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;
    2. 会产生内部碎片,内存利用率低。

    动态分区分配:

    动态分区分配又称为可变分区分配。

    这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。

    因此系统分区的大小和数目是可变的。

    (eg:假设某计算机内存大小为64MB,系统区8MB,用户区共56MB…)

    1. 系统要用什么样的数据结构记录内存的使用情况?

    在这里插入图片描述

    1. 当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?

      算法算法思想分区排列顺序优点缺点
      首次适应从头到尾找适合的分区空闲分区以地址递增次序排列综合看性能最好。算法开销小,回收分区后一般不需要对空闲分区队列重新排序
      最佳适应优先使用更小的分区,以保留更多大分区空闲分区以容量递增次序排列会有更多的大分区被保留下来,更能满足大进程需求会产生很多太小的、难以利用的碎片;算法开销大,回收分区后可能需要对空闲分区队列重新排序
      最坏适应优先使用更大的分区,以防止产生太小的不可用的碎片空闲分区以容量递减次序排列可以减少难以利用的小碎片大分区容易被用完,不利于大进程; 算法开销大(原因同上)
      邻近适应由首次适应演变而来,每次从上次查找结束位置开始查找空闲分区以地址递增次序排列(可排列成循环链表)不用每次都从低地址的小分区开始检索。算法开销小(原因同首次适应算法)会使高地址的大分区也被用完
    2. 如何进行分区的分配与回收操作?假设系统采用的数据结构是“空闲分区表”…如何分配?

      有相邻就合并分区表,无就创建一个表项。

    1.4 非连续分配方式

    请添加图片描述

    分页存储:

    位置分区名称如何分编号
    内存页框将内存空间分为一个个大小相等的分区页框号,从0开始
    进程的逻辑地址空间“页”或“页面”与页框大小相等的一个个部分页号,从0开始

    (页框=页帧=内存块=物理块=物理页面)。(页框号=页帧号=内存块号=物理块号=物理页号)

    操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入一个页框中。也就是说,进程的页面与内存的页框有一一对应的关系。各个页面不必连续存放,可以放到不相邻的各个页框中。

    (注:进程的最后一个页面可能没有一个页框那么大。也就是说,分页存储有可能产生内部碎片,因此页框不能太大,否则可能产生过大的内部碎片造成浪费)

    逻辑地址结构:

    在这里插入图片描述

    页表:

    为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。

    注:页表通常存在PCB(进程控制块)中

    1. 一个进程对应一张页表
    2. 进程的每个页面对应一个页表项
    3. 每个页表项由“页号”和“块号”组成
    4. 页表记录进程页面和实际存放的内存块之间的映射关系
    5. 每个页表项的长度是相同的

    在这里插入图片描述

    Eg:

    假设某系统物理内存大小为4GB,页面大小为4KB,则每个页表项至少应该为多少字节?

    内存块大小 = 页面大小 = 4KB = 212B

    4GB 的内存总共会被分为 232 / 212 = 220 个内存块

    内存块号的范围应该是 0 ~ (220-1)

    内存块号至少要用 20bit 来表示

    至少要用 3B 来表示块号(3*8=24bit)

    由于页号是隐含的,因此每个页表项占3B,存储整个页表至少需要3*(n+1)B

    注意:页表记录的只是内存块号,而不是内存块的起始地址!J 号内存块的起始地址 = J * 内存块大小

    如何地址转换:

    在这里插入图片描述

    在系统中通常设置一个页表寄存器 (PTR),存放页表在内存的起始地址 F 和页表长度 M.

    进程未执行时,页表的始址和长度存放在进程控制块中,当进程执行时,才将页表始址和长度存入页表寄存器。

    设页面大小为L,逻辑地址A到物理地址E的变换过程如下(逻辑地址、页号、每页的长度都是十进制数):

    1. 计算页号P(P = A/L)和页内偏移量W(W = A%L)。
    2. 比较页号P和页表长度M,若P>=M,则产生越界中断,否则继续执行。
    3. 页表中页号P对应的页表项地址=页表始地址F + 页号P * 页表项长度,取出该页表項内容b,即为物理块号。

    要注意区分页表长度和页表项长度。页表长度的值是指一共有多少页,页表项长度是指页地址占多大的存储空间。

    计算E = b * L + W,得到的物理地址E去访问内存。

    以上整个地址变换过程均是由硬件自动完成的。

    例如

    若页面大小为 1KB,页号 2 对应的物理块为 b = 8,计算逻辑地址 A = 2500 的物理地址 E 的过程如下:

    P = 2500 / 1K = 2,

    W = 2500 % 1K = 452,

    查找得到页号 2 对应的物理块的块号为 8,E = 8 × 1024 + 452 = 8644。

    1.5 快表

    快表,又称联想寄存器(TLB,translation lookaside buffer),是一种访问速度比内存快很多的高速缓存(TLB不是内存!),用来存放最近访问的页表项的副本,可以加速地址变换的速度。一般集成在CPU内部.

    与此对应,内存中的页表常称为慢表。

    在这里插入图片描述

    如何转换:

    1. CPU给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较。

    2. 如果找到匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量 拼接形成物理地址,最后,访问该物理地址对应的内存单元。

      因此,若快表命中,则访问某个逻辑地址仅需一次访存即可。

    3. 如果没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。

      因此,若快表未命中,则访问某个逻辑地址需要两次访存

    (注意:在找到页表项后,应同时将其存入快表,以便后面可能的再次访问。

    但若快表已满,则必须按照一定的算法对旧的页表项进行替换)

    地址变换过程访问一个逻辑地址的访存次数
    基本地址变换机构①算页号、页内偏移量
    ②检查页号合法性
    ③查页表,找到页面存放的内存块号
    ④根据内存块号与页内偏移量得到物理地址
    ⑤访问目标内存单元
    两次访存具有
    快表的地址变换机构①算页号、页内偏移量
    ②检查页号合法性
    ③查快表。若命中,即可知道页面存放的内存块号,可直接进行⑤若未命中则进行④
    ④查页表,找到页面存放的内存块号,并且将页表项复制到快表中
    ⑤根据内存块号与页内偏移量得到物理地址
    ⑥访问目标内存单元
    快表命中,只需一次访存
    快表未命中,需要两次访存

    TLB 和普通 Cache 的区别——TLB 中只有页表项的副本,而普通 Cache 中可能会有其他各种数据的副本

    时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)

    空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的)

    1.6 二级页表

    在这里插入图片描述

    把页表再分页并离散存储,然后再建立一张页表记录页表各个部分的存放位置,称为页目录表,或称外层页表,或称顶层页表结构:

    在这里插入图片描述

    如何转换:

    1. 按照地址结构将逻辑地址拆分成三部分
    2. 从PCB中读出页目录表始址,再根据一级页号查页目录表,找到下一级页表在内存中的存放位置
    3. 根据二级页号查二级页表,找到最终想访问的内存块号
    4. 结合页内偏移量得到物理地址

    注意几个细节:

    1. 若分为两级页表后,页表依然很长,则可以采用更多级页表,一般来说各级页表的大小不能超过一个页面

      :某系统按字节编址,采用 40 位逻辑地址,页面大小为 4KB,页表项大小为 4B,假设采用纯页式存储,则要采用()级页表,页内偏移量为()位?

      页面大小= 4KB = 212B,按字节编址,因此页内偏移量为12位

      页号 = 40-12 = 28 位, 页表项大小= 4B 页面大小= 212B,则每个页面可存放212/ 4= 210 个页表项

      因此各级页表最多包含 210 个页表项,需要10位二进制位才能映射到 210 个页表项,因此每一级的页表对应页号应为10位。总共28位的页号至少要分为三级

    在这里插入图片描述

    1. 两级页表的访存次数分析(假设没有快表机构)

      第一次访存:访问内存中的页目录表

      第二次访存:访问内存中的二级页表

      第三次访存:访问目标内存单元

    1.7 基本分段存储管理方式

    在这里插入图片描述

    分段:

    进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址

    内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。

    由于是按逻辑功能模块划分,用户编程更方便,程序的可读性更高

    在这里插入图片描述

    逻辑地址结构:

    在这里插入图片描述

    段表:

    在这里插入图片描述

    地址变换:

    在这里插入图片描述

    对比分页:

    1. 页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。

    2. 段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。

    3. 分段对用户是可见的,用户编程时需要显式地给出段名。

    4. 页的大小固定且由系统决定。

    5. 段的长度却不固定,决定于用户编写的程序。

    6. 分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。

    7. 分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。

    分段比分页更容易实现信息的共享和保护。

    1. 不能被修改的代码称为纯代码或可重入代码(不属于临界资源),这样的代码是可以共享的。
    2. 可修改的代码是不能共享的(比如,有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)

    1.8 段页式管理

    在这里插入图片描述

    分页、分段的优缺点分析:

    优点缺点
    分页管理内存空间利用率高,不会产生外部碎片, 只会有少量的页内碎片不方便按照逻辑模块实现信息的共享和保护
    分段管理很方便按照逻辑模块实现信息的共享和保护如果段长过大,为其分配很大的连续空间会很不方便。 另外,段式管理会产生外部碎片

    段页式管理:

    将进程按逻辑模块分段,再将各段分页(如每个页面4KB)

    再将内存空间分为大小相同的内存块/页框/页帧/物理块

    进程前将各页面分别装入各内存块中
    在这里插入图片描述

    逻辑结构:

    在这里插入图片描述

    如何转换:

    在这里插入图片描述

    1.9 总结

    1. 为什么要进行内存管理?

      在单道批处理系统阶段,一个系统在一个时间段内只执行一个程序,内存的分配极其简单,即仅分配给当前运行的进程。引入多道程序的并发执行后,进程之间共享的不仅仅是处理机,还有主存储器。然而,共享主存会形成一些特殊的挑战。若不对内存进行管理,则容易导致内存数据的混乱,以至于限制进程的并发执行。因此,为了更好地支持多道程序并发执行,必须进行内存管理。

    2. 页式管理中每个页表项大小的下限如何决定?

      页表项的作用是找到该页在内存中的位置。以32位逻辑地址空间、字节编址单位、一页 4KB 为例,地址空间内共含有232B/4KB=1M页,20位才能保证表示范围能容纳所有页面,又因为以字节作为编址单位,即页表项的大小>=[20/8]=3B.所以在这个条件下,为了保证页表项能够指向所有页面,页表項的大小应该大于3B:当然,也可选择更大的页表项大小,让一个页面能够正好容下整数个页表项,以方便存储(例如取成4B,一页正好可以装下1K个页表项),或增加一些其他信息。

    3. 多级页表解决了什么问题又会带来什么问题?

      多级页表解决了当逻辑地址空间过大时,页表的长度会大大增加的问题。而采用多级页表时,一次访盘需要多次访问内存甚至磁盘,会大大增加一次访存的时间。

    展开全文
  • 《Linux内存管理:转换后备缓冲区(TLB)原理》 《内存管理:Linux Memory Management:MMU、段、分页、PAE、Cache、TLB》 《Memory Management Concepts overview(内存管理基本概念)》 《NUMA - Non Uniform ...
  • 分页内存管理

    千次阅读 2019-03-18 16:31:41
    文章目录一、分页内存管理详解1、分页内存管理的核心思想2、分页内存管理能解决什么问题?3、虚拟地址的构成与地址翻译4、页表5、分页内存管理的优缺点二、分页内存管理例子解析三、缺页中断和页面置换的目标1、缺页...
  • Android 内存管理机制

    千次阅读 多人点赞 2018-09-03 14:41:45
    内存管理基础:从整个计算机领域简述主要的内存管理技术。 Linux的内存管理机制:Android毕竟是基于Linux内核实现的操作系统,因此有必要了解一下Linux的内存管理机制。 Android的内存管理相关知识:Android又...
  • 内存管理的功能 内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 地址转换:在道程序环境下,程序的逻辑地址与内存的物理地址不可能一致,因此...
  • 操作系统(内存管理)

    热门讨论 2009-09-20 12:55:25
    文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半...
  • Linux内存管理机制(最透彻的一篇)

    万次阅读 多人点赞 2018-08-05 14:10:09
    摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入内核讨论系统物理内存管理和内核内存的使用方法。力求从外内、水到渠成地引导网友分析Linux的内存管理与使用。在本章最后,...
  • JVM内存管理及GC机制

    万次阅读 2021-12-06 21:42:40
    Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。...
  • STM32内存管理

    千次阅读 2019-03-15 15:28:43
    内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。 内存管理的实现方法有很种,他们其实最终都是要实现 2 个函数: ...
  • 深入理解操作系统[8]:内存管理

    千次阅读 2018-11-16 17:54:34
    文章目录1 存储系统的层次结构2....主存:CPU的控制部件只能从主存储器取得指令和数据,数据能够从主存储器读取并将它们装入寄存器,或者从寄存器存入主存储器。CPU 与外围设备交换的信息一般也...
  • 8.3 分页内存管理 1、由连续分配方式发展为分页存储管理方式的主要动力是()。A A.提高内存利用率 B.提高系统吞吐量 C.满足用户的需要 D.更好的满足道程序运行的需要 2、在页式存储管理中,当CPU形成一个有效地址...
  • 内存管理的概念及作用

    千次阅读 2018-05-25 13:34:26
    有效的内存管理道程序设计非常重要,不仅方便用户使用存储器、提高内存利用率,还可以通过虚拟技术从逻辑上扩充存储器。内存管理的功能:1.内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使...
  • 内存管理:页式虚拟内存管理

    千次阅读 2019-12-17 21:00:38
    页式存储管理——虚拟内存——缺页中断,页面替换算法 开章明意: 创建一个进程(创建进程是在磁盘),进程以字节为单位编号,然后再进程分为许多页(每页4KB),内存中有对应的页框(设定同页)。通过页表(记录...
  • 内存管理的基本思想与算法

    千次阅读 2018-05-30 16:46:59
    介绍操作系统是如何来管理内存资源。 层次化存储体结构 计算机的存储体系 寄存器(register) 在CPU内部,非常快速,昂贵 高速缓存(cache) 非常快速,昂贵,容量小,易失性 主存(RAM) 中等速度,...
  • Linux内存描述之高端内存--Linux内存管理(五)

    万次阅读 多人点赞 2016-08-31 14:33:52
    日期 内核版本 架构 作者 GitHub CSDN 2016-08-31 Linux-4.7 X86 & arm ... Linux内存管理 http://blog.csdn.net/vanbreaker/article/details/75799411 前景回顾前面我们讲服务器体系(SMP, NUMA, M
  • MMU内存管理单元详解

    千次阅读 2020-04-28 23:15:07
    在传统的批处理系统如 DOS 系统,应用程序与操作系统在内存中的布局大致如下图: 应用程序直接访问物理内存,操作系统占用一部分内存区。 操作系统的职责是“加载”应用程序,“运行”或“卸载”应用程序。...
  • 在操作系统内存管理的目的是什么呢? 其中最主要的就是提高内存的利用率,所谓的提高内存利用率,就是尽可能的在内存中多存储进程,这就涉及为进程分配内存空间了。分配的方式主要是有两种——连续分配和离散...
  • Windows内存管理

    千次阅读 2020-10-17 10:17:53
    1.虚拟内存: 最适合用来管理大型对象或者结构数组 ...2.内存映射文件: 最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行个进程之间共享数据 3.内存堆栈: 最适合用来管理大量的小对象 ...
  • 重学C语言内存管理

    千次阅读 多人点赞 2021-05-02 16:02:30
    内存管理简介2. 内存分类3. 栈区(stack)4. 堆区(heap)5. 全局区(静态区)6. 常量区7. malloc、calloc、realloc函数8. strcpy、memcpy、memmove函数9. 实现动态数组10. 内存越界11. 内存泄露(Memory Leak)12. ...
  • Redis内存管理和优化

    千人学习 2016-11-21 14:05:28
    Redis是一个高性能的开源NOSQL内存数据库。本次分享主要介绍Redis内存管理机制,Redis内存使用情况分析定位,阿里云Redis相关优化,以及相关的佳实践。
  • 操作系统——分页式内存管理

    万次阅读 多人点赞 2017-08-27 18:50:41
    答:道程序并发执行,共享的不仅仅只有处理器,还有内存,并发执行不过不进行内存管理,必将会导致内存数据的混乱,以至于限制了进程的并发执行。扩充内存的两种方式?答:覆盖和交换技术是扩充内存的两种方法1...
  • STL学习——STL内存管理技术

    万次阅读 多人点赞 2016-08-18 23:08:53
    当我们new一个对象时,实际做了两件事情:(1)使用malloc申请了一块内存。(2)执行构造函数。
  • UE4内存管理

    千次阅读 2017-03-09 15:03:45
    大家好! 呃,我有一段时间没有更新博客了;我和Vincent以及同事程序员Marc-...现在我们的游戏有了人物的面部动画以及强大的配音,将其提高了一个崭新的层次。Polygon上还有一篇关于FATED的文章,一切开始变
  • 包括:内存管理单元MMU的作用,虚拟内存与物理内存之间的映射方式,页表的概念,高速缓存(Cache)的作用,物理内存与高速缓存之间的映射关系等。当然,想要深入了解,本文并不适合,本文只是从原理上,讲述以上几者...
  • 1、了解虚拟存储技术的特点,掌握虚拟存储请求页式存储管理中几种基本页面置换算法的基本思想和实现过程,并比较它们的效率。 2、了解程序设计技术内存泄露的原因 二、实验内容 1、模拟实现请求页式存储管理的几种...
  • 1、在页式存储管理中,为了实现主存的空间分配,应设置()。D A.段表 B.空闲区表 C.位示图 D.页表 2、每次分配时总是从低地址高地址顺序查找空闲区表,找到第一个能满足作业长度要求的空闲区,此种分配算法称为...
  • Android内存管理机制

    千次阅读 2017-11-26 02:27:10
    1、基于Linux内存管理 ... Android系统是基于Linux 2.6内核开发的开源操作系统,而linux系统的内存管理有其...不过Android系统对Linux的内存管理机制进行了优化,Linux系统会在进程活动停止后就结束该进程,而An

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 696,506
精华内容 278,602
关键字:

多到技术中的内存管理