垃圾回收概述
什么是垃圾
运行程序中没有任何指针指向的对象
为什么要垃圾回收
对内存进行回收,避免内存耗尽。垃圾回收的同时可以对内存碎片进行整理。
早期垃圾回收
在C,C++中,内存需要程序员使用完后手动回收。
Java垃圾回收机制
内存回收由垃圾回收器负责,无需程序员参与。
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。
频繁回收年轻代,较少回收老年代,基本不懂永久代
对象的finalization机制
Java提供了对象终止(finalization)机制允许开发人员提供对象被销毁值之前的自定义处理逻辑
垃圾回收器在回收一个对象前,会调用对象的finalize方法。finalize方法是Object中的方法
finalize()可以在子类被重写,用于对象被回收时进行资源释放(关闭文件,套接字,数据库连接)
不要主动调用finalize方法,原因:
(1)在finalize()时可能会导致对象复活。
(2)finalize()方法的执行时间是没有保障的,由GC线程决定。极端情况下,如果不发生GC,则finalize将没有执行机会。
(3)糟糕的finalize()会严重影响GC的性能。
基于finalize的对象的三种状态
(1)可触及的:可达性分析算法下,可以到达
(2)可复活的:可达性分析算法下,不可以到达,但是对象有可能在finalize中复活
(3)不可触及的:finalize方法被调用,对象没有复活。finalize方法在对象的声明周期中只能调用一次。
只有对象为不可触及时可以被回收
判断一个对象是否可回收,需要经历两次标记过程
(1)对象到GC Roots没有引用链,进行第一次标记。
(2)进行筛选,判断对象是否有必要执行finalize方法
(1)对象没有重写finalize方法,或者finalize方法已经执行过。则虚拟机认为不必执行,对象认为是不可触及的
(2)finalize方法重写,并且没有执行过,对象加入到F-Queue队列中,由一个虚拟机自动创建的,低优先级的finalizer线程
出发其finalize方法执行。
(3)finalize方法是对象逃离死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果对象在引用链上,则移出
即将回收集合。当对象再次出现不在引用链上时,对象直接成为不可及。
垃圾回收相关算法
标记阶段的算法
在垃圾回收器进行垃圾回收之前,我们需要找出堆中哪些对象是需要回收的对象。
引用计数法
-
介绍
每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。当对象被其他对象引用时,引用计数器加1,当引用失效时,引用减1
当引用计数器的值为0时,说明对象不再被使用,可以回收。 -
优点
简单,垃圾对象便于辨识,判断效率高,回收没有延迟性
-
缺点
需要一个字段存储计数器,有一定的空间开销
引用改变时需要对计数器进行修改,由一定的时间开销
无法处理循环引用的问题。因此在Java中并没有使用此算法
-
补充
引用计数法系统吞吐量较高,在标记阶段不需要STW(stop the world)
python中选用了引用计数法
python解决循环引用的方式:(1)手动解除 (2)使用弱应用
可达性分析算法
可达性分析算法又称为(跟搜索算法,追踪性垃圾回收)
-
介绍
GC Roots:一组必须活跃的引用
以GC Roots中的对象为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
内存中活着的对象都会被GC Roots中的对象直接或间接连接。搜索走过的路径称为引用链。
如果目标对象不被任何引用链相连,则为不可达,意味着不再被使用,可以被回收。
-
优点
简单,高效,可以解决循环依赖的问题,Java选择了此种算法
-
缺点
使用可达性分析算法来判断内存是否可回收,分析工作需要在一个能保障一致性的快照中进行。
这是导致垃圾回收时必须Stop The World的一个重要原因
-
GC Roots中包含的对象
(1)栈帧中局部变量表中的对象(方法参数,局部变量)
(2)本地方法栈中引用的对象
(3)方法区中类静态属性引用的对象,方法区中常量引用的对象(字符串常量池中的引用)
(4)被同步锁synchronized持有的对象
(5)Java虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻的异常对象)
(6)反应Java虚拟机内部情况的JMXBean,JVMTI中注册的回溯、本地代码缓存
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾回收器以及当前回收的内存区域的不同,还可以有其他对象临时性的加入。
如果只针对Java堆中的某一块区域进行垃圾回收(比如只对新时代进行回收),这个区域的对象又可能被其他区域的对象引用。这时候需要将关联的区域对象也加入到GC Roots集合中考虑,才能保证可达性分析的准确性。
清除阶段的算法
标记-清除算法(Mark-Sweep)
-
执行过程
(1)从GC Roots开始遍历,标记所有被引用的对象,一般在对象的Header中记录为可达对象。
(2)对堆内存进行从头到尾的线性遍历,如果发现某个对象为不可达,就将其回收。
标记需要遍历一次,清除时需要遍历一次
-
回收是指什么
将地址空间加入到空闲地址列表中,加入新对象时,直接进行覆盖
-
缺点
效率不高;gc时需要STW;空闲内存不连续,需要维护空闲列表
复制算法(Copying)
核心思想:将空间分为两块,每次使用其中一块。在进行垃圾回收时,把存活对象复制到空闲内存中。清除使用的内存块中的所有对象。
只需要遍历一次,标记的同时进行复制。
-
优点
没有标记和情况过程;简单;高效
避免空间碎片问题
-
缺点
需要两倍内存
对于G1这种拆分为大量region的GC,复制而不是移动,意为GC需要维护region之间对象的引用关系,需要内存占用和时间开销
如果存活对象很多,那么复制量很大。因此适用于新生代
标记-压缩算法(Mark-Compact)
背景:复制算法使用与新生代淘汰率高的区域,不适用于老年代这种存活对象占大多数的区域。
-
执行过程
(1)标记所有可达对象
(2)将存活对象压到内存一端,按顺序排放
(3)清理边界外的空间
-
优点
没有内存碎片,分配新内存时修改指针的偏移量即可。
消除了复制算法内存减半的代价
-
缺点
效率不如复制算法
对象的移动意味着地址的改变,因此需要调整引用的地址
移动过程中需要STW
分代收集算法
分代收集算法的基础:不同对象的声明周期不同。对于不同生命周期的对象可以采用不同的收集方式。以提高回收效率。
一般把Java分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,提高垃圾回收的效率。
目前几乎所有的GC都是采用分代收集算法算法执行垃圾回收的
-
年轻代
年轻代的特点:区域相对老年代较小;生命周期较短,存活率低;回收频繁
适合复制算法,复制算法占用内存的问题可以通过设置Survivor区域的比例来缓解
-
老年代
老年代特点:区域加大;对象生命周期长;存活率高;回收频率较低
这种情况下,复制算法明显不合适。一般使用标记回收或标记压缩算法或混合使用