JVM 基础 - Java垃圾回收(GC)
文章目录
前言
Java垃圾回收主要是针对堆和方法区进行;程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
一、判断一个对象是否可被回收
1、引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class AA {
public Object instance = null;
public static void main(String[] args) {
AA objectA = new AA();
AA objectB = new AA();
objectA.instance = objectB;
objectB.instance = objectA;
objectA = null;
objectB = null;
}
}
解释:上图中,运行AA objectA = new AA(),生成一个对象1,此时对象1的引用数为1,运行AA objectB = new AA()生成一个对象2,此时对象2的引用数为2;运行objectA.instance = objectB 时,对象2的引用数+1,也就是对象2的引用数为2,同理,运行objectB.instance = objectA 时,对象1的引用数也变为2;运行objectA = null 时,对象1的引用数-1,此时对象1的引用数还剩1个,同理,运行objectB = null 时,对象2的引用数也还剩1个,对象1和对象2由于都还有引用(并且是相互引用),导致无法被回收。
2、可达性分析算法
通过 根对象集合(GC Roots) 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用可达性分析算法来判断对象是否可被回收
在 Java 中 GC Roots 一般包含以下内容:
1、虚拟机栈中引用的对象
2、本地方法栈中引用的对象
3、方法区中类静态属性引用的对象
4、方法区中的常量引用的对象
3、方法区的回收
方法区主要是对常量池的回收和对类的卸载。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
2、加载该类的 ClassLoader 已经被回收。
3、该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
二、引用类型
不管是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
1、强引用
被强引用关联的对象不会被回收,使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
2、软引用
被软引用关联的对象只有在内存不够的情况下才会被回收,使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
3、弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前,使用 WeakReference 类来实现弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
4、虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。 使用 PhantomReference 来实现虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
三、垃圾回收算法
1、标记-清除
将存活的对象进行标记,然后清理掉未被标记的对象。
不足:会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2、标记-整理
在标记-清除的基础上进行改进,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
相比标记-清除算法,优点是回收后,内存空间是连续的。
3、复制
复制算法是将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间全部清理掉。
复制算法的主要不足是永远只使用了内存的一半空间。
JVM中的新生代(年轻代)使用的就是复制算法回收垃圾,只不过不是划分为相同大小的两块内存,而是分配成1个Eden区和2个Survivor区,分配比例默认为8:1:1 。
4、分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
新生代使用: 复制算法
老年代使用: 标记 - 清除 或者 标记 - 整理 算法
四、垃圾收集器
1、Serial(串行) 收集器
以串行的方式执行,收集过程中其它线程阻塞,是单线程的收集器,每次只有一个线程进行垃圾收集工作,是新生代(年轻代)的收集器,基于复制算法实现,是 Client 模式下的默认新生代收集器,主要用于桌面应用。
2、ParNew 收集器
是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
3、Parallel Scavenge 收集器
被称为“吞吐量优先”收集器,是多线程的,是一个新生代的垃圾回收器,采用的是复制算法。关注的是程序到达一个可控制的吞吐量(Thoughput ,CPU 用于运行用户代码的时间/CPU总消耗时间)。
4、Serial Old 收集器
是 Serial 垃圾收集器的老年代版本,同样是个单线程的收集器,是基于标记-整理算法。也是给 Client 模式下的虚拟机使用。
5、Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。使用的是多线程-标记整理算法。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6、CMS 收集器
CMS 是基于标记-清除算法,设计的目的是减少停顿时间(这是以牺牲吞吐量为代价的,导致 CPU 利用率不够高)。基于标记清除算法,会存在内存碎片化的问题。
分为以下四个流程:
1、初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
2、并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
3、重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
4、并发清除: 不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
7、G1收集器
G1(Garbage First) 本质上是一个分带垃圾回收器。G1垃圾回收器相对 CMS 垃圾回收器,有两个改进:
1、基于标记-整理 算法,不产生内存碎片。
2、可以准确的控制停顿时间,在不牺牲吞吐的情况下实现低停顿的垃圾回收。
G1 为了避免全区域垃圾收集,把堆内存划分为大小固定的几个独立区域,并跟踪这些区域的回收进度。同时在后台维护一个优先列表,每次根据收集时间的,优先回收垃圾最多的区域。
其它收集器进行收集的范围都是单个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 引入了额外的概念,Region。G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
8、JDK默认使用的垃圾收集器
JDK8默认使用的是吞吐量优先的垃圾回收器Parallel Scavenge(年轻代)+Parallel Old(老年代),而JDK9默认使用的是G1垃圾回收器。