JVM垃圾回收算法
1 什么时候回收垃圾?
1、什么场景下该使用什么垃圾回收策略?
在对内存要求苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多内存。
在CPU使用率高的情况下:降低高并发时垃圾回收的频率,让CPU更多的去执行你的业务而不是垃圾回收。
2、垃圾回收发生在哪些区域?
堆(回收对象)、方法区(不用的常量、类)
3、对象什么时候被回收?
引用计数法:通过对象的引用计数器来判断对象是否被引用。无法处理循环引用问题。
A --> B --> C --> D --> B 此时B被引用了两次
当A不再引用B
A B --> C --> D --> B BCD循环引用无法被回收。
可达性分析法:以根对象(GC Roots)为起点向下搜索,形成引用链。如果对象不在引用链上,便视作对象不可达,可以回收。
哪些对象可以作为GC Roots对象?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即 Native方法)引用的对象
注意的是,一个对象不可达,也不一定会被回收。
public class GCTest {
private static GCTest obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize方法被调用了");
obj = this; // 重新引用对象,从FQ队列中出队
}
public static void main(String[] args) throws InterruptedException {
obj = new GCTest();
obj = null;
System.gc();
TimeUnit.MILLISECONDS.sleep(1000);
extracted();
TimeUnit.MILLISECONDS.sleep(1000);
obj = null; // finalize()方法已被调用过,对象会被直接回收
System.gc();
extracted();
}
private static void extracted() {
if (obj == null) {
System.out.println("obj == null");
} else {
System.out.println("obj可用");
}
}
}
finalize方法被调用了
obj可用
obj == null
finalize()的建议
-
避免使用finalize()方法,操作不当可能会导致问题。上面的例子中,第二次回收时如果不设置obj==null,对象很难被回收掉。
-
finalize()优先级低,何时会被调用无法确定,因为什么时间发生GC不确定。
-
建议使用try...catch...finally来替代finalize()。
4、四种引用
强引用,例如new对象,永远不会回收对象。
软引用,描述有用但是非必需对象,只有内存不足时被回收。
弱引用,描述非必需对象,无论内存情况都会被回收。
虚引用,主要用来跟踪对象被垃圾回收器回收的活动,结合引用队列使用。不影响对象的生命周期。
Object obj = new Object();
SoftReference<String> sr = new SoftReference<>("helllo");
WeakReference<String> sr = new WeakReference<>("helllo");
ReferenceQueue< String> queue = new ReferenceQueue<>();
PhantomReference< String> pr= new PhantomReference<>(“hello“queue)
2 垃圾回收算法
垃圾回收算法
基础垃圾回收算法
-
标记清除法(Mark-Sweep):标记后直接删除。
- 容易产生内存碎片;分配大内存时速度受到影响,需要找到适合它大小的空间。
-
标记整理法(Mark-Compact):标记,内存整理到一侧,删除三步。
- 整理存在开销,没有碎片。
-
复制(Copy):内存分为两块,每次只使用其中一块;将存活对象复制到另一块内存,然后清空当前内存;交换使用另一块内存。
- 内存利用率低,性能好,没有碎片。
综合垃圾回收算法
- 分代回收算法:把内存分为多个区域,不同区域使用不同的回收算法。
- 增量算法:每次只收集一小块内存区域的垃圾。
分代收集算法
商业虚拟机普遍采用分代收集算法,根据对象的存活周期,把内存分为多个区域,不同区域使用不同的回收算法。
分代的好处是更有效的清除不再需要的对象,提升垃圾回收的效率。
- 新生代回收(Minor GC | Young GC)
- 老年代回收(Major GC),通常伴随Full GC,Major GC≈ Full GC
- 清理整个堆(Full GC)
典型的对象分配流程如下。
新生代采用的是复制算法,存放存活周期短的对象。老年代对象存活时间长,采用标记-清除或标记-整理算法。
堆的年轻代和老年代,内存比例1:2。年轻代分为Eden和Survivor,内存比例8:1:1。
1. 对象首先存放到Eden中,当Eden内存满后,垃圾收集线程执行minor gc,通过GC Root进行可达性分析,找到非垃圾对象,复制到Survivor S0区(对象头中分代年龄标记为1),清空Eden区域,Eden又可以存储新对象。
2. 当Eden再次满后,同时对Eden和S0执行minor gc,将非垃圾对象复制到S1(分代年龄加1),清空Eden和S0。S0和S1,同一时间只有一个被使用。
3. 当分代年龄达到15后,对象会被复制到老年代。
4. 当老年代满了,会对年轻代和老年代执行full GC。full GC不起作用,则会OOM。
命令行输入jvisualvm
,打开Java VisualVM
工具的Visual GC插件
可以看到程序执行过程中堆容量变化。每次Eden满进行GC,调整堆区域。
非典型的情况:
- 新建的对象不一定分配到伊甸园。
-
对象大于
-XX:PretenureSizeThreshold
,就会直接分配到老年代 -
新生代空间不够。例如大数组。
- 对象也不一定要达到年龄才进入老年代
动态年龄:如果Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。
触发垃圾回收的条件
Minor GC触发条件:伊甸园空间不足
Full GC触发条件:
- 老年代空间不足(老年代已满;空间碎片太多,没有连续内存分配对象)
- 元空间不足
- 要转成老年代对象所需的空间大于老年代剩余空间。
- 显示调用System.gc(),建议垃圾回收器执行垃圾回收。设置
-XX:+DisableExplicitGC
参数忽略该操作。
分代收集调优思路
- 让GC尽量发生在新生代,尽量减少Full GC的发生。
- 合理设置Survior区域的大小,避免内存浪费。(复制算法)