目录
- 前言
- 一、基础概念
- 二、垃圾回收算法
- 三、垃圾收集器
- 四、引用
- 后记
前言
本篇主要介绍Java垃圾回收机制——GC的相关内容。
“基础知识”是本专栏的第一个部分,本篇博文是第五篇博文,如有需要,可:
- 点击这里,返回本专栏的索引文章
- 点击这里,返回上一篇《【Java校招面试】基础知识(四)——JVM》
一、基础概念
01. 垃圾回收(GC)发生的位置
垃圾回收发生在JVM内存模型的堆内存
中
02. 对象被判定为垃圾的标准
没有被其他对象引用。
03. 判定对象是否为垃圾的算法
1) 引用计数算法: 通过判断对象的引用数量来决定对象是否可以被回收。每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。任何引用计数为0的对象实例可以被当作垃圾收集。
- 优点: 执行效率高,程序执行受影响小。
- 缺点: 无法检测出循环引用的情况,会导致内存泄漏。
2) 可达性分析算法: 通过判断对象的引用链是否可达来决定对象是否可以被回收;
04. 可作为GC Root的对象
1) 虚拟机栈中引用的对象(栈帧中的本地变量表);
2) 方法区中的常量引用对象;
3) 方法区中的类静态属性引用的对象;
4) 本地方法栈中JNI(Native方法)的引用对象;
5) 活跃线程的引用对象。
二、垃圾回收算法
01. 标记清除算法(Mark and Sweep)
- 标记: 从根集合进行扫描,对存活的对象进行标记
- 清除: 对堆内存从头到尾进行线性遍历,回收不可达对象内存
- 存在的问题: 产生大量不连续的碎片,造成堆内存碎片化
02. 复制算法(Copying)
将堆内存按照一定的比例分成两块或多块,分为对象面和空闲面。选择其中的一块或两块作为对象面,其他的作为空闲面。对象主要在对象面上创建。当对象面的内存用完时,将存活的对象从对象面复制到空闲面。然后将对象面多有对象内存清除。适用于对象存活率低的场景。
- 优点:
1) 解决了碎片化的问题;
2) 顺序分配内存,简单高效;
3) 适用于对象存活率较低的场景; - 缺点: 在对象存活率较高的场景中,需要进行较多的复制
03. 标记整理算法(Compacting)
- 标记: 从根集合进行扫描,对存活的对象进行标记
- 清除: 移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
优点:
1) 避免内存的不连续性;
2) 不用设置两块内存互换;
3) 适用于对象存活率较高的场景;
04. 分代收集算法(Generational Collector)——目前的主流垃圾回收算法
- 特点:
1) 垃圾回收算法的组合;
2) 按照对象生命周期的不同划分区域以采取不同的垃圾回收算法; - 目的: 提高JVM的回收效率
05. JDK 8中对堆内存所作的调整
1) JDK 8以前及JDK 8的JVM内存模型区别
从图中可见,JDK 8中,从堆内存中去除了Permanent Generation(永久代)
,取而代之的是在本地内存中新设的Meta Space(元空间)
2) 永久代中存放的数据
① 类的元数据(如类的名称、访问修饰符、父类、实现的接口等)
② 常量池
③ 静态变量
④ 即时编译器编译后的代码
⑤ 一些需要在类加载时完成的初始化操作
3) 永久代存在的问题(被元空间替代的原因)
① 内存限制问题: 永久代的大小是通过-XX:MaxPermSize参数来指定的,而该参数的默认值较小,容易导致OutOfMemoryError异常。
② 类加载器泄漏问题: 在永久代中,存在着一些类加载器无法被回收的情况,导致永久代的内存无法释放,最终也会导致OutOfMemoryError异常。
③ 字符串常量池溢出问题: 由于字符串常量池被存储在永久代中,如果不合理地使用字符串常量,就会导致永久代的内存快速耗尽,最终也会导致OutOfMemoryError异常。
④ 性能问题: 永久代的垃圾回收效率很低,会导致应用程序的性能下降。
⑤ 维护成本问题: 永久代需要手动调整大小,而且需要进行垃圾回收,增加了应用程序的维护成本。
4) 永久代和元空间的区别
① 存储位置: 永久代位于JVM堆内存中,而元空间则是位于本地内存中。
② 大小限制: 永久代的大小是固定的,而且它很容易被填满,导致OutOfMemoryError。而元空间的大小是动态的,它可以根据需要来自动调整大小,因此更加灵活。
③ 垃圾回收: 永久代是由JVM的垃圾回收机制来进行管理和清除的。而元空间不再使用垃圾回收机制进行管理,而是使用本地内存的机制,例如使用操作系统的虚拟内存来管理。
④ 移植性: 由于永久代是Java虚拟机的一部分,因此它只能在Java虚拟机中使用。而元空间是本地内存的一部分,因此它可以在不同的虚拟机和操作系统中使用。
06. 垃圾回收器的分类
- Minor GC: 针对年轻代的垃圾回收,通常情况下只会回收年轻代中的Eden区和一个Survivor区。
- Major GC: 针对老年代的垃圾回收,通常情况下会回收整个老年代。
- Full GC: 针对整个堆空间的垃圾回收,包括年轻代和老年代。
他们的区别主要有以下几点:
1) 发生频率不同: Minor GC频率很高,可能会发生多次,Major GC和Full GC发生频率较低。
2) 对应用程序的影响不同: Minor GC对应用程序的影响较小,Major GC和Full GC会导致较长的停顿时间,对应用程序的影响较大。
3) 回收效率不同: Minor GC的回收效率较高,Major GC和Full GC的回收效率较低。
07. 触发Full GC的条件(答出3点即可)
1) 老年代空间不足
2) 永久代空间不足(针对JDK 7及以下版本)
3) CMS GC时出现promotion failed
、concurrent mode failure
4) Minor GC晋升到老年代的平均大小大于老年代的剩余空间
5) 程序中手动调用System.gc()
6) 使用RMI(远程方式)来进行RPC或管理的JDK应用,每小时执行1次Full GC
08. 年轻代
用于存放并尽可能快速地收集掉那些生命周期短的对象
1) 包含一个Eden(伊甸园)区和两个Suivivor(一个From区,一个To区),比例为8:1:1
2) 每次将Eden和From中的存活对象复制到To区中,最后清理掉Eden和From区;
3) 每次复制,存活对象的年龄+1,当对象的年龄达到一定值(默认是15,可以通过-XX:MaxTenuringThreshold修改)时,对象将会被移动到老年代。
09. 老年代
存放生命周期较长的对象,占整个堆内存的2/3
10. 常见的GC调优参数
- -XX:SurvivorRatio, Eden和Suivivor的比值,默认8:1;
- -XX:NewRatio, 老年代和年轻代内存大小的比例;
- -XX:MaxTenuringThreshold, 对象聪年轻代晋升到老年代经过GC次数的最大阈值;
11. Stop-The-World
1) JVM由于要执行GC而停止了应用程序的执行;
2) 任何一种GC算法中都会发生;
3) 多数GC优化通过减少Stop-The-World发生的时间来提高程序性能。
12. SafePoint
1) 分析过程中对象引用关系不会发生变化的点;
2) 产生SafePoint的地方: 方法调用、循环跳转、异常跳转等。GC发生时,让所有的线程都跑到最新的安全点,如果发现线程不在安全点,则恢复线程,等其跑到安全点。
3) 安全点数量需要适中,太少会让GC等待太长的时间;太多会增加程序运行的负荷。
三、垃圾收集器
01. 新生代常见的垃圾收集器
1) Serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程;
- 简单高效,Client模式下默认的年轻代收集器。
2) ParNew收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样;
- 单核执行效率不如Serial,在多核下执行才有优势,默认开启的线程数与CPU核数相同;
- 在Server模式下重要的收集器,除Serial外,只有ParNew可以和CMS收集器配合工作。
3) Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 比起关注用户线程停顿时间,更关注系统吞吐量;
- 在多核下执行才有优势,Server模式下默认的年轻代收集器;
- 如果对于垃圾收集器运作原理不是很了解,在优化过程中遇到困难时,可以使用-XX:+UseAdaptiveSizePolicy把调优任务交给虚拟机。
02. 老年代常见的垃圾收集器
1) Serial Old收集器(-XX:+UseSerialOldGC,标记整理算法)
- 单线程收集,进行垃圾收集时,必须暂停所有工作线程;
- 简单高效,Client模式下默认的老年代收集器。
2) Parallel Old收集器(-XX:+UseParallelOldGC,标记整理算法)
多线程,吞吐量优先
3) CMS收集器(-XX:+UseConcMarkSweepGC,标记清除算法)
- 初始标记:Stop-The-World
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
存在的问题: 采用标记清除算法,不对内存进行压缩,会造成空间碎片化的问题
03. 既可用于老年代也可用于年轻代的GC
G1(Garbage First)收集器(-XX:+UseG1GC,复制+标记整理算法)
1) 特点:
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
2) 设计:
- 将整个Java堆内存划分成多个大小相等的Region
- 年轻代和老年代不再物理隔离
04. 所有垃圾收集器的关系
05. 两个在新的垃圾收集器
这两个垃圾收集器在JDK 11中作为实验特性引入,在JDK 15中已成为正式的垃圾收集器。
- Epsilon GC
- ZGC
06. Object类的finalize()方法的作用和C++的析构函数有什么不同?
1) C++中的析构函数是在对象生命周期结束时自动调用的,而不是在垃圾回收时调用的。
2) C++中的析构函数可以手动调用,而Java中的finalize()方法只能由垃圾回收器调用。
3) C++中的析构函数可以被重载,而Java中的finalize()方法是Object类的一个标准方法,不能被重载。
四、引用
01. Java中的强引用,软引用,弱引用,虚引用都是什么?
强引用(Strong Reference):
1) 最普遍的引用: Object obj = new Object();
2) 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象;
3) 通过将对象的引用设置为null来弱化引用,使其被回收。
软引用(Soft Reference):
1) 对象处在有用但非必须的状态;
2) 只有当内存空间不足时,GC才会回收该引用的对象的内存;
3) 可以用来实现高速缓存;
4) 用法
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str);
弱引用(Weak Reference):
1) 非必需的对象,比软引用更弱一些;
2) GC时会被回收;
3) 由于GC线程优先级很低,被回收的概率也不大;
4) 适用于引用偶尔被使用且不影响垃圾收集的对象;
5) 用法:
String str = new String("abc");
WeakReference<String> weakReference = new weakReference<>(str);
虚引用(Phantom Reference):
1) 不会决定对象的生命周期;
2) 任何时候都可能被垃圾收集器回收;
3) 跟踪对象被垃圾收集器回收的活动,起哨兵的作用;
4) 必须和引用队列ReferenceQueue联合使用,因为它的get函数返回值永远为null,因此无法用get函数判断它是否已被回收;
5) 用法:
String str = new String("abc");
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(str, refQueue);
02. 四种引用的比较
强引用 > 软引用 > 弱引用 > 虚引用
引用类型 | 被回收的时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 垃圾回收时 | 对象缓存 | GC运行后终止 |
虚引用 | 未知/瞬间 | 标记、哨兵 | 未知/瞬间 |
03. 引用队列(ReferenceQueue)
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达;
- 存储关联的且被GC的软引用、弱引用和虚引用。
04. 引用队列的代码示例
1) 目标对象MyObject
public class MyObject {
public String name;
public MyObject(String name){
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.printf("Finalizing Object %s", name);
super.finalize();
}
}
2) 目标对象弱引用MyObjectWeakReference
public class MyObjectWeakReference extends WeakReference<MyObject>{
public String name;
public MyObjectWeakReference(MyObject referent, ReferenceQueue<MyObject> q) {
super(referent, q);
this.name = referent.name;
}
}
3) 调用类
public class ReferenceQueueTest {
private static ReferenceQueue<MyObject> q = new ReferenceQueue<>();
private static void checkQueue() {
Reference ref;
while ((ref = (Reference<MyObject>) q.poll()) != null) {
System.out.println("In Queue: Weak Reference of " + ((MyObjectWeakReference) ref).name);
System.out.println("Reference Object " + ref.get());
}
}
public static void main(String[] args) {
List<WeakReference<MyObject>> refList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
refList.add(new MyObjectWeakReference(new MyObject("MyObject" + i), q));
System.out.println("Created Weak Reference " + refList.get(i));
}
System.out.println("First Time Check.");
checkQueue();
System.gc();
try {Thread.sleep(1000);} catch (InterruptedException ie) {}
System.out.println("Second Time Check.");
checkQueue();
}
}
4) 代码流程分析
i) 创建3个弱引用对象;
ii) 输出引用队列中的所有引用对象;
iii) 手动gc并等待其结束(这里是让线程睡眠1秒);
iv) 再次输出引用队列中的所有引用对象。
5) 代码输出
Created Weak Reference referencequeuetest.MyObjectWeakReference@15db9742
Created Weak Reference referencequeuetest.MyObjectWeakReference@6d06d69c
Created Weak Reference referencequeuetest.MyObjectWeakReference@7852e922
First Time Check.
Finalizing Object MyObject2
Finalizing Object MyObject1
Finalizing Object MyObject0
Second Time Check.
In Queue: Weak Reference of MyObject2
Reference Object null
In Queue: Weak Reference of MyObject1
Reference Object null
In Queue: Weak Reference of MyObject0
Reference Object null
6) 代码输出解析
i) 创建3个弱引用对象;
ii) 首次检查时,因为还没有发生GC,所以引用队列里什么都没有,没有任何输出;
iii) GC发生,MyObject的finalize方法被调用;
iv) 第二次检查时,因为已经发生GC,所以3个MyObjectWeakReference都在RefernceQueue里;
v) 因为已经发生GC,所以通过ref.get()得到的对象为null;
后记
GC(垃圾收集)部分内容也不少,我将这些知识点分为基础概念
、垃圾回收算法
、垃圾收集器
和引用
4个部分,便于阅读和查找。