C/C++的内存管理
Java的内存管理
java中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。通过垃
圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。其他很多现代语言比如C#、Python、Go都拥有自己的垃圾回收器
垃圾回收的对比
**自动垃圾回收 **自动根据对象是否使用由虚拟机来回收对象
? 优点:降低程序员实现难度、降低对象回收bug的可能性
? 缺点:程序员无法控制内存回收的及时性
**手动垃圾回收 **由程序员编程实现对象的删除
? 优点:回收及时性高,由程序员把控回收的时机
? 缺点:编写不当容易出现悬空指针、重复释放、内存泄漏等问题
线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。而方法的栈帧在执行完方法之后就会
自动弹出栈并释放掉对应的内存。
方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:
1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
2、加载该类的类加载器已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用。
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还 在使用,不允许被回收。
常见的有两种判断方法:引用计数法和可达性分析法。
引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。
优点:
缺点:
1.每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
GC Root(垃圾收集根节点)是指在垃圾回收过程中被认为是活动对象并且不能被回收的对象。GC Root对象具有特殊的引用关系,保持着对其他对象的直接或间接引用,而其他对象通过这些引用与GC Root对象相互关联。它们被认为是内存中的起始点,GC Root对象以及可以从GC Root对象直接或间接访问到的对象都会被视为活动对象,并且不会被垃圾回收器回收。
基本原理:
- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收
软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。
软引用的执行过程如下:
1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。
2.内存不足时,虚拟机尝试进行垃圾回收。
3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
4.如果依然内存不足,抛出OutOfMemory异常。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
SoftReference提供了一套队列机制:
1、软引用创建时,通过构造器传入引用队列
2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
3、通过代码遍历引用队列,将SoftReference的强引用删除
弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。 在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道
直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;
终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后 由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该 对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。
1、找到内存中存活的对象
2、释放不再存活对象的内存,使得程序能再次利用这部分空间
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所
有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。
1.吞吐量
吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 /
(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高
2.最大暂停时间
最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最
大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响就越短。
**3.堆使用效率 **
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算
法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。
标记清除算法的核心思想分为两个阶段:
优缺点:
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:1. 碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小无法进行分配。
2. 分配速度慢。由于内存碎片的存在,需要维护一个空闲链表(记录着每块空闲内存的地址),极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法的核心思想是:
完整的复制算法的例子:
复制算法的优缺点
复制算法只需要遍历一次存活对象复制到To空间即可,比**标记-整理(清除对象一次,整理对象一次)**算法少了一次遍历的过程,因而性 能较好,但是不如标记-清除算法, 因为标记清除算法不需要进行对象的移动
复制算法在复制之后就会将对象按顺序放 入To空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
每次只能让一半的内存空间来为创 建对象使用
标记整理算法也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
所有存活对象。
优缺点
优点
缺点
分代垃圾回收将整个内存区域划分为年轻代和老年代
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
为什么分代GC算法要把堆分成年轻代和老年代?
故有以下几个原因:
在arthas中查看分代之后的内存情况:
垃圾回收器的组合关系
垃圾回收器是垃圾回收算法的具体实现。 由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。 具体的关系图如下:
它是 Serial 收集器的多线程版本。
Parallel Scavenge允许手动设置最大暂停时间和吞吐量。
Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小。
停顿时间较短:因为多个线程进行垃圾回收,回收速度快
吞吐量低:多线程占用CPU执行时间长,导致吞吐量低
与 ParNew 一样是多线程收集器。
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。用户体验好。
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。
具有以下缺点:
SerialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器。
而G1设计目标就是将上述两种垃圾回收器的优点融合:
1.支持巨大的堆空间回收,并有较高的吞吐量。
2.支持多CPU并行垃圾回收。
3.允许用户设置最大暂停时间。
JDK9之后强烈建议使用G1垃圾回收器。
G1垃圾回收器 – 内存结构
G1出现之前的垃圾回收器,内存结构一般是连续的,如下图:
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、Survivor、Old区。Region的大小通过堆空间大小/2048计算得到,也可以通过参XX:G1HeapRegionSize=32m指定(其 中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M。
G1垃圾回收有两种方式:
G1垃圾回收器 – 执行流程
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。 比如 -XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region。
G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率
混合回收
Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同:
从以下四个方面回答
在Java内存运行时区域的各个部分中分为堆、栈、方法区。栈是线程独占的,与线程的生命周期相同。所以;主要就是回收方法区和堆区。
引用计数算法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值 就减一;任何时刻计数器为零的对象就是不可能再被使用的。
但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这 个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的 引用计数就很难解决对象之间相互循环引用的问题。
举个简单的例子:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问, 但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
可达性分析算法:
当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
如下图所示,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
标记清除算法、标记整理算法、复制算法、分代回收算法
又分为老年代、新生代的垃圾回收器,上面有详解,这里就不过多赘述了。
新创建的对象一般会被分配在新生代中,常用的新生代的垃圾回收器是ParNew 垃圾回收器,它按照8:1:1 将新生代分成Eden 区,以及两个Survivor 区。某一时刻,我们创建的对象将Eden 区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,Minor GC 就触发了。
在正式Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。为什么要做这样的检查呢?原因很简单,假如Minor GC 之后Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:
则”,具体来说就是看-XX:-HandlePromotionFailure
参数是否设置了。
老年代空间分配担保规则是这样的,如果老年代中剩余空间大小,大于历次Minor GC 之后剩余对象的大小,那就允许进行Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:
开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:
前面都是成功GC 的例子,还有3 中情况,会导致GC 失败,报OOM:
Full GC会“Stop The World”,即在GC期间全程暂停用户的应用程序。
当Eden 区的空间耗尽时Java 虚拟机便会触发一次Minor GC 来收集新生代的垃圾,存活下来的对象, 则会被送到Survivor 区,简单说就是当新生代的Eden区满的时候触发Minor GC。
serial GC 中,老年代内存剩余已经小于之前年轻代晋升老年代的平均大小,则进行Full GC4。
而在CMS等并发收集器中则是每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行Full GC 回收。
可以采用以下措施来减少Full GC的次数:
虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生, 如果经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
设置。
因为老年代保留的对象都是难以消亡的,而标记复制算法在对象存活率较高时就要进行较多的复制操 作,效率将会降低,所以在老年代一般不能直接选用这种算法。
如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每 次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大 量的空间。如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来 回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
内存泄漏(memory leak):内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏。
内存溢出(out of memory):简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。