文章目录
一:如何定义一个只能在堆上生成对象的类?
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() {}
};
二:内存池的设计?细节,怎么获取,怎么使用,怎么归还?
(1)为什么需要设计内存池?
通常我们用new或malloc来分配内存的话,由于申请的大小不确定,所以当频繁的使用时会造成内存碎片和效率的降低
。
内存池就是:先申请分配一个大的内存块留作备用。当真正需要使用内存的时候,就从内存池中分配一块内存使用,当这块使用完了之后再还给内存池。若是内存块不够了就向内存再申请一块大的内存块。
可以看出这样做有两个好处:
- 由于向内存申请的内存块都是比较大的,所以能够降低外碎片问题。
- 一次性向内存申请一块大的内存慢慢使用,避免了频繁的向内存请求内存操作,提高内存分配的效率。
(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。
(4)C++ 分配器中内存池的设计?
六:内存泄漏的姿势
什么是内存泄漏?
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。泄漏的区域一般就是堆存储区。主要原因 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并不能找出来。
动态检测
这里推荐一个新的工具:
经典而强大的工具 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();
}
}