什么是垃圾回收
程序运行会产生各种各种的数据,那么这些数据存在于内存当中,这些数据不可能是永久存在的,无效的资源对象需要进行垃圾回收,释放内存
不同的编程语言都有GC垃圾回收
java语言自带GC垃圾回收器,并且有JVM自动进行垃圾回收,程序员主要关注代码实现,不关注垃圾回收 System.gc();
C/C++语言当中,程序员new一个对象,相当于申请了一块内存,如果需要释放资源得手动通过delete关键字进行内存的释放
常用算法:
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,
对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。关于 JVM 的 GC 算法
主要有下面几种:
引用计数算法
在对象头处维护一个counter,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减。当counter为0时,表明该对象已经被
废弃,不处于存活状态。这种方式一方面无法区分软、虛、弱、强引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放counter,永远不
能GC。
可达性分析算法
通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,
则证明该对象是不可用的。如果对象在进行可行性分析后发现没有与GC Roots相连的引用链,也不会理解死亡。它会暂时被标记上并且进行一次筛选,筛选
的条件是是否与必要执行finalize()方法。如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执
行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。
可达性分析算法是通过枚举根节点来实现的,最重要的问题是GC停顿。为了确保一致性(即所有对象之间的关系是确定下来的)而导致GC进行时必须进行停顿。
在HotSpot的中,使用OopMap的数据结构存储特定位置上的调试信息,存储栈上那个位置原来是什么东西,这个信息是在JIT编译时跟机器码一起产生的。因
为只有编译器知道源代码跟产生的代码的对应关系。 这样,GC在扫描时就可以得知这些信息了。这样做的目的是使HotSpot能够快速准确的完成GC Roots枚
举,以期望减少GC停顿所带来的影响。HotSpot没有在所有的指令生成OopMap,所以只是在“特定位置”记录这些信息,这些位置就是安全点。程序执行时并
非在所有的位置上都能停顿下来GC,只有在到达安全点时才能暂停。安全点选取基本上是以“是否让程序长时间执行的特征”选定。此外,HotSpot虚拟机在安
全点的基础上还增加了安全区域的概念,安全区域是安全点的扩展。在一段安全区域中能够实现安全点不能达成的效果。
标记清除法(Mark-Sweep)
算法思想:
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象
是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC
操作。
优点
最大的优点是,相比于引用计数法,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。 此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置(后面俩算法涉及到移动位置的问题)。
缺点
很长的幽灵时间,判断对象已经死亡,消耗了很多时间,这样从对象死亡到对象被回收之间的时间过长。 每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。
没有移动对象,导致可能出现很多碎片空间无法利用的情况。
如下图所示:
标记压缩算法
标记压缩算法可以解决标记清除算法的内存碎片问题。
其算法可以看作三步:
-
-
标记垃圾对象
-
清除垃圾对象
-
内存碎片整理
-
其过程如下:
首先标记除垃圾对象(黄色)
清除垃圾对象
内存碎片整理
复制算法
算法思想
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。注意:
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。优点
实现简单不产生内存碎片
缺点
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半分代算法
分代算法基于复制算法和标记压缩算法。
首先,标记清除算法、复制算法、标记压缩算法都有各自的缺点,如果单独用其中某一算法来做GC,会有很大的问题。
例如,标记清除算法会产生大量的内存碎片,复制算法会损失一半的内存,标记压缩算法的碎片整理会造成较大的消耗。
其次,复制算法和标记压缩算法都有各自适合的使用场景。
复制算法适用于每次回收时,存活对象少的场景,这样就会减少复制量。
标记压缩算法适用于回收时,存活对象多的场景,这样就会减少内存碎片的产生,碎片整理的代价就会小很多。
分代算法将内存区域分为两部分:新生代和老年代。
根据新生代和老年代中对象的不同特点,使用不同的GC算法。
新生代对象的特点是:创建出来没多久就可以被回收(例如虚拟机栈中创建的对象,方法出栈就会销毁)。也就是说,每次回收时,大部分是垃圾对象,所以新生代适用于复制算法。
老年代的特点是:经过多次GC,依然存活。也就是说,每次GC时,大部分是存活对象,所以老年代适用于标记压缩算法。
新生代分为eden区、from区、to区,老年代是一整块内存空间,如下所示:
分代算法执行过程
首先简述一下新生代GC的整个过程(老年代GC会在下面介绍):新创建的对象总是在eden区中出生,当eden区满时,
会触发Minor GC,此时会将eden区中的存活对象复制到from和to中一个没有被使用的空间中,假设是to区(正在被使用的from区
中的存活对象也会被复制到to区中)。
有几种情况,对象会晋升到老年代:
超大对象会直接进入到老年代(受虚拟机参数-XX:PretenureSizeThreshold参数影响,默认值0,即不开启,单位为Byte,例
如:3145728=3M,那么超过3M的对象,会直接晋升老年代)
如果to区已满,多出来的对象也会直接晋升老年代
复制15次(15岁)后,依然存活的对象,也会进入老年代
此时eden区和from区都是垃圾对象,可以直接清除。