JVM学习04-垃圾回收概念与算法
一、常用的垃圾回收算法
- 引用计数法
为对象配备一个整型计数器,只要有任何一个对象引用了这个对象,这个对象的计数器就加1,引用失效时引用计数器就减1。只要对象的引用计数器的值为0,则对象就不可能再被使用。
问题:
无法处理循环引用的情况。
每次因引用产生和消除,都需要伴随一个加法和减法操作,对系统性能会有一定影响。
Java虚拟机并未选择此算法作为垃圾回收算法。
- 标记清除法(Mark-Sweep)
标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
- 标记阶段,通过根节点标记所有的从根节点开始的可达对象。未被标记的对象就是未被引用的垃圾对象
- 清除阶段,清除所有未被标记的对象。
问题:回收后的空间不连续,会产生空间碎片。在对象的堆空间分配过程中,尤其是大对象的内在分配,不连续内在空间的工作效率要低于连续空间。
- 复制算法
将原有的内在空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中存活的对象复制到未使用的内在块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
优点:效率高、没碎片
效率高体现在:如果垃圾多,要复制的存活对象相对会少。
没碎片体现在:对象在垃圾回收过程中被统一复制到新内存空间中,所以可确保没碎片。
缺点:系统内存折半
在Java新生代串行垃圾回收器中,使用了复制算法思想。新生代分为eden、from、to三个部分。from和to可视为用于复制的两块大小相同、地位相等且可进行角色互换的空间块。
from和to空间也被称为survivor空间,即幸存者空间用于存放未被回收的对象,如下图。
- 新生代 :存放年轻对象的堆空间。年轻对象指刚刚创建的或经历垃圾回收次数不多的对象。
- 老年代:存放老年对象的堆空间。老年对象就是经历多次垃圾回收依然存活的对象。
垃圾加收时,eden空间中存活的对象,会被复制到未使用的survivor空间中(假设是to ),正在使用的survivor空间(假设是from) 中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间满了,对象也会直接进入老年代)。此时 eden和from空间中的剩余对象就是垃圾对象,可以直接清空,to空间则存放此次回收后的存活对象。
这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间的浪费。
PS:复制算法比较适合用于新生代。因为在新生代,垃圾对象通常会多于存活对象,复制算法的效果会比较好。
- 标记压缩法
老年代大部分对象是存活对象,标记压缩法是一种老年代的回收算法,先从根节点开始对所有可达对象做一次标记,之后将所有的存活对象压缩到内存的一端,之后清理边界外所有的空间。
优点:既避免了碎片的产生,又不需要两块相同的内在空间,性价比较高。
- 分代算法
即前面的汇总,不同的内存区间使用不同的回收算法。
新生代回收频率高,每次回收耗时短
老年代回收频率低,耗时长。
为了支持高频率的新生代回收,虚拟机可能使用一种叫卡表的数据结构。卡表为一个比特位集合,每个比特位表示老年代区域中所有对象是否持有新生代的对象引用。这样在新生代GC时,可以不用花大量时间扫描所有老年代对象来确定每个对象的引用关系,先扫描卡表,只有卡表的标记位为1时,才需要扫描特定老年代对象。卡表位为0的所在区域必定不含有新生代对象引用。
- 分区算法
分区算法将整个堆空间划分为连续不同的小区间,每个小区间都独立使用独立回收。
优点:可以控制一次回收多少个小区间,更好控制GC产生的停顿时间。每次合理地回收若干个小区间,而不是整个堆空间。(有没有使用场景?比如什么版本的的虚拟机这么用)
二、判断可触及性
可触及性包含三种状态
- 可触及:从根节点开始,可以到达这个对象
- 可复活:对象的所有引用都被释放,但对象有可能在finalize()函数中复活。
- 不可触及的:对象的finalize()函数被调用,并且没有复活,就会进入不可触及状态。
以上三种状态中,只有对象不可触及时,才可以被回收。
对象复活
- finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)
- finalize()函数可能引用外泄,无意中复活对象
finalize()被系统调用,调用时间不明确,推荐在try-catch-finally中进行资源释放。
public class CanReliveObj { public static CanReliveObj obj; public static void main(String[] args) throws InterruptedException { obj = new CanReliveObj(); obj = null; //这里调用 gc以后,会调用到finalize()方法,将obj对象复活 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } System.out.println("第二次gc"); obj = null; // finalize()方法这次不会被调用,所以这回 obj对象真正被回收 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("CanReliveObj finalize called"); obj = this; } @Override public String toString() { return "I am CanReliveObj"; } }
引用和可触及性的强度
强引用、软引用、弱引用、虚引用,不知道干啥用的,先略过。
强引用特点:
- 强引用可以直接访问目标对象
- 强引用指向的对象在任何时候都不会被系统回收,虚拟机宁可OOM,也不回收强引用指向的对象
强引用可能导致内存泄漏
public class StrongReference { public static void main(String[] args) { StringBuilder sb1 = new StringBuilder("abcde"); StringBuilder sb2 = sb1; System.out.println(sb1 == sb2); System.out.println(sb1.equals(sb2)); sb1 = new StringBuilder("fgihjk"); System.out.println(sb1 == sb2); System.out.println(sb1.equals(sb2)); } } 运行结果 true true false false
- 垃圾回收时的停顿现象:Stop-The-World
import java.util.HashMap;
/**
* -Xmx1g -Xms1g -Xmn512k -XX:+UseSerialGC -XX:+PrintGCDetails -Xloggc:StopWorldTestGcLog.log
* @author dmn
*/
public class StopWorldTest {
public static void main(String args[]) {
MyThread t = new MyThread();
PrintThread p = new PrintThread();
t.start();
p.start();
}
/**
* 不断申请内存操作
*/
public static class MyThread extends Thread {
HashMap map = new HashMap();
@Override
public void run() {
try {
while (true) {
// System.out.println((map.size() * 512) / 1024 / 1024);
if (map.size() * 512 / 1024 / 1024 >= 880) {
map.clear();
System.out.println("clean map");
}
byte[] b1;
for (int i = 0; i < 100; i++) {
b1 = new byte[512];
map.put(System.nanoTime(), b1);
}
Thread.sleep(1);
}
} catch (Exception e) {
}
}
}
/**
* 输出时间
*/
public static class PrintThread extends Thread {
public static final long starttime = System.currentTimeMillis();
@Override
public void run() {
try {
while (true) {
long t = System.currentTimeMillis() - starttime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(100);
}
} catch (Exception e) {
}
}
}
}
日志里可以看到 [Times: user=0.83 sys=0.01, real=0.85 secs] 的real就是Full GC实际花费的时间
29.998: [Full GC (Allocation Failure) 29.998: [Tenured: 1048063K->1048063K(1048064K), 0.7081273 secs] 1048511K->1048461K(1048512K), [Metaspace: 8695K->8695K(1056768K)], 0.7083946 secs] [Times: user=0.70 sys=0.01, real=0.71 secs]
30.707: [Full GC (Allocation Failure) 30.707: [Tenured: 1048063K->1048063K(1048064K), 0.7488220 secs] 1048511K->1048511K(1048512K), [Metaspace: 8593K->8593K(1056768K)], 0.7489386 secs] [Times: user=0.73 sys=0.01, real=0.75 secs]
31.456: [Full GC (Allocation Failure) 31.456: [Tenured: 1048063K->1031386K(1048064K), 0.8560296 secs] 1048511K->1031386K(1048512K), [Metaspace: 8563K->8563K(1056768K)], 0.8560950 secs] [Times: user=0.83 sys=0.01, real=0.85 secs]
用VisualGC看效果可太炫酷了。