常考设计题

一:如何定义一个只能在堆上生成对象的类?

1.类的对象建立的方式

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

  • 静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

  • 动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

2.只能在堆上

类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。

将对象建立在栈上时,编译器会管理该对象的整个生命周期。

编译器为类对象分配空间时,会先检查类的析构函数的访问性。如果编译器无法调用析构函数则不会再栈上分配空间

所以思路来了:析构函数设为私有,然后用 new 调用构造函数在堆上分配,就可以达到禁止用户在栈上创建对象的目的

class  A  
{  
public :  
    A(){}  
    void  destory(){ delete   this ;}  
private :  
    ~A(){}  
};  
  • 上述方法的一个缺点就是,无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。
    在这里插入图片描述
  • 另一个缺点就是。类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下,类似于单例模式:
class A
{
protected:
    A() {}
    ~A() {}

public:
    static A *create() //静态函数
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};
A * p1 = A::create();
...
p1->destory();

3.只能在栈上

只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。代码如下:

class A
{
private:
   void *operator new(size_t t) {}    // 注意函数的第一个参数和返回值都是固定的
   void operator delete(void *ptr) {} // 重载了new就需要重载delete
public:
   A() {}
   ~A() {}
};

摘自:C++定义一个只能在堆上(栈上)生成对象的类

二:内存池的设计?细节,怎么获取,怎么使用,怎么归还?

(1)为什么需要设计内存池?

通常我们用new或malloc来分配内存的话,由于申请的大小不确定,所以当频繁的使用时会造成内存碎片和效率的降低

内存池就是:先申请分配一个大的内存块留作备用。当真正需要使用内存的时候,就从内存池中分配一块内存使用,当这块使用完了之后再还给内存池。若是内存块不够了就向内存再申请一块大的内存块。

可以看出这样做有两个好处:

  1. 由于向内存申请的内存块都是比较大的,所以能够降低外碎片问题。
  2. 一次性向内存申请一块大的内存慢慢使用,避免了频繁的向内存请求内存操作,提高内存分配的效率。

(2)设计开始前需要考虑什么?

  • 如何分配定长记录?不定长记录?
  • 分配,回收策略是什么?
  • 如何解决多线程的问题?如何处理缓存竞争的问题?
  • 申请的内存如何管理?对于大,小对象的申请处理方式?
  • 万一最近分配的对象不连续,那么如何处理造成的大量缺页的问题?
  • debug,需要加入埋点调试功能吗?
  • 内存如何对齐?
  • 等等。。。

(3)看看 tcmalloc 是怎么做的?

对于大内存的分配策略(其实就是伙伴系统)

如果分配的对象大于一个 Page,我们就需要用多个 Page 来分配了:
在这里插入图片描述
span 中的 start 指向 page 的序列号。length 表示拥有 page 的数量。
在这里插入图片描述
所以分配对象时,大的对象直接分配 Span,小的对象从 Span 中分配。

这里牵扯到的一个问题就是如何合并span。合并的条件是(1)大小相同 (2)地址连续

大小相同就是在同一个链表上找,但是地址连续如何确定呐?我们经过上面的讨论知道了span中存有page的序列号,那么我去看每一个span的序列号是否连续就行了啊!!

其实所谓的合并就是我们已经知道了span中表示的page序列号是连续的。然后我们需要做的就是将这两个页面合并成一个。
在这里插入图片描述
最简单的一种方式,用一个数组记录每个Page所属的 Span,而数组索引就是 Page ID。这种方式虽然简洁明了,但是在 Page 比较少的时候会有很大的空间浪费。

为此,我们可以使用 RadixTree 这种数据结构,用较少的空间开销,和不错的速度来完成这件事:
在这里插入图片描述
把 RadixTree 理解成压缩过的前缀树(trie),所谓压缩,就是在一条路径上的节点都只有一个子节点,就把这条路径合并到父节点去。这个结构被称之为PageMap(得到page->span的映射)

所以span的合并就是根据这个结构去运行的。(我没有看懂)

到这里,我们已经实现了 PageHeap,对所有 Page进行管理

对于小内存的分配策略(< 1 page的内存分配)

在这里插入图片描述
按照我们之前设计的,每种规格的对象,都从不同的 Span 进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个CentralCache 内存,我们用链表把所有 Span 组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span。

  • 如何分配定长的记录?(freelist

tcmalloc 对于这种定长记录就是使用的这种结构。将相同大小的定长块组织为一个链表。

当然如果内存块大小不一样就组织为多条链表。

FreeList
自由表(英语:free list)[1]是一种用来实现特定动态内存分配方案的数据结构,也称自由列表。自由表的核心原理是将若干未分配的内存块用链表连接起来,将未分配区域的第一个字作为指向下一个未分配区域的指针使用。自由表非常适合用来实现内存池,因为内存池中对象的大小都是相同的。

用自由表实现内存的分配和回收非常简单:回收内存时只需将内存块链入自由表;分配时也只需从自由表的一端取下即可直接使用。如果内存块的大小不一,则分配前还需要在自由表中搜索足够大的内存块,可能有一定的额外消耗。(这里就有什么首次适配法,最佳适配法,最差适配法等等)

因为自由表使用了链表结构,所以也继承了它的劣势:访问局部性低下,难以利用缓存。

  • 如何分配不定长的记录?

在这里插入图片描述
我们把所有的变长记录进行“取整”,例如分配7字节,就分配8字节,31字节分配32字节,得到多种规格的定长记录。这里带来了内部内存碎片的问题,即分配出去的空间不会被完全利用,有一定浪费。为了减少内部碎片,分配规则按照 8, 16, 32, 48, 64, 80这样子来。注意到,这里并不是简单地使用2的幂级数,因为按照2的幂级数,内存碎片会相当严重,分配65字节,实际会分配128字节,接近50%的内存碎片(这是因为它要求地址连续)。而按照这里的分配规格,只会分配80字节,一定程度上减轻了问题。

看起来基本满足功能,但是这里有一个严重的问题,在多线程的场景下,所有线程都从CentralCache 分配的话,竞争可能相当激烈。

ThreadCache

在这里插入图片描述
每个线程都一个线程局部的 ThreadCache,按照不同的规格,维护了对象的链表;

总结:

在tcmalloc内存管理的体系之中,一共有三个层次:
ThreadCache、CentralCache、PageHeap
PageHeap == 伙伴 CentralCache == kmem_cache(高速缓存)

分配内存和释放内存的时候都是按从前到后的顺序,在各个层次中去进行尝试。基本思想是:前面的层次分配内存失败,则从下一层分配一批补充上来;前面的层次释放了过多的内存,则回收一批到下一层次。

这几个层次从前到后,主要有这么几方面的变化:

线程私有性:ThreadCache,顾名思义,是每个线程一份的。理想情况下,每个线程的内存需求都在自己的ThreadCache里面完成,线程之间不需要竞争,非常高效。而CentralCache和PageHeap则是全局的;
内存分配粒度:在tcmalloc里面,有两种粒度的内存,object和span。span是连续page的内存,而object则是由span切成的小块。object的尺寸被预设了一些规格(class),比如16字节、32字节、等等,同一个span切出来的object都是相同的规格。object不大于256K,超大的内存将直接分配span来使用。ThreadCache和CentralCache都是管理object,而PageHeap管理的是span。

摘自:图解 TCMalloc
tcmalloc浅析

(4)C++ 分配器中内存池的设计?

STL之空间配置器:

六:内存泄漏的姿势

什么是内存泄漏?

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。泄漏的区域一般就是堆存储区。主要原因 new/delete 。

常见情景:

  • 循环引用
  • 多态基类没有声明virtual 析构函数
  • 在构造函数抛出异常的情况下,则泄露,因为构造函数异常时,析构函数不会被调用。
  • 系统资源的泄漏。RAII 机制
  • 不匹配使用new[] 和 delete[]。在释放对象数组时在delete中没有使用方括号
  • 局部分配的内存,未在调用者函数体内释放
  • C字符串的截断操作。但是没有进行对应的内存释放。
char* getMemory()
{
    char *p = (char *)malloc(30); 
    return p;
}
 
int main()
{
    char *p = getMemory();
    
    return 0;
}

如何检测:

静态检测
  • 眼睛看
  • 使用静态代码分析工具。比如 splint, PC-LINT, BEAM 等。BEAM 支持的平台
  • 调用会埋点的new和delete或者自定义new与delete

WIN下:将 malloc 和 free 函数映射到它们的调试版本,即 _malloc_dbg 和 _free_dbg,这两个函数将跟踪内存分配和释放。 此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的 malloc 和 free 函数。”即为malloc和free做了钩子,用于记录内存分配信息。

Linux下面也有原理相同的方法——mtrace。mtrace的原理是记录每一对malloc-free的执行,若每一个malloc都有相应的free,则代表没有内存泄露,对于任何非malloc/free情況下所发生的内存泄露问题,mtrace并不能找出来。

动态检测

见:Linux下几款C++程序中的内存泄露检查工具

这里推荐一个新的工具:

经典而强大的工具 gperftools。gperftools 是 google 开源的一个工具集,包含了 tcmalloc,heap profiler,heap checker,cpu profiler 等等

gperftools 的一些经典用法,我们就不在这里进行介绍了,大家可以自行查看文档。而使用 gperftools 可以在不重启程序的情况下,进行内存泄露检查,这个恐怕是很少有人了解。

如何避免?

  • new delete 成对出现
  • 使用智能指针。
  • 使用资源类

三:设计一个文件下载服务.有大到 2G的文件,也有 500K的文件,问如何设计?

四:QQ,昵称的数据;需要修改昵称,内存放不下,如何组织(B,B+树是数据 库提供的,不能用.现在让你自己设计

五:32 位的代码往 64 位移动时需要考虑什么?

https://www.oracle.com/technetwork/cn/server-storage/solaris/ilp32tolp64issues-137107-zhs.html

六:两个线程交替打印AB

使用 wait notify

public class PrintAAndB01 {

    public static Boolean flag = true;
    // 控制循环次数
    public static int i = 0;
    public static Object lock = new Object();


    public static void main(String[] args) {
        FutureTask<Object> futureTaskA = new FutureTask<>(new PrintA());
        FutureTask<Object> futureTaskB = new FutureTask<>(new PrintB());
        new Thread(futureTaskA, "线程A").start();
        new Thread(futureTaskB, "线程B").start();
    }


    static class PrintA implements Callable {
        @Override
        public Object call() throws Exception {
            while (i < 10) {
                synchronized (lock) {
                    if (flag) {
                        // 调用 wait后 释放 lock 的对象监视器锁,然后阻塞等待被 notify 唤醒
                        lock.wait();
                    } else {
                        System.out.println(Thread.currentThread().getName() + "------ A");
                        flag = !flag;
                        i++;
                        lock.notify();
                    }
                }
            }
            return null;
        }
    }

    static class PrintB implements Callable {
        @Override
        public Object call() throws Exception {
            while (i < 10) {
                synchronized (lock) {
                    if (!flag) {
                        lock.wait();
                    } else {
                        flag = !flag;
                        System.out.println(Thread.currentThread().getName() + "------ B");
                        i++;
                        lock.notify();
                    }
                }
            }
            return null;
        }
    }
}

使用 condition + lock 实现

public class PrintAAndB03 {
    private int num = 0;
    private ReentrantLock lock = new ReentrantLock();
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();

    private void printABC(int aim, Condition curThread, Condition nextThread) {
        for (int i = 0; i < 3; ) {
            lock.lock();
            try {
                while (num % 2 != aim) {
                    //阻塞当前线程
                    curThread.await();
                }
                System.out.println(Thread.currentThread().getName());
                num++;
                i++;
                //唤醒下一个线程,而不是唤醒所有线程
                nextThread.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        PrintAAndB03 printABC_lock = new PrintAAndB03();
        new Thread(() -> {
            printABC_lock.printABC(0, printABC_lock.conditionA, printABC_lock.conditionB);
        }, "A").start();
        new Thread(() -> {
            printABC_lock.printABC(1, printABC_lock.conditionB, printABC_lock.conditionA);
        }, "B").start();
    }
}

七:造成线程死锁

八:多个线程同时到达后一起运行

Java并发编程实战之基础知识-同步工具类

九:LRU cache 设计

使用散列表和链表实现LRU缓存淘汰算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值